-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: add openspec init --global for tool global directory installation
#753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
askpatrickw
wants to merge
3
commits into
Fission-AI:main
Choose a base branch
from
askpatrickw:init-global
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,315
−535
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
2 changes: 2 additions & 0 deletions
2
openspec/changes/archive/2026-02-23-init-global/.openspec.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| schema: spec-driven | ||
| created: 2026-02-24 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| ## Context | ||
|
|
||
| `openspec init` and `openspec update` install skills and slash commands into project-relative directories (e.g., `.claude/commands/opsx/`). The only exception is Codex, whose adapter already returns an absolute path to `~/.codex/prompts/`. Several other tools (Claude Code, OpenCode) have documented global directories but OpenSpec has no way to target them. | ||
|
|
||
| The existing architecture — a `ToolCommandAdapter` interface with `getFilePath()`, a `CommandAdapterRegistry`, and file-write logic in `InitCommand` / `UpdateCommand` that already handles absolute paths via `path.isAbsolute()` — provides a clean extension point. | ||
|
|
||
| ## Goals / Non-Goals | ||
|
|
||
| **Goals:** | ||
| - Allow `openspec init --global --tools <ids>` to write skills and commands to each tool's global directory | ||
| - Allow `openspec update --global` to refresh globally-installed files | ||
| - Support `OPENSPEC_GLOBAL_ROOT` env var to override base paths | ||
| - Migrate Codex adapter to use the new `getGlobalRoot()` pattern explicitly | ||
| - Keep all existing project-local behaviour unchanged | ||
|
|
||
| **Non-Goals:** | ||
| - GUI or interactive prompts for global mode — `--tools` is always required | ||
| - Global installation of schemas or the `openspec/` directory structure — only skills and commands | ||
| - Auto-detection of whether to install globally vs locally | ||
| - Global skill installation paths for tools that only support project-scoped files (Cursor, Windsurf, etc.) | ||
| - Windows path verification — mark as "unknown / not yet confirmed" rather than guess | ||
|
|
||
| ## Decisions | ||
|
|
||
| ### 1. Extend `ToolCommandAdapter` with `getGlobalRoot()` | ||
|
|
||
| An initial consideration was per-command `getGlobalFilePath(commandId)`, but skills also need global paths (see Decision #2). Instead, add a single optional `getGlobalRoot(): string | null` method to the interface. Adapters that support global installation return an absolute path to the tool's base directory; others return `null`. `InitCommand` and `UpdateCommand` derive both skill and command paths from this root. | ||
|
|
||
| **Why not a separate GlobalAdapter class?** The global and local adapters share `formatFile()` and `toolId`. Splitting them would duplicate the formatting logic or require inheritance. A single method addition keeps the adapter registry simple — one adapter per tool, two path strategies. | ||
|
|
||
| **Why optional (not abstract)?** There are 23+ adapters. Making it required would force boilerplate `return null` in ~20 of them. Instead, the base registry lookup treats missing `getGlobalRoot` as equivalent to returning `null`. | ||
|
|
||
| ### 2. Skills also need global path resolution | ||
|
|
||
| Skills are written to `.<tool>/skills/openspec-*/SKILL.md` — structurally parallel to commands. The same global-vs-local routing applies. If the tool has a global root (e.g., `~/.claude/`), skills go under `<globalRoot>/skills/` and commands under `<globalRoot>/commands/`. The single `getGlobalRoot()` method from Decision #1 handles both — no per-command or per-skill path methods needed. | ||
|
|
||
| ### 3. Route via `--global` flag in InitCommand and UpdateCommand | ||
|
|
||
| Both commands gain a `--global` boolean option. When set: | ||
| - `InitCommand` skips directory structure creation (`openspec/`, `config.yaml`) — those are project-local concerns | ||
| - File writes use `adapter.getGlobalRoot()` to derive absolute paths instead of joining with `projectPath` | ||
| - `--tools` is required — no interactive selection (global install is explicit) | ||
| - Tools whose adapter returns `null` from `getGlobalRoot()` are skipped with a stderr warning | ||
|
|
||
| ### 4. `OPENSPEC_GLOBAL_ROOT` env var | ||
|
|
||
| When set, overrides the base path for all `getGlobalRoot()` calls. Implementation: a helper function `resolveGlobalRoot(adapter)` checks the env var first, falling back to the adapter's native path. This keeps env var handling out of individual adapters. | ||
|
|
||
| ### 5. Codex migration | ||
|
|
||
| The Codex adapter currently returns an absolute path from `getFilePath()`. Migration: | ||
| - `getFilePath()` → return project-relative path (`.codex/prompts/opsx-<id>.md`) for consistency | ||
| - `getGlobalRoot()` → return the current absolute base (`~/.codex/`) | ||
|
|
||
| This is a **breaking change for Codex project-local installs** (previously impossible since path was always absolute). In practice, Codex has always been global-only, so this formalizes what already exists. The `openspec init` default (non-global) will now create project-local Codex files; `openspec init --global --tools codex` preserves current behavior. | ||
|
|
||
| ## Risks / Trade-offs | ||
|
|
||
| **[Risk] Codex migration changes default behavior** → The current Codex adapter always writes globally. After migration, `openspec init --tools codex` (without `--global`) would write project-locally. This is actually more correct — global should be opt-in. Document in release notes. | ||
|
|
||
| **[Risk] Global files have no project association** → A globally-installed skill cannot reference project-specific paths. This is fine for OpenSpec skills which are project-agnostic instructions. If future skills need project context, they'll need project-local installation. | ||
|
|
||
| **[Risk] Multiple global installs from different OpenSpec versions** → Running `openspec init --global` from different projects with different OpenSpec versions could overwrite global files with different versions. `openspec update --global` mitigates this — run it from the latest version. No lockfile or version tracking needed initially. | ||
|
|
||
| **[Risk] Tool global paths change across versions** → If Claude Code moves its global directory, the adapter needs updating. Each adapter's `getGlobalRoot()` documents the source of truth for the path. Keep these paths in sync with upstream tool documentation. |
34 changes: 34 additions & 0 deletions
34
openspec/changes/archive/2026-02-23-init-global/proposal.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| ## Why | ||
|
|
||
| `openspec init` and `openspec update` always install skills and commands into the current project directory. This forces consultants, agencies, and developers who work across many client repositories to either commit OpenSpec files into repos they don't control or re-run `openspec init` in every project. Several supported tools already have first-class global installation paths (`~/.claude/`, `~/.config/opencode/`, `~/.codex/`), and Codex already proves the pattern works inside this codebase — its adapter returns an absolute path today. | ||
|
|
||
| Ref: [GitHub Issue #752](https://github.com/Fission-AI/OpenSpec/issues/752) | ||
|
|
||
| ## What Changes | ||
|
|
||
| - Add an optional `getGlobalRoot(): string | null` method to the `ToolCommandAdapter` interface — returns an absolute path to the tool's global configuration root, or `null` | ||
| - Implement `getGlobalRoot()` across all 23 existing adapters (Claude Code, OpenCode, Codex return paths; Cursor, Windsurf, and others return `null`) | ||
| - Migrate the Codex adapter so `getFilePath()` returns a project-relative path and `getGlobalRoot()` returns the current absolute path — behaviour unchanged, intent explicit | ||
| - Add `--global` flag to `openspec init` requiring `--tools <id|all>` — routes file writes to global paths derived from `getGlobalRoot()` | ||
| - Add `--global` flag to `openspec update` — regenerates globally-installed files | ||
| - Support `OPENSPEC_GLOBAL_ROOT` env var to override the base path for all global installs | ||
|
|
||
| ## Capabilities | ||
|
|
||
| ### New Capabilities | ||
| - `global-install`: Adapter-level global path resolution, `--global` flag routing in init/update, and `OPENSPEC_GLOBAL_ROOT` env var handling | ||
|
|
||
| ### Modified Capabilities | ||
| - `command-generation`: `ToolCommandAdapter` interface gains `getGlobalRoot()`, all adapters implement it, Codex adapter migrated | ||
| - `cli-init`: `InitCommand` gains `--global` flag, routes to global paths when set, requires `--tools` with `--global` | ||
| - `cli-update`: `UpdateCommand` gains `--global` flag, scopes update to globally-installed files | ||
|
|
||
| ## Impact | ||
|
|
||
| - **Adapters** (`src/core/command-generation/adapters/`): All 23 adapters gain a `getGlobalRoot()` method. Most return `null`. Claude, OpenCode, Codex return absolute paths. | ||
| - **Interface** (`src/core/command-generation/types.ts`): `ToolCommandAdapter` gains one optional method. | ||
| - **Registry** (`src/core/command-generation/registry.ts`): Gains `getGlobalAdapters()` helper to filter adapters with global support. | ||
| - **CLI** (`src/cli/index.ts`): `init` and `update` commands gain `--global` option. | ||
| - **Init** (`src/core/init.ts`): `InitCommand` gains `executeGlobal()` for global-path routing of both skills and commands. | ||
| - **Update** (`src/core/update.ts`): `UpdateCommand` gains `executeGlobal()` for global-scope updates. | ||
| - **Existing behaviour**: All project-local behaviour is unchanged. Global and local installs coexist — project-local takes precedence per each tool's own resolution order. |
54 changes: 54 additions & 0 deletions
54
openspec/changes/archive/2026-02-23-init-global/specs/cli-init/spec.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: Global installation mode | ||
|
|
||
| The `openspec init` command SHALL support a `--global` flag that installs skills and commands to tool global directories instead of project directories. | ||
|
|
||
| #### Scenario: Global init for a specific tool | ||
|
|
||
| - **WHEN** `openspec init --global --tools claude` is executed | ||
| - **THEN** the system SHALL write skills and commands to Claude Code's global directory (`~/.claude/`) | ||
| - **AND** the system SHALL NOT write any files to the current working directory | ||
| - **AND** the system SHALL NOT create the `openspec/` directory structure | ||
|
|
||
| #### Scenario: Global init for multiple tools | ||
|
|
||
| - **WHEN** `openspec init --global --tools claude,opencode` is executed | ||
| - **THEN** the system SHALL write skills and commands to each tool's respective global directory | ||
|
|
||
| #### Scenario: Global init for all supported tools | ||
|
|
||
| - **WHEN** `openspec init --global --tools all` is executed | ||
| - **THEN** the system SHALL install for all tools where `getGlobalRoot()` returns non-null | ||
| - **AND** the system SHALL print a summary listing installed tools and skipped tools (those without global support) | ||
|
|
||
| #### Scenario: Global init without --tools | ||
|
|
||
| - **WHEN** `openspec init --global` is executed without `--tools` | ||
| - **THEN** the system SHALL exit with a non-zero error code | ||
| - **AND** display: "--tools is required with --global. Use --tools all to install for all tools with a known global path." | ||
|
|
||
| #### Scenario: Global init for tool without global support | ||
|
|
||
| - **WHEN** `openspec init --global --tools cursor` is executed | ||
| - **AND** Cursor has no known global filesystem path | ||
| - **THEN** the system SHALL exit with a non-zero error code | ||
| - **AND** display a clear message that the specified tool does not support global installation | ||
|
|
||
| #### Scenario: Global init success output | ||
|
|
||
| - **WHEN** global initialization completes successfully | ||
| - **THEN** the system SHALL display a summary of files written per tool | ||
| - **AND** display the global directory paths used | ||
| - **AND** SHALL NOT display project-local "next steps" instructions | ||
|
|
||
| ### Requirement: Help text for global flag | ||
|
|
||
| The `openspec init --help` output SHALL document the `--global` flag. | ||
|
|
||
| #### Scenario: Help text content | ||
|
|
||
| - **WHEN** `openspec init --help` is displayed | ||
| - **THEN** the output SHALL document the `--global` flag | ||
| - **AND** note that `--tools` is required when using `--global` | ||
| - **AND** list which tools support global installation |
26 changes: 26 additions & 0 deletions
26
openspec/changes/archive/2026-02-23-init-global/specs/cli-update/spec.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: Global update mode | ||
|
|
||
| The `openspec update` command SHALL support a `--global` flag that updates globally-installed OpenSpec files. | ||
|
|
||
| #### Scenario: Global update | ||
|
|
||
| - **WHEN** `openspec update --global` is executed | ||
| - **THEN** the system SHALL regenerate globally-installed skill and command files for all tools that have global files present | ||
| - **AND** use the existing marker-based update logic to refresh managed content | ||
| - **AND** preserve any user-authored content outside OpenSpec markers | ||
|
|
||
| #### Scenario: Global update with no globally-installed files | ||
|
|
||
| - **WHEN** `openspec update --global` is executed | ||
| - **AND** no globally-installed OpenSpec files are found | ||
| - **THEN** the system SHALL display a message indicating no global installations were found | ||
| - **AND** suggest running `openspec init --global --tools <id>` first | ||
|
|
||
| #### Scenario: Non-global update unchanged | ||
|
|
||
| - **WHEN** `openspec update` is executed without `--global` | ||
| - **THEN** the system SHALL only update project-local files | ||
| - **AND** SHALL NOT modify or scan globally-installed files | ||
| - **AND** all existing update behavior SHALL remain unchanged | ||
askpatrickw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
61 changes: 61 additions & 0 deletions
61
openspec/changes/archive/2026-02-23-init-global/specs/command-generation/spec.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| ## MODIFIED Requirements | ||
|
|
||
| ### Requirement: ToolCommandAdapter interface | ||
|
|
||
| The system SHALL define a `ToolCommandAdapter` interface for per-tool formatting. | ||
|
|
||
| #### Scenario: Adapter interface structure | ||
|
|
||
| - **WHEN** implementing a tool adapter | ||
| - **THEN** `ToolCommandAdapter` SHALL require: | ||
| - `toolId`: string identifier matching `AIToolOption.value` | ||
| - `getFilePath(commandId: string)`: returns file path for command (relative from project root) | ||
| - `formatFile(content: CommandContent)`: returns complete file content with frontmatter | ||
| - **AND** `ToolCommandAdapter` SHALL optionally support: | ||
| - `getGlobalRoot()`: returns absolute path to the tool's global configuration directory, or `null` if the tool has no global filesystem path | ||
|
|
||
| #### Scenario: Claude adapter formatting | ||
|
|
||
| - **WHEN** formatting a command for Claude Code | ||
| - **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields | ||
| - **AND** file path SHALL follow pattern `.claude/commands/opsx/<id>.md` | ||
| - **AND** `getGlobalRoot()` SHALL return `~/.claude/` (macOS/Linux) or `%APPDATA%\Claude\` (Windows) | ||
|
|
||
| #### Scenario: Cursor adapter formatting | ||
|
|
||
| - **WHEN** formatting a command for Cursor | ||
| - **THEN** the adapter SHALL output YAML frontmatter with `name` as `/opsx-<id>`, `id`, `category`, `description` fields | ||
| - **AND** file path SHALL follow pattern `.cursor/commands/opsx-<id>.md` | ||
| - **AND** `getGlobalRoot()` SHALL return `null` | ||
|
|
||
| #### Scenario: Windsurf adapter formatting | ||
|
|
||
| - **WHEN** formatting a command for Windsurf | ||
| - **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields | ||
| - **AND** file path SHALL follow pattern `.windsurf/workflows/opsx-<id>.md` | ||
| - **AND** `getGlobalRoot()` SHALL return `null` | ||
|
|
||
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: CommandAdapterRegistry global filtering | ||
|
|
||
| The registry SHALL support filtering adapters by global installation support. | ||
|
|
||
| #### Scenario: Get adapters with global support | ||
|
|
||
| - **WHEN** calling `CommandAdapterRegistry.getGlobalAdapters()` | ||
| - **THEN** it SHALL return an array of adapters where `getGlobalRoot()` returns a non-null value | ||
|
|
||
| ### Requirement: Codex adapter migration | ||
|
|
||
| The Codex adapter SHALL separate project-local and global path concerns. | ||
|
|
||
| #### Scenario: Codex project-local path | ||
|
|
||
| - **WHEN** calling `getFilePath()` on the Codex adapter | ||
| - **THEN** it SHALL return a project-relative path: `.codex/prompts/opsx-<id>.md` | ||
|
|
||
| #### Scenario: Codex global path | ||
|
|
||
| - **WHEN** calling `getGlobalRoot()` on the Codex adapter | ||
| - **THEN** it SHALL return the absolute path to the Codex home directory (respecting `$CODEX_HOME`, defaulting to `~/.codex/`) |
80 changes: 80 additions & 0 deletions
80
openspec/changes/archive/2026-02-23-init-global/specs/global-install/spec.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: Global root resolution | ||
|
|
||
| The system SHALL resolve a tool's global installation root directory via the adapter interface. | ||
|
|
||
| #### Scenario: Adapter with known global root | ||
|
|
||
| - **WHEN** calling `getGlobalRoot()` on a tool adapter that supports global installation (e.g., Claude Code, OpenCode, Codex) | ||
| - **THEN** it SHALL return an absolute path to the tool's global configuration directory | ||
| - **AND** the path SHALL follow the tool's own documented convention per platform | ||
|
|
||
| #### Scenario: Adapter without global support | ||
|
|
||
| - **WHEN** calling `getGlobalRoot()` on a tool adapter that has no global filesystem path (e.g., Cursor, Windsurf) | ||
| - **THEN** it SHALL return `null` | ||
|
|
||
| #### Scenario: OPENSPEC_GLOBAL_ROOT override | ||
|
|
||
| - **WHEN** the `OPENSPEC_GLOBAL_ROOT` environment variable is set | ||
| - **THEN** the system SHALL use the env var value as the base path for all global installs, replacing the adapter's native global root | ||
| - **AND** the per-tool subdirectory structure SHALL be preserved under the override path | ||
|
|
||
| ### Requirement: Global path derivation for commands | ||
|
|
||
| The system SHALL derive global command file paths from the adapter's global root. | ||
|
|
||
| #### Scenario: Deriving global command path | ||
|
|
||
| - **WHEN** a tool adapter returns a non-null global root | ||
| - **AND** the system needs the global path for a command | ||
| - **THEN** the system SHALL construct the absolute path by combining the global root with the tool's command path pattern (e.g., `<globalRoot>/commands/opsx/<id>.md` for Claude Code) | ||
|
|
||
| ### Requirement: Global path derivation for skills | ||
|
|
||
| The system SHALL derive global skill file paths from the adapter's global root. | ||
|
|
||
| #### Scenario: Deriving global skill path | ||
|
|
||
| - **WHEN** a tool adapter returns a non-null global root | ||
| - **AND** the system needs the global path for a skill | ||
| - **THEN** the system SHALL construct the absolute path by combining the global root with the tool's skill path pattern (e.g., `<globalRoot>/skills/openspec-<id>/SKILL.md` for Claude Code) | ||
|
|
||
| ### Requirement: Global tool path reference | ||
|
|
||
| The system SHALL use the following global root paths per tool and platform. | ||
|
|
||
| #### Scenario: Claude Code global root | ||
|
|
||
| - **WHEN** resolving the global root for Claude Code | ||
| - **THEN** on macOS and Linux it SHALL be `~/.claude/` | ||
| - **AND** on Windows it SHALL be `%APPDATA%\Claude\` | ||
|
|
||
| #### Scenario: OpenCode global root | ||
|
|
||
| - **WHEN** resolving the global root for OpenCode | ||
| - **THEN** it SHALL respect `$XDG_CONFIG_HOME` if set, defaulting to `~/.config/opencode/` | ||
| - **AND** on Windows it SHALL be `%APPDATA%\opencode\` | ||
|
|
||
| #### Scenario: Codex global root | ||
|
|
||
| - **WHEN** resolving the global root for Codex | ||
| - **THEN** it SHALL respect `$CODEX_HOME` if set, defaulting to `~/.codex/` | ||
| - **AND** on Windows it SHALL be `%USERPROFILE%\.codex\` | ||
|
|
||
| ### Requirement: Global and local coexistence | ||
|
|
||
| Global and project-local installations SHALL coexist without conflict. | ||
|
|
||
| #### Scenario: Both global and local installed | ||
|
|
||
| - **WHEN** a tool has both global and project-local OpenSpec files | ||
| - **THEN** the project-local files SHALL take precedence per each tool's own resolution order | ||
| - **AND** global files serve as a fallback for projects without local installation | ||
|
|
||
| #### Scenario: Update scoping | ||
|
|
||
| - **WHEN** `openspec update` is run without `--global` | ||
| - **THEN** the system SHALL only update project-local files | ||
| - **AND** SHALL NOT modify globally-installed files |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.