Skip to content

Comments

feat: add openspec init --global for tool global directory installation#753

Open
askpatrickw wants to merge 3 commits intoFission-AI:mainfrom
askpatrickw:init-global
Open

feat: add openspec init --global for tool global directory installation#753
askpatrickw wants to merge 3 commits intoFission-AI:mainfrom
askpatrickw:init-global

Conversation

@askpatrickw
Copy link

@askpatrickw askpatrickw commented Feb 24, 2026

Install skills and commands to tool global directories (~/.claude/, ~/.config/opencode/, ~/.codex/) instead of project directories. This lets consultants and multi-repo developers install once globally rather than per-project.

  • Add optional getGlobalRoot() method to ToolCommandAdapter interface
  • Implement for Claude (/.claude/), OpenCode (XDG-aware), Codex (/.codex/)
  • Return null from all 20 remaining adapters (Cursor, Windsurf, etc.)
  • Migrate Codex adapter: getFilePath() now returns project-relative path
  • Add --global flag to openspec init (requires --tools)
  • Add --global flag to openspec update
  • Add resolveGlobalRoot() helper with OPENSPEC_GLOBAL_ROOT env var override
  • Add getGlobalAdapters() registry method

I tested OpenCode and Claude manually locally and I can use them in repos with no openspec folders within. I was able to use /openspec-propose from within an existing repo with had never had openspec init run and OpenSpec generated the files in the correct places.

Closes #752

Summary by CodeRabbit

  • New Features

    • Added global installation mode (--global) for init and update; supports global installs for Claude, OpenCode, and Codex
    • OPENSPEC_GLOBAL_ROOT environment variable to override global base
    • Project-local installs unchanged; project files continue to take precedence over global
  • Documentation

    • CLI help and specs updated to document --global usage, required flags, and per-tool compatibility
  • Tests

    • New unit and integration tests for global resolution, init, and update scenarios

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

Adds global-install support: ToolCommandAdapter gains optional getGlobalRoot(), many adapters implement it (mostly null; Claude/OpenCode/Codex return paths), a resolveGlobalRoot helper and registry filtering are added, and init/update CLI flows gain a --global mode with executeGlobal() paths and extensive tests/specs updated.

Changes

Cohort / File(s) Summary
Design & Specs
openspec/changes/archive/2026-02-23-init-global/.openspec.yaml, .../design.md, .../proposal.md, .../tasks.md, .../specs/cli-init/spec.md, .../specs/cli-update/spec.md, .../specs/command-generation/spec.md, .../specs/global-install/spec.md, openspec/specs/cli-init/spec.md, openspec/specs/cli-update/spec.md, openspec/specs/command-generation/spec.md, openspec/specs/global-install/spec.md
New design/proposal/tasks/specs describing global-root concept, CLI --global behavior, adapter contract, Codex migration, OPENSPEC_GLOBAL_ROOT, and acceptance scenarios.
Types / Registry / Utilities
src/core/command-generation/types.ts, src/core/command-generation/registry.ts, src/core/command-generation/global-root.ts, src/core/command-generation/index.ts
Added optional `getGlobalRoot(): string
Adapters — global roots (non-null)
src/core/command-generation/adapters/claude.ts, src/core/command-generation/adapters/opencode.ts, src/core/command-generation/adapters/codex.ts
Implemented platform-aware getGlobalRoot() for Claude and OpenCode; Codex migrated to project-relative getFilePath() and a new getGlobalRoot() honoring CODEX_HOME.
Adapters — global roots (null)
Adapters (many)
src/core/command-generation/adapters/...
amazon-q.ts, antigravity.ts, auggie.ts, cline.ts, codebuddy.ts, continue.ts, costrict.ts, crush.ts, cursor.ts, factory.ts, gemini.ts, github-copilot.ts, iflow.ts, kilocode.ts, kiro.ts, pi.ts, qoder.ts, qwen.ts, roocode.ts, windsurf.ts
Added getGlobalRoot(): null stub to adapters that do not support global installs (uniform API surface change).
CLI surface
src/cli/index.ts
Added --global option handling for init and update; routes to new executeGlobal() methods and extends action option types with global?: boolean.
Init / Update core
src/core/init.ts, src/core/update.ts
Added global?: boolean option fields and globalMode members; implemented executeGlobal() flows to resolve global roots, validate/skip tools, generate/write global files, and emit per-tool summaries.
Barrel / exports
src/core/command-generation/index.ts
Re-exported resolveGlobalRoot for use by core commands.
Tests
test/core/command-generation/adapters.test.ts, test/core/command-generation/global-install.test.ts, test/core/global-init.test.ts
Updated/added tests: Codex getFilePath vs getGlobalRoot, resolveGlobalRoot behavior, registry filtering, and integration tests for init --global / update --global scenarios (platform/env variations, error cases, mixed-tool installs).

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI (src/cli/index.ts)
  participant Init as InitCommand (src/core/init.ts)
  participant Registry as CommandAdapterRegistry
  participant Adapter as ToolCommandAdapter
  participant FS as Filesystem

  CLI->>Init: run `openspec init --global --tools claude,opencode`
  Init->>Registry: resolve requested tools / getGlobalAdapters()
  Registry-->>Init: adapters for requested tools
  Init->>Adapter: resolveGlobalRoot(adapter)
  Adapter-->>Init: absolute global root (or null)
  Init->>FS: write generated skill/command files to resolved global paths
  FS-->>Init: write results
  Init-->>CLI: summary (installed / skipped)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • TabishB

"🐇
I hopped from home to dot-config, so spry,
Claudes and Codex now sit under my sky,
No repo clutter, just global delight,
OpenSpec's roots planted snug and right! 🌱"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the primary change: adding a --global flag to openspec init for tool global directory installation.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #752: --global flag for init/update, getGlobalRoot() interface method, implementations for Claude/OpenCode/Codex, Codex migration, per-tool filtering, OPENSPEC_GLOBAL_ROOT override, and coexistence of global/local installations.
Out of Scope Changes check ✅ Passed All code changes are directly aligned with the global installation feature objectives; no unrelated modifications detected in specifications, adapters, CLI, tests, or command generation modules.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…tion

Install skills and commands to tool global directories (~/.claude/, ~/.config/opencode/,
~/.codex/) instead of project directories. This lets consultants and multi-repo developers
install once globally rather than per-project.

- Add optional `getGlobalRoot()` method to ToolCommandAdapter interface
- Implement for Claude (~/.claude/), OpenCode (XDG-aware), Codex (~/.codex/)
- Return null from all 20 remaining adapters (Cursor, Windsurf, etc.)
- Migrate Codex adapter: getFilePath() now returns project-relative path
- Add `--global` flag to `openspec init` (requires `--tools`)
- Add `--global` flag to `openspec update`
- Add `resolveGlobalRoot()` helper with OPENSPEC_GLOBAL_ROOT env var override
- Add `getGlobalAdapters()` registry method

Closes Fission-AI#752

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@askpatrickw askpatrickw marked this pull request as ready for review February 24, 2026 02:52
@askpatrickw askpatrickw requested a review from TabishB as a code owner February 24, 2026 02:52
@greptile-apps
Copy link

greptile-apps bot commented Feb 24, 2026

Greptile Summary

This PR adds global installation support for OpenSpec skills and commands, allowing consultants and multi-repo developers to install once globally rather than per-project. The implementation introduces an optional getGlobalRoot() method to the ToolCommandAdapter interface, implements it for Claude (/.claude/), OpenCode (XDG-aware), and Codex (/.codex/), and returns null from 20 remaining adapters.

Key Changes:

  • Added --global flag to openspec init (requires --tools) and openspec update commands
  • Implemented resolveGlobalRoot() helper with OPENSPEC_GLOBAL_ROOT env var override support
  • Migrated Codex adapter: getFilePath() now returns project-relative paths instead of absolute
  • Added getGlobalAdapters() registry method to filter tools with global support
  • Comprehensive test coverage with manual validation by author

Architecture:
The implementation cleanly separates global from local installation paths, with adapters defining their native global roots and a centralized resolver handling environment variable overrides. Commands and skills are generated using existing generators, then written to global directories with path manipulation to strip the project-relative prefix.

Minor Consideration:
The regex pattern /^\.?[^/\\]+[/\\]/ used in init.ts:244 and update.ts:770 to strip the first directory segment from relative paths (e.g., .claude/commands/opsx/explore.mdcommands/opsx/explore.md) works correctly for current adapters but could be made more explicit or documented for future maintainability.

Confidence Score: 4/5

  • This PR is generally safe to merge with minor considerations for path handling robustness
  • The implementation is well-structured with proper interface design, comprehensive tests, and clear separation of concerns. The regex-based path manipulation for stripping directory segments is functional but slightly fragile. The feature is well-tested and the author manually validated the functionality.
  • Pay attention to src/core/init.ts:244 and src/core/update.ts:770 where regex path manipulation occurs

Important Files Changed

Filename Overview
src/core/command-generation/types.ts Added optional getGlobalRoot() method to ToolCommandAdapter interface for global directory support
src/core/command-generation/global-root.ts New helper function resolveGlobalRoot() that respects OPENSPEC_GLOBAL_ROOT env var override
src/core/command-generation/registry.ts Added getGlobalAdapters() method to filter adapters with global installation support
src/core/command-generation/adapters/claude.ts Implemented getGlobalRoot() returning ~/.claude/ (or %APPDATA%\Claude\ on Windows)
src/core/command-generation/adapters/opencode.ts Implemented getGlobalRoot() with XDG-aware path resolution for OpenCode
src/core/command-generation/adapters/codex.ts Migrated from absolute to relative paths in getFilePath(), added getGlobalRoot() respecting CODEX_HOME
src/cli/index.ts Added --global flag to init and update commands with proper routing and validation
src/core/init.ts Added executeGlobal() method with regex-based path manipulation; requires --tools with --global
src/core/update.ts Added executeGlobal() method that scans for existing global installations and updates them

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    CLI["CLI: openspec init --global --tools X"]
    InitCmd["InitCommand.executeGlobal()"]
    Registry["CommandAdapterRegistry"]
    Resolver["resolveGlobalRoot()"]
    
    CLI --> InitCmd
    InitCmd --> Registry
    Registry --> |"getGlobalAdapters()"| FilterAdapters["Filter adapters with getGlobalRoot() != null"]
    
    InitCmd --> Resolver
    Resolver --> |"Check OPENSPEC_GLOBAL_ROOT"| EnvCheck{Env var set?}
    EnvCheck --> |Yes| EnvPath["Use $OPENSPEC_GLOBAL_ROOT/{toolId}"]
    EnvCheck --> |No| AdapterPath["Use adapter.getGlobalRoot()"]
    
    EnvPath --> GlobalRoot["Global Root Path"]
    AdapterPath --> GlobalRoot
    
    GlobalRoot --> GenSkills["Generate Skills\n{globalRoot}/skills/openspec-X/"]
    GlobalRoot --> GenCommands["Generate Commands\n{globalRoot}/commands/opsx/"]
    
    GenCommands --> PathManip["Strip first dir segment\nfrom relative path"]
    PathManip --> WriteFiles["Write to global directories"]
    GenSkills --> WriteFiles
    
    subgraph "Adapter Implementations"
        Claude["claude: ~/.claude/"]
        OpenCode["opencode: $XDG_CONFIG_HOME/opencode/"]
        Codex["codex: $CODEX_HOME or ~/.codex/"]
        Cursor["cursor: null"]
        Others["20 other adapters: null"]
    end
    
    FilterAdapters --> Claude
    FilterAdapters --> OpenCode
    FilterAdapters --> Codex
Loading

Last reviewed commit: 00e058f

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

45 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/index.ts (1)

90-100: ⚠️ Potential issue | 🟡 Minor

Document which tools support --global in help output.

The CLI spec calls for listing supported global tools in help text; the current flag description doesn’t include that list.

✍️ Suggested update
 const availableToolIds = AI_TOOLS.filter((tool) => tool.skillsDir).map((tool) => tool.value);
 const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}`;
+const globalToolIds = ['claude', 'opencode', 'codex'];
+const globalToolsHint = `Supported global tools: ${globalToolIds.join(', ')}`;
@@
-  .option('--global', 'Install skills and commands to tool global directories (requires --tools)')
+  .option('--global', `Install skills and commands to tool global directories (requires --tools). ${globalToolsHint}`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/index.ts` around lines 90 - 100, The help text for the --global flag
doesn't list which tools support global installs; update the CLI to compute the
supported-global tool ids from AI_TOOLS (similar to availableToolIds) by
filtering for the property that indicates global support (e.g., a field like
globalInstall/globalDir/supportsGlobal on each tool) and then include that
comma-separated list in the --global option description. Modify the code around
availableToolIds/toolsOptionDescription and the
program.command(...).option('--global', ...) call so the option text explicitly
names the tools that support --global.
♻️ Duplicate comments (13)
src/core/command-generation/adapters/costrict.ts (1)

32-34: LGTM.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/costrict.ts` around lines 32 - 34,
getGlobalRoot currently returns null in the Costrict adapter (function
getGlobalRoot), and the reviewer approved the change; no code modification is
required—leave the getGlobalRoot(): null { return null; } implementation as-is
in src/core/command-generation/adapters/costrict.ts.
src/core/command-generation/adapters/crush.ts (1)

35-37: LGTM.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/crush.ts` around lines 35 - 37,
getGlobalRoot() in the crush adapter is approved as-is; no changes
required—leave the method getGlobalRoot() returning null in
src/core/command-generation/adapters/crush.ts unchanged.
src/core/command-generation/adapters/pi.ts (1)

47-49: LGTM.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/pi.ts` around lines 47 - 49, The review
approves the current implementation—no change required: leave the
getGlobalRoot() method as implemented (returning null) in the adapter (function
getGlobalRoot) and proceed with the PR as-is.
src/core/command-generation/adapters/continue.ts (1)

33-35: LGTM. Same pattern as geminiAdapter; the DRY refactor suggestion in gemini.ts applies here too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/continue.ts` around lines 33 - 35, The
getGlobalRoot() implementation in continue.ts is identical to geminiAdapter's
pattern; refactor by extracting the shared logic into a common helper or base
class (e.g., move the null-returning getGlobalRoot behavior into a shared
utility used by both continueAdapter and geminiAdapter) and update continue.ts
to call that shared function or extend the base so the duplicate getGlobalRoot()
method is removed; ensure you reference and reuse the same symbol/name used for
the DRY helper in gemini.ts so both adapters share one implementation.
src/core/command-generation/adapters/factory.ts (1)

32-34: LGTM.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/factory.ts` around lines 32 - 34, No
change required: the getGlobalRoot() method in factory.ts intentionally returns
null and is approved as-is; if you later need it to return a real path, update
the method implementation and return type from getGlobalRoot(): null to
something like getGlobalRoot(): string | null and implement logic there.
src/core/command-generation/adapters/windsurf.ts (1)

58-60: LGTM.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/windsurf.ts` around lines 58 - 60, No
change required: the reviewer approved the implementation of getGlobalRoot(), so
leave the getGlobalRoot(): null { return null; } method as-is (no modifications
or refactors needed) and proceed to merge; ensure you do not add duplicate
review comments or edits related to this method.
src/core/command-generation/adapters/auggie.ts (1)

32-34: Same pattern as github-copilot.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/auggie.ts` around lines 32 - 34,
getGlobalRoot currently returns null in auggie.ts, duplicating the same stub
pattern used in github-copilot.ts; replace the placeholder with the correct
implementation or a shared helper. Locate the getGlobalRoot method in the
adapter (function getGlobalRoot) and either implement the proper logic that
returns the adapter’s global root path or delegate to a common utility (extract
a shared getGlobalRoot helper and call it from both auggie.ts and
github-copilot.ts) so the behavior is consistent and not duplicated.
src/core/command-generation/adapters/cursor.ts (1)

50-52: Same pattern as github-copilot.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/cursor.ts` around lines 50 - 52,
getGlobalRoot() currently returns null as a stub (same issue as in
github-copilot.ts); replace this with the same implementation used in
github-copilot.ts so the adapter returns the correct global root value (or
delegates to the shared helper used there). Locate getGlobalRoot in the Cursor
adapter and copy the logic from the github-copilot adapter (or call the common
helper function used by that file) so the method returns the actual global root
rather than null.
src/core/command-generation/adapters/cline.ts (1)

32-34: Same pattern as github-copilot.ts.

See the comment on github-copilot.ts lines 31–33 — the optional-method boilerplate concern applies here identically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/cline.ts` around lines 32 - 34, The
no-op getGlobalRoot() method in the Cline adapter is redundant boilerplate like
in github-copilot.ts; remove the explicit getGlobalRoot(): null { return null; }
from the class so the adapter doesn't define an unnecessary optional-method stub
(or implement the actual behavior if required), ensuring the class relies on the
interface's optional method semantics instead of a null-returning placeholder.
src/core/command-generation/adapters/iflow.ts (1)

34-36: Same pattern as github-copilot.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/iflow.ts` around lines 34 - 36, The
getGlobalRoot() method in iflow.ts is a stub that returns null and duplicates
the same pattern from github-copilot.ts; update getGlobalRoot() in iflow.ts to
match the real implementation used in github-copilot.ts (replace the null stub
with the same logic/return type and behavior), ensuring the function name
getGlobalRoot and any helper calls used in github-copilot.ts are mirrored so
both adapters behave consistently.
src/core/command-generation/adapters/kiro.ts (1)

31-33: Same pattern as github-copilot.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/kiro.ts` around lines 31 - 33, The
getGlobalRoot() implementation in kiro.ts simply returns null and duplicates the
same pattern found in github-copilot.ts; refactor to avoid duplication by either
implementing the shared logic used by github-copilot.ts or extracting a common
helper used by both adapters (e.g., move the real getGlobalRoot behavior into a
shared utility and have kiro.ts and github-copilot.ts call that), then update
kiro.ts's getGlobalRoot() to call the shared helper instead of returning null.
src/core/command-generation/adapters/antigravity.ts (1)

31-33: Same pattern as github-copilot.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/antigravity.ts` around lines 31 - 33,
getGlobalRoot currently returns null in antigravity.ts which duplicates the
pattern in github-copilot.ts; update getGlobalRoot to match the
github-copilot.ts implementation by either delegating to the shared helper used
there (e.g., call the common getGlobalRoot helper) or copying that same logic
into antigravity.ts so both adapters behave identically, keeping the method
signature and return type unchanged and removing the duplicated null-return
behavior.
src/core/command-generation/adapters/kilocode.ts (1)

28-30: Same pattern as github-copilot.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/kilocode.ts` around lines 28 - 30, The
getGlobalRoot() override in the kilocode adapter duplicates the trivial
implementation from github-copilot.ts; remove this redundant method from the
Kilocode adapter (or replace it with the same consolidated implementation used
by github-copilot.ts) so the adapter relies on the shared/base behavior instead
of repeating the identical getGlobalRoot() { return null; } stub.
🧹 Nitpick comments (7)
src/core/command-generation/adapters/gemini.ts (1)

31-33: Consider extracting a shared nullGlobalRoot mixin to reduce boilerplate across all null-returning adapters.

This identical 3-line block is duplicated across ~20 adapter files in this PR. A single shared object literal would centralize it:

♻️ Proposed extraction (applies to all null-returning adapters)

Create a shared file (e.g., src/core/command-generation/adapters/shared.ts):

+// Shared mixin for adapters that have no global installation root
+export const nullGlobalRoot = {
+  getGlobalRoot(): null {
+    return null;
+  },
+} as const;

Then spread it into each null-returning adapter:

 export const geminiAdapter: ToolCommandAdapter = {
   toolId: 'gemini',
+  ...nullGlobalRoot,

   getFilePath(commandId: string): string {
     return path.join('.gemini', 'commands', 'opsx', `${commandId}.toml`);
   },

   formatFile(content: CommandContent): string {
     // ...
   },
-
-  getGlobalRoot(): null {
-    return null;
-  },
 };

Repeat for every other null-returning adapter in this PR (continue, windsurf, factory, costrict, pi, crush, and the ~13 others not shown here).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/gemini.ts` around lines 31 - 33, Extract
a shared object (e.g., export const nullGlobalRoot) that implements
getGlobalRoot(): null and return null, place it in a new module (suggested name:
src/core/command-generation/adapters/shared.ts), then import and spread that
object into each null-returning adapter (including the adapter object in
src/core/command-generation/adapters/gemini.ts) instead of defining the
three-line getGlobalRoot() { return null; } block in every file; ensure the
exported symbol name nullGlobalRoot and the method name getGlobalRoot are used
so existing adapter shapes remain unchanged.
src/core/command-generation/adapters/github-copilot.ts (1)

31-33: Optional refactoring: Remove redundant getGlobalRoot(): null stubs from non-global adapters.

Since getGlobalRoot() is optional on ToolCommandAdapter (declared as getGlobalRoot?()), adapters without global support can simply omit the method. The getGlobalAdapters() filter already uses optional chaining (adapter.getGlobalRoot?.() != null), which correctly excludes both adapters without the method (returning undefined) and those returning null. The explicit null-returning stubs across ~19 adapters (including github-copilot.ts) are unnecessary boilerplate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/github-copilot.ts` around lines 31 - 33,
Remove the redundant getGlobalRoot(): null stub from non-global adapters (e.g.,
in github-copilot.ts) since ToolCommandAdapter declares getGlobalRoot?() as
optional; simply delete the getGlobalRoot method from the adapter class so it
relies on optional chaining in getGlobalAdapters() (which uses
adapter.getGlobalRoot?.() != null) to filter adapters that provide a global
root.
src/core/command-generation/types.ts (1)

48-50: Clarify the absolute-path example for getGlobalRoot docs.

The description says “absolute path” but the example uses ~, which is shell expansion. Consider updating the example to an explicit absolute path or a <home> placeholder to avoid confusion.

💡 Suggested doc tweak
-   * `@returns` Absolute path to the global root (e.g., '~/.claude/'), or null
+   * `@returns` Absolute path to the global root (e.g., '<home>/.claude/'), or null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/types.ts` around lines 48 - 50, Update the JSDoc
for getGlobalRoot in types.ts to avoid using shell-tilde; change the example
from '~/.claude/' to an explicit absolute-path or a placeholder like
'/home/username/.claude/' or '<home>/.claude/' so the docs accurately reflect an
absolute path and don't imply shell expansion.
src/core/command-generation/adapters/opencode.ts (1)

24-31: Normalize XDG_CONFIG_HOME to an absolute path.

This keeps the adapter aligned with the “absolute path” contract even if the env var is relative.

♻️ Suggested tweak
-    return xdgConfig
-      ? path.join(xdgConfig, 'opencode')
+    return xdgConfig
+      ? path.resolve(xdgConfig, 'opencode')
       : path.join(os.homedir(), '.config', 'opencode');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/opencode.ts` around lines 24 - 31, The
getGlobalRoot() function should normalize XDG_CONFIG_HOME to an absolute path
before joining with 'opencode'; update the branch that reads
process.env.XDG_CONFIG_HOME in getGlobalRoot to call path.resolve(...) (or
otherwise convert the possibly-relative env value to an absolute path) and then
use that resolved path in path.join(..., 'opencode') so the adapter always
returns an absolute path even when XDG_CONFIG_HOME is relative.
openspec/specs/cli-init/spec.md (1)

5-12: Clarify that installed commands are slash commands.

To avoid confusion with terminal CLI commands, spell out “slash commands” (or “/opsx:*”).
Based on learnings: In the OpenSpec codebase, distinguish between CLI commands (terminal-based, e.g., openspec status) and slash commands (agent interface commands, e.g., /opsx:clarify).

✏️ Suggested wording
-The `openspec init` command SHALL support a `--global` flag that installs skills and commands to tool global directories instead of project directories.
+The `openspec init` command SHALL support a `--global` flag that installs skills and slash commands to tool global directories instead of project directories.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/specs/cli-init/spec.md` around lines 5 - 12, Update the wording in
the scenario for `openspec init` to disambiguate "commands" as agent slash
commands: change the phrase "the system SHALL write skills and commands to
Claude Code's global directory" to explicitly say "the system SHALL write skills
and slash commands (e.g., /opsx:...) to Claude Code's global directory
(~/.claude/)". Also ensure any other occurrences in this block referencing
"commands" (including the `openspec/` directory and `--global` behavior) are
clarified to distinguish terminal CLI commands (e.g., `openspec status`) from
slash commands used by agents.
openspec/specs/command-generation/spec.md (1)

38-61: resolveGlobalRoot() utility has no specification.

resolveGlobalRoot() is the central routing function for all global CLI operations (init, update), but it is only referenced in tasks.md with no corresponding requirement or scenario in spec.md. Its contract — specifically the precedence logic between OPENSPEC_GLOBAL_ROOT and adapter.getGlobalRoot(), and its return value when the adapter has a null global root — should be specified here.

📝 Suggested addition
+### Requirement: Global-root resolution utility
+
+The system SHALL provide a `resolveGlobalRoot(adapter: ToolCommandAdapter)` helper for determining the effective global installation root.
+
+#### Scenario: Resolve global root with env var override
+
+- **WHEN** calling `resolveGlobalRoot(adapter)` and `OPENSPEC_GLOBAL_ROOT` is set
+- **THEN** it SHALL return the value of `OPENSPEC_GLOBAL_ROOT` as an absolute path
+- **AND** it SHALL NOT call `adapter.getGlobalRoot()`
+
+#### Scenario: Resolve global root from adapter
+
+- **WHEN** calling `resolveGlobalRoot(adapter)` and `OPENSPEC_GLOBAL_ROOT` is not set
+- **THEN** it SHALL return `adapter.getGlobalRoot()` (which may be `null`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/specs/command-generation/spec.md` around lines 38 - 61, Add a new
requirement and scenarios for the resolveGlobalRoot() utility: specify that
resolveGlobalRoot() SHALL prefer an explicit OPENSPEC_GLOBAL_ROOT environment
variable over adapter.getGlobalRoot(), SHALL return adapter.getGlobalRoot() when
OPENSPEC_GLOBAL_ROOT is unset and adapter.getGlobalRoot() is non-null, and SHALL
return null (or an explicit error/empty result as chosen) when both
OPENSPEC_GLOBAL_ROOT is unset and adapter.getGlobalRoot() is null; include
scenarios for (1) OPENSPEC_GLOBAL_ROOT set (returns that value), (2)
OPENSPEC_GLOBAL_ROOT unset but adapter.getGlobalRoot() non-null (returns adapter
path), and (3) both unset/null (define returned value/behavior), referencing the
resolveGlobalRoot() utility and adapter.getGlobalRoot() to locate the code.
openspec/changes/archive/2026-02-23-init-global/tasks.md (1)

40-43: Mixed-support behavior (skip vs. error) is undocumented in any spec scenario.

Tasks 6.6 and 6.9 imply different CLI behaviors:

  • --tools cursorerror (all requested tools are globally unsupported)
  • --tools claude,cursorskip cursor, install claude (partially unsupported)

This distinction is not captured in any spec scenario visible in this PR. Without a spec requirement, this branching behavior risks inconsistent implementation or regression. Consider adding a CLI scenario in openspec/specs/cli-init/spec.md (or equivalent) that explicitly documents when the command errors vs. when it silently skips.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/archive/2026-02-23-init-global/tasks.md` around lines 40 -
43, Add a CLI spec scenario that documents the mixed-support behavior for the
--tools flag: one case specifying that when all requested tools are globally
unsupported (matching test 6.6, e.g., --tools cursor) the command must return an
error, and another case showing that when some tools are supported and some are
not (matching test 6.9, e.g., --tools claude,cursor) the command should install
supported tools and skip unsupported ones without failing; update the cli-init
spec (spec.md) to list which tools are considered globally-supported and state
the exact error vs. skip semantics for --tools so implementations and tests are
unambiguous.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openspec/changes/archive/2026-02-23-init-global/.openspec.yaml`:
- Line 2: The `created` date in the file's frontmatter (the created field in
.openspec.yaml) does not match the parent directory name
`2026-02-23-init-global`; pick the canonical date for this change and make them
consistent by either updating the created field in .openspec.yaml to
`2026-02-23` or renaming the directory to `2026-02-24-init-global`, and ensure
the change is applied consistently across this entry.

In `@openspec/changes/archive/2026-02-23-init-global/specs/cli-update/spec.md`:
- Around line 1-26: Add a short explicit sentence to the "Global update mode"
requirement stating that this delta is applied independently and that OpenSpec
archive changes are not transactional across multiple deltas; reference the
"Global update mode" heading and the scenario blocks (e.g., "Scenario: Global
update", "Scenario: Global update with no globally-installed files", "Scenario:
Non-global update unchanged") so readers know the per-delta partial-sync
atomicity model applies and that no cross-delta transaction is implied.

In `@openspec/changes/archive/2026-02-23-init-global/tasks.md`:
- Around line 24-25: Task 4.2 is ambiguous about the dual patterns `openspec-*`
vs `opsx-*`; clarify which artifact type each pattern represents and ensure the
global scan covers both; update the Task 4.2 description to state explicitly
"scan global adapter roots for opsx-<id>.md (commands) and openspec-<id>.md
(skills/other artifacts)" (referencing the adapter.getFilePath() scenarios), and
update Task 6.11 to add tests that assert the global update scan finds and
regenerates files matching both `opsx-*` and `openspec-*` patterns so nothing is
silently skipped.

In `@openspec/specs/command-generation/spec.md`:
- Around line 38-61: Add a new scenario for the OpenCode adapter documenting its
global-root contract: call OpenCodeAdapter.getGlobalRoot() and assert it returns
the adapter's XDG-aware absolute home directory (use ~/.config/opencode/ on
macOS/Linux, %APPDATA%\opencode\ on Windows, and respect XDG/APPDATA environment
overrides), so the spec explicitly documents OpenCodeAdapter.getGlobalRoot()’s
expected behavior alongside Claude and Codex.
- Around line 44-47: Clarify and implement the intended filtering contract for
CommandAdapterRegistry.getGlobalAdapters(): decide whether it should include
adapters based on their declared getGlobalRoot() (Option 1) or based on the
effective path returned by resolveGlobalRoot() (Option 2). Update the spec text
and the implementation for getGlobalAdapters() accordingly: if you choose Option
1, state that OPENSPEC_GLOBAL_ROOT only overrides adapters that already return a
non-null getGlobalRoot() and keep getGlobalAdapters() filtering on
getGlobalRoot(); if you choose Option 2, state that getGlobalAdapters() uses
resolveGlobalRoot() (which checks OPENSPEC_GLOBAL_ROOT first) and change
getGlobalAdapters() to call resolveGlobalRoot(adapter) instead of
adapter.getGlobalRoot() so adapters with null getGlobalRoot() can be included
when OPENSPEC_GLOBAL_ROOT makes them resolvable.
- Line 22: The spec's Windows path for getGlobalRoot() is incorrect; change the
documented Windows return value of getGlobalRoot() from "%APPDATA%\Claude\" to
"%USERPROFILE%\.claude\" (e.g., C:\Users\<username>\.claude\) so it matches
Claude Code's actual global configuration directory; update the spec line that
currently states getGlobalRoot() SHALL return "~/.claude/ (macOS/Linux) or
%APPDATA%\Claude\ (Windows)" to instead specify "%USERPROFILE%\.claude\
(Windows)" while keeping the macOS/Linux value unchanged.

In `@src/core/command-generation/adapters/codex.ts`:
- Around line 31-37: The adapter documentation/header is inconsistent:
getFilePath(commandId) now returns a project‑relative path
(".codex/prompts/opsx-<commandId>.md") but the doc text still claims a
global/absolute home; update the adapter docs to state that prompt files are
stored in a project‑relative ".codex" directory under the project root and
explain how getGlobalRoot() (which calls getCodexHome()) differs or when a
global home is used; update any mention of "global home directory / absolute" to
clearly describe both behaviors and reference getFilePath and getGlobalRoot by
name so readers can find the code.

In `@src/core/init.ts`:
- Around line 217-266: The global init currently only aggregates totalFiles;
update the loop over toolResults to track per-tool file counts and emit per-tool
summary lines including the global path: inside the for (const { toolId,
globalRoot, name } of toolResults) block create a local counter (e.g.,
filesForTool) and increment it whenever you write a file (both in the
shouldGenerateSkills branch where you call FileSystemUtils.writeFile and in the
shouldGenerateCommands branch where you write commandFile), still increment
totalFiles as before, then collect a summary entry (e.g., `${name}:
${filesForTool} files → ${globalRoot}`) into an array or map; after the loop
print those per-tool lines (instead of only the aggregated total) alongside the
existing overall Installed/totalFiles output so callers see per-tool counts and
directories.

In `@src/core/update.ts`:
- Around line 737-773: The code currently detects existing global commands by
checking adapter.getFilePath('explore'), which can miss active workflows;
instead call generateCommands(commandContents, adapter) early, set hasCommands =
generatedCommands.length > 0, and reuse that generatedCommands list later when
writing files (replace the current second call to generateCommands with the
already-built generatedCommands); update the block around
shouldGenerateCommands/hasCommands to use generatedCommands so command detection
and writes reflect the active workflow set.

In `@test/core/global-init.test.ts`:
- Around line 114-128: The test may write to the real Claude global root because
OPENSPEC_GLOBAL_ROOT is deleted; before calling InitCommand.executeGlobal() stub
or mock claudeAdapter.getGlobalRoot (the function used to locate Claude's global
root) to return a temporary directory (or set OPENSPEC_GLOBAL_ROOT to a temp
path) so the test operates on an isolated temp folder; do this setup inside the
test (or a before/after hook) and ensure teardown/cleanup of the temp directory
after the test finishes.

---

Outside diff comments:
In `@src/cli/index.ts`:
- Around line 90-100: The help text for the --global flag doesn't list which
tools support global installs; update the CLI to compute the supported-global
tool ids from AI_TOOLS (similar to availableToolIds) by filtering for the
property that indicates global support (e.g., a field like
globalInstall/globalDir/supportsGlobal on each tool) and then include that
comma-separated list in the --global option description. Modify the code around
availableToolIds/toolsOptionDescription and the
program.command(...).option('--global', ...) call so the option text explicitly
names the tools that support --global.

---

Duplicate comments:
In `@src/core/command-generation/adapters/antigravity.ts`:
- Around line 31-33: getGlobalRoot currently returns null in antigravity.ts
which duplicates the pattern in github-copilot.ts; update getGlobalRoot to match
the github-copilot.ts implementation by either delegating to the shared helper
used there (e.g., call the common getGlobalRoot helper) or copying that same
logic into antigravity.ts so both adapters behave identically, keeping the
method signature and return type unchanged and removing the duplicated
null-return behavior.

In `@src/core/command-generation/adapters/auggie.ts`:
- Around line 32-34: getGlobalRoot currently returns null in auggie.ts,
duplicating the same stub pattern used in github-copilot.ts; replace the
placeholder with the correct implementation or a shared helper. Locate the
getGlobalRoot method in the adapter (function getGlobalRoot) and either
implement the proper logic that returns the adapter’s global root path or
delegate to a common utility (extract a shared getGlobalRoot helper and call it
from both auggie.ts and github-copilot.ts) so the behavior is consistent and not
duplicated.

In `@src/core/command-generation/adapters/cline.ts`:
- Around line 32-34: The no-op getGlobalRoot() method in the Cline adapter is
redundant boilerplate like in github-copilot.ts; remove the explicit
getGlobalRoot(): null { return null; } from the class so the adapter doesn't
define an unnecessary optional-method stub (or implement the actual behavior if
required), ensuring the class relies on the interface's optional method
semantics instead of a null-returning placeholder.

In `@src/core/command-generation/adapters/continue.ts`:
- Around line 33-35: The getGlobalRoot() implementation in continue.ts is
identical to geminiAdapter's pattern; refactor by extracting the shared logic
into a common helper or base class (e.g., move the null-returning getGlobalRoot
behavior into a shared utility used by both continueAdapter and geminiAdapter)
and update continue.ts to call that shared function or extend the base so the
duplicate getGlobalRoot() method is removed; ensure you reference and reuse the
same symbol/name used for the DRY helper in gemini.ts so both adapters share one
implementation.

In `@src/core/command-generation/adapters/costrict.ts`:
- Around line 32-34: getGlobalRoot currently returns null in the Costrict
adapter (function getGlobalRoot), and the reviewer approved the change; no code
modification is required—leave the getGlobalRoot(): null { return null; }
implementation as-is in src/core/command-generation/adapters/costrict.ts.

In `@src/core/command-generation/adapters/crush.ts`:
- Around line 35-37: getGlobalRoot() in the crush adapter is approved as-is; no
changes required—leave the method getGlobalRoot() returning null in
src/core/command-generation/adapters/crush.ts unchanged.

In `@src/core/command-generation/adapters/cursor.ts`:
- Around line 50-52: getGlobalRoot() currently returns null as a stub (same
issue as in github-copilot.ts); replace this with the same implementation used
in github-copilot.ts so the adapter returns the correct global root value (or
delegates to the shared helper used there). Locate getGlobalRoot in the Cursor
adapter and copy the logic from the github-copilot adapter (or call the common
helper function used by that file) so the method returns the actual global root
rather than null.

In `@src/core/command-generation/adapters/factory.ts`:
- Around line 32-34: No change required: the getGlobalRoot() method in
factory.ts intentionally returns null and is approved as-is; if you later need
it to return a real path, update the method implementation and return type from
getGlobalRoot(): null to something like getGlobalRoot(): string | null and
implement logic there.

In `@src/core/command-generation/adapters/iflow.ts`:
- Around line 34-36: The getGlobalRoot() method in iflow.ts is a stub that
returns null and duplicates the same pattern from github-copilot.ts; update
getGlobalRoot() in iflow.ts to match the real implementation used in
github-copilot.ts (replace the null stub with the same logic/return type and
behavior), ensuring the function name getGlobalRoot and any helper calls used in
github-copilot.ts are mirrored so both adapters behave consistently.

In `@src/core/command-generation/adapters/kilocode.ts`:
- Around line 28-30: The getGlobalRoot() override in the kilocode adapter
duplicates the trivial implementation from github-copilot.ts; remove this
redundant method from the Kilocode adapter (or replace it with the same
consolidated implementation used by github-copilot.ts) so the adapter relies on
the shared/base behavior instead of repeating the identical getGlobalRoot() {
return null; } stub.

In `@src/core/command-generation/adapters/kiro.ts`:
- Around line 31-33: The getGlobalRoot() implementation in kiro.ts simply
returns null and duplicates the same pattern found in github-copilot.ts;
refactor to avoid duplication by either implementing the shared logic used by
github-copilot.ts or extracting a common helper used by both adapters (e.g.,
move the real getGlobalRoot behavior into a shared utility and have kiro.ts and
github-copilot.ts call that), then update kiro.ts's getGlobalRoot() to call the
shared helper instead of returning null.

In `@src/core/command-generation/adapters/pi.ts`:
- Around line 47-49: The review approves the current implementation—no change
required: leave the getGlobalRoot() method as implemented (returning null) in
the adapter (function getGlobalRoot) and proceed with the PR as-is.

In `@src/core/command-generation/adapters/windsurf.ts`:
- Around line 58-60: No change required: the reviewer approved the
implementation of getGlobalRoot(), so leave the getGlobalRoot(): null { return
null; } method as-is (no modifications or refactors needed) and proceed to
merge; ensure you do not add duplicate review comments or edits related to this
method.

---

Nitpick comments:
In `@openspec/changes/archive/2026-02-23-init-global/tasks.md`:
- Around line 40-43: Add a CLI spec scenario that documents the mixed-support
behavior for the --tools flag: one case specifying that when all requested tools
are globally unsupported (matching test 6.6, e.g., --tools cursor) the command
must return an error, and another case showing that when some tools are
supported and some are not (matching test 6.9, e.g., --tools claude,cursor) the
command should install supported tools and skip unsupported ones without
failing; update the cli-init spec (spec.md) to list which tools are considered
globally-supported and state the exact error vs. skip semantics for --tools so
implementations and tests are unambiguous.

In `@openspec/specs/cli-init/spec.md`:
- Around line 5-12: Update the wording in the scenario for `openspec init` to
disambiguate "commands" as agent slash commands: change the phrase "the system
SHALL write skills and commands to Claude Code's global directory" to explicitly
say "the system SHALL write skills and slash commands (e.g., /opsx:...) to
Claude Code's global directory (~/.claude/)". Also ensure any other occurrences
in this block referencing "commands" (including the `openspec/` directory and
`--global` behavior) are clarified to distinguish terminal CLI commands (e.g.,
`openspec status`) from slash commands used by agents.

In `@openspec/specs/command-generation/spec.md`:
- Around line 38-61: Add a new requirement and scenarios for the
resolveGlobalRoot() utility: specify that resolveGlobalRoot() SHALL prefer an
explicit OPENSPEC_GLOBAL_ROOT environment variable over adapter.getGlobalRoot(),
SHALL return adapter.getGlobalRoot() when OPENSPEC_GLOBAL_ROOT is unset and
adapter.getGlobalRoot() is non-null, and SHALL return null (or an explicit
error/empty result as chosen) when both OPENSPEC_GLOBAL_ROOT is unset and
adapter.getGlobalRoot() is null; include scenarios for (1) OPENSPEC_GLOBAL_ROOT
set (returns that value), (2) OPENSPEC_GLOBAL_ROOT unset but
adapter.getGlobalRoot() non-null (returns adapter path), and (3) both unset/null
(define returned value/behavior), referencing the resolveGlobalRoot() utility
and adapter.getGlobalRoot() to locate the code.

In `@src/core/command-generation/adapters/gemini.ts`:
- Around line 31-33: Extract a shared object (e.g., export const nullGlobalRoot)
that implements getGlobalRoot(): null and return null, place it in a new module
(suggested name: src/core/command-generation/adapters/shared.ts), then import
and spread that object into each null-returning adapter (including the adapter
object in src/core/command-generation/adapters/gemini.ts) instead of defining
the three-line getGlobalRoot() { return null; } block in every file; ensure the
exported symbol name nullGlobalRoot and the method name getGlobalRoot are used
so existing adapter shapes remain unchanged.

In `@src/core/command-generation/adapters/github-copilot.ts`:
- Around line 31-33: Remove the redundant getGlobalRoot(): null stub from
non-global adapters (e.g., in github-copilot.ts) since ToolCommandAdapter
declares getGlobalRoot?() as optional; simply delete the getGlobalRoot method
from the adapter class so it relies on optional chaining in getGlobalAdapters()
(which uses adapter.getGlobalRoot?.() != null) to filter adapters that provide a
global root.

In `@src/core/command-generation/adapters/opencode.ts`:
- Around line 24-31: The getGlobalRoot() function should normalize
XDG_CONFIG_HOME to an absolute path before joining with 'opencode'; update the
branch that reads process.env.XDG_CONFIG_HOME in getGlobalRoot to call
path.resolve(...) (or otherwise convert the possibly-relative env value to an
absolute path) and then use that resolved path in path.join(..., 'opencode') so
the adapter always returns an absolute path even when XDG_CONFIG_HOME is
relative.

In `@src/core/command-generation/types.ts`:
- Around line 48-50: Update the JSDoc for getGlobalRoot in types.ts to avoid
using shell-tilde; change the example from '~/.claude/' to an explicit
absolute-path or a placeholder like '/home/username/.claude/' or
'<home>/.claude/' so the docs accurately reflect an absolute path and don't
imply shell expansion.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7d1860 and 00e058f.

📒 Files selected for processing (45)
  • openspec/changes/archive/2026-02-23-init-global/.openspec.yaml
  • openspec/changes/archive/2026-02-23-init-global/design.md
  • openspec/changes/archive/2026-02-23-init-global/proposal.md
  • openspec/changes/archive/2026-02-23-init-global/specs/cli-init/spec.md
  • openspec/changes/archive/2026-02-23-init-global/specs/cli-update/spec.md
  • openspec/changes/archive/2026-02-23-init-global/specs/command-generation/spec.md
  • openspec/changes/archive/2026-02-23-init-global/specs/global-install/spec.md
  • openspec/changes/archive/2026-02-23-init-global/tasks.md
  • openspec/specs/cli-init/spec.md
  • openspec/specs/cli-update/spec.md
  • openspec/specs/command-generation/spec.md
  • openspec/specs/global-install/spec.md
  • src/cli/index.ts
  • src/core/command-generation/adapters/amazon-q.ts
  • src/core/command-generation/adapters/antigravity.ts
  • src/core/command-generation/adapters/auggie.ts
  • src/core/command-generation/adapters/claude.ts
  • src/core/command-generation/adapters/cline.ts
  • src/core/command-generation/adapters/codebuddy.ts
  • src/core/command-generation/adapters/codex.ts
  • src/core/command-generation/adapters/continue.ts
  • src/core/command-generation/adapters/costrict.ts
  • src/core/command-generation/adapters/crush.ts
  • src/core/command-generation/adapters/cursor.ts
  • src/core/command-generation/adapters/factory.ts
  • src/core/command-generation/adapters/gemini.ts
  • src/core/command-generation/adapters/github-copilot.ts
  • src/core/command-generation/adapters/iflow.ts
  • src/core/command-generation/adapters/kilocode.ts
  • src/core/command-generation/adapters/kiro.ts
  • src/core/command-generation/adapters/opencode.ts
  • src/core/command-generation/adapters/pi.ts
  • src/core/command-generation/adapters/qoder.ts
  • src/core/command-generation/adapters/qwen.ts
  • src/core/command-generation/adapters/roocode.ts
  • src/core/command-generation/adapters/windsurf.ts
  • src/core/command-generation/global-root.ts
  • src/core/command-generation/index.ts
  • src/core/command-generation/registry.ts
  • src/core/command-generation/types.ts
  • src/core/init.ts
  • src/core/update.ts
  • test/core/command-generation/adapters.test.ts
  • test/core/command-generation/global-install.test.ts
  • test/core/global-init.test.ts

- Use generated command list for global command detection instead of
  hard-coded 'explore' probe, ensuring all active workflows are checked
- Mock claudeAdapter.getGlobalRoot() in test to avoid writing to real
  ~/.claude directory during test execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
test/core/global-init.test.ts (1)

114-131: Properly isolates from the real filesystem by mocking claudeAdapter.getGlobalRoot().

This was flagged in a prior review and is now correctly addressed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/core/global-init.test.ts` around lines 114 - 131, Test properly isolates
from the real filesystem by deleting process.env.OPENSPEC_GLOBAL_ROOT and
mocking claudeAdapter.getGlobalRoot to return a tempClaudeRoot; ensure the
InitCommand.executeGlobal call uses that mocked root and assertions check the
tempClaudeRoot/skills directory (e.g., path.join(tempClaudeRoot, 'skills')) for
installed openspec packages—keep the vi.spyOn(claudeAdapter,
'getGlobalRoot').mockReturnValue(tempClaudeRoot) and the delete process.env line
so no real ~/.claude is touched.
🧹 Nitpick comments (3)
src/core/update.ts (2)

738-744: Duplicated path-stripping regex — consider extracting a helper.

The regex cmd.path.replace(/^\.?[^/\\]+[/\\]/, '') appears on both Line 742 and Line 770 to strip the tool-directory prefix from project-relative command paths. Extracting this into a small helper (e.g., stripToolDirPrefix(cmdPath: string): string) would reduce the risk of the two diverging and make the intent self-documenting.

Example helper
+/** Strips the leading tool-directory segment from a project-relative command path. */
+function stripToolDirPrefix(cmdPath: string): string {
+  return cmdPath.replace(/^\.?[^/\\]+[/\\]/, '');
+}

Then use path.join(globalRoot, stripToolDirPrefix(cmd.path)) at both sites.

Also applies to: 767-773

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/update.ts` around lines 738 - 744, Extract the repeated regex
cmd.path.replace(/^\.?[^/\\]+[/\\]/, '') into a small helper (e.g.,
stripToolDirPrefix(cmdPath: string): string) and use it where the
project-relative command path is normalized (currently in the
generation/checking logic around generatedCommands and hasCommands and the other
occurrence near command installation checks). Update calls like
path.join(globalRoot, stripToolDirPrefix(cmd.path)) and any other places that
perform the same replacement to ensure a single, well-named implementation and
avoid divergence.

80-84: globalMode is assigned but never read.

this.globalMode is set in the constructor but never referenced anywhere in the class. The CLI calls executeGlobal() directly, so this field is dead code. Consider removing it, or if it's intended for future routing inside execute(), add a comment explaining the intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/update.ts` around lines 80 - 84, The field this.globalMode assigned
in the constructor of the class (via UpdateCommandOptions) is never read; either
remove the globalMode property and its assignment from the constructor to
eliminate dead code, or if you intend it for future routing inside execute(),
keep the property but add a brief comment above the declaration explaining its
planned use and ensure execute() will consult globalMode (or wire execute() to
call executeGlobal() when true) so the field is actually used; update references
around the constructor, the globalMode declaration, and any call sites of
execute()/executeGlobal() accordingly.
test/core/global-init.test.ts (1)

184-189: Consider asserting the expected console output for the "no global files" scenario.

This test only verifies that executeGlobal() doesn't throw. Since console.log is already spied on, you could assert the guidance message was printed, which would strengthen the test.

Example assertion
     it('should show message when no global files exist', async () => {
       const updateCommand = new UpdateCommand({ global: true });
       await updateCommand.executeGlobal();
 
-      // Should not throw, just show a message
+      expect(console.log).toHaveBeenCalledWith(
+        expect.stringContaining('No globally-installed OpenSpec files found')
+      );
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/core/global-init.test.ts` around lines 184 - 189, Add an assertion that
the guidance message is printed when no global files exist by checking the
existing console.log spy after calling UpdateCommand.executeGlobal();
specifically, after creating const updateCommand = new UpdateCommand({ global:
true }) and awaiting updateCommand.executeGlobal(), assert that console.log was
called with the expected guidance string (the user-facing message produced by
executeGlobal) so the test verifies output as well as absence of exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/update.ts`:
- Around line 778-780: The catch block in update.ts currently uses an unsafe
cast `(error as Error).message`; change it to the guarded extraction pattern
used in the local execute() method: compute a message = error instanceof Error ?
error.message : String(error) (and optionally include error.stack when
available) and pass that message to spinner.fail so non-Error throws (strings,
objects) won't cause a secondary exception; update the catch surrounding the
update logic that references toolName to use this guarded message extraction.

In `@test/core/global-init.test.ts`:
- Around line 104-111: resolveGlobalRoot() currently applies
OPENSPEC_GLOBAL_ROOT unconditionally, letting environment overrides enable
global roots for adapters that should be null; update resolveGlobalRoot() to
first call adapter.getGlobalRoot() and if that call returns null, return null
(ignore OPENSPEC_GLOBAL_ROOT), otherwise allow the environment override
(process.env.OPENSPEC_GLOBAL_ROOT) to replace the adapter path; reference
resolveGlobalRoot() and adapter.getGlobalRoot() so the fix ensures null-adapters
remain excluded from global installs regardless of OPENSPEC_GLOBAL_ROOT.

---

Duplicate comments:
In `@test/core/global-init.test.ts`:
- Around line 114-131: Test properly isolates from the real filesystem by
deleting process.env.OPENSPEC_GLOBAL_ROOT and mocking
claudeAdapter.getGlobalRoot to return a tempClaudeRoot; ensure the
InitCommand.executeGlobal call uses that mocked root and assertions check the
tempClaudeRoot/skills directory (e.g., path.join(tempClaudeRoot, 'skills')) for
installed openspec packages—keep the vi.spyOn(claudeAdapter,
'getGlobalRoot').mockReturnValue(tempClaudeRoot) and the delete process.env line
so no real ~/.claude is touched.

---

Nitpick comments:
In `@src/core/update.ts`:
- Around line 738-744: Extract the repeated regex
cmd.path.replace(/^\.?[^/\\]+[/\\]/, '') into a small helper (e.g.,
stripToolDirPrefix(cmdPath: string): string) and use it where the
project-relative command path is normalized (currently in the
generation/checking logic around generatedCommands and hasCommands and the other
occurrence near command installation checks). Update calls like
path.join(globalRoot, stripToolDirPrefix(cmd.path)) and any other places that
perform the same replacement to ensure a single, well-named implementation and
avoid divergence.
- Around line 80-84: The field this.globalMode assigned in the constructor of
the class (via UpdateCommandOptions) is never read; either remove the globalMode
property and its assignment from the constructor to eliminate dead code, or if
you intend it for future routing inside execute(), keep the property but add a
brief comment above the declaration explaining its planned use and ensure
execute() will consult globalMode (or wire execute() to call executeGlobal()
when true) so the field is actually used; update references around the
constructor, the globalMode declaration, and any call sites of
execute()/executeGlobal() accordingly.

In `@test/core/global-init.test.ts`:
- Around line 184-189: Add an assertion that the guidance message is printed
when no global files exist by checking the existing console.log spy after
calling UpdateCommand.executeGlobal(); specifically, after creating const
updateCommand = new UpdateCommand({ global: true }) and awaiting
updateCommand.executeGlobal(), assert that console.log was called with the
expected guidance string (the user-facing message produced by executeGlobal) so
the test verifies output as well as absence of exceptions.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00e058f and d55d933.

📒 Files selected for processing (2)
  • src/core/update.ts
  • test/core/global-init.test.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/core/update.ts (1)

723-774: Consider cleanup/rehydration when delivery or workflow set changes.
Global update only rewrites files that already exist (hasSkills/hasCommands) and never removes deselected artifacts. If profiles/delivery change, stale global skills/commands remain and newly selected ones won’t be created. Consider mirroring local update’s cleanup logic under globalRoot (remove deselected, drop commands on skills-only, etc.).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/update.ts` around lines 723 - 774, The global update loop (using
globalAdapters, resolveGlobalRoot) only writes files that already exist via
hasSkills/hasCommands and never removes stale artifacts, so stale global
skills/commands persist when profiles/delivery change; update the logic to
mirror the local update cleanup/rehydration: compute the desired set from
generatedCommands and skillTemplates (respecting
shouldGenerateCommands/shouldGenerateSkills), remove any existing global
artifacts under skillsDir that do not match the current skillTemplates (e.g.,
openspec-* dirs and SKILL.md files), delete global generated command files that
are not present in generatedCommands, and create any newly selected
skills/commands even if they didn’t previously exist before calling
FileSystemUtils.writeFile; ensure this cleanup happens per adapter/globalRoot
before writing new files so stale items are removed and new selections are
created.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/update.ts`:
- Around line 713-721: The global update path is passing unfiltered profile
workflows into generators; ensure you filter to known workflow IDs like in
execute(). After computing workflows (from getProfileWorkflows),
intersect/filter that array against ALL_WORKFLOWS before calling
getSkillTemplates and getCommandContents (used in executeGlobal), so change the
code around workflows -> shouldGenerateSkills/getSkillTemplates and
shouldGenerateCommands/getCommandContents to use the filtered list (reference
symbols: executeGlobal, workflows, ALL_WORKFLOWS, getSkillTemplates,
getCommandContents, getProfileWorkflows).

---

Nitpick comments:
In `@src/core/update.ts`:
- Around line 723-774: The global update loop (using globalAdapters,
resolveGlobalRoot) only writes files that already exist via
hasSkills/hasCommands and never removes stale artifacts, so stale global
skills/commands persist when profiles/delivery change; update the logic to
mirror the local update cleanup/rehydration: compute the desired set from
generatedCommands and skillTemplates (respecting
shouldGenerateCommands/shouldGenerateSkills), remove any existing global
artifacts under skillsDir that do not match the current skillTemplates (e.g.,
openspec-* dirs and SKILL.md files), delete global generated command files that
are not present in generatedCommands, and create any newly selected
skills/commands even if they didn’t previously exist before calling
FileSystemUtils.writeFile; ensure this cleanup happens per adapter/globalRoot
before writing new files so stale items are removed and new selections are
created.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d55d933 and 37f4d8f.

📒 Files selected for processing (1)
  • src/core/update.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openspec init --global: Install skills and commands to tool global directories

1 participant