@fission-ai/openspec 1.14.0 → 1.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -31
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.js +88 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +20 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +18 -0
- package/dist/cli/commands/schema.d.ts +6 -0
- package/dist/cli/commands/schema.js +57 -0
- package/dist/cli/commands/spec.d.ts +3 -0
- package/dist/cli/commands/spec.js +42 -0
- package/dist/cli/commands/store.d.ts +3 -0
- package/dist/cli/commands/store.js +130 -0
- package/dist/cli/commands/workset.d.ts +3 -0
- package/dist/cli/commands/workset.js +96 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +86 -43
- package/dist/commands/completion.js +4 -1
- package/dist/commands/config.d.ts +16 -7
- package/dist/commands/config.js +342 -395
- package/dist/commands/context.d.ts +8 -2
- package/dist/commands/context.js +22 -37
- package/dist/commands/doctor.d.ts +6 -7
- package/dist/commands/doctor.js +14 -27
- package/dist/commands/schema.d.ts +19 -5
- package/dist/commands/schema.js +662 -702
- package/dist/commands/spec.d.ts +13 -4
- package/dist/commands/spec.js +91 -120
- package/dist/commands/store.d.ts +27 -2
- package/dist/commands/store.js +1 -120
- package/dist/commands/workflow/default-schema.d.ts +2 -0
- package/dist/commands/workflow/default-schema.js +4 -0
- package/dist/commands/workflow/instructions.js +1 -1
- package/dist/commands/workflow/shared.d.ts +1 -1
- package/dist/commands/workflow/shared.js +1 -1
- package/dist/commands/workset.d.ts +22 -2
- package/dist/commands/workset.js +1 -88
- package/dist/core/archive.js +32 -12
- package/dist/core/artifact-graph/outputs.js +39 -1
- package/dist/core/completion-tip.js +3 -1
- package/dist/core/completions/completion-provider.js +4 -2
- package/dist/core/config-schema.d.ts +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +11 -0
- package/dist/core/parsers/requirement-blocks.js +57 -0
- package/dist/core/specs-apply.js +5 -1
- package/dist/core/templates/workflows/apply-change.js +4 -3
- package/dist/core/templates/workflows/continue-change.js +17 -0
- package/dist/core/templates/workflows/explore.js +8 -4
- package/dist/core/templates/workflows/update-change.js +4 -4
- package/dist/core/templates/workflows/verify-change.js +26 -20
- package/dist/core/validation/validator.js +23 -2
- package/dist/core/view.js +2 -26
- package/dist/utils/change-metadata.d.ts +17 -0
- package/dist/utils/change-metadata.js +27 -2
- package/dist/utils/file-system.d.ts +7 -0
- package/dist/utils/file-system.js +25 -0
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +28 -4
package/README.md
CHANGED
|
@@ -33,11 +33,6 @@ Our philosophy:
|
|
|
33
33
|
→ scalable from personal projects to enterprises
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
> [!TIP]
|
|
37
|
-
> **New workflow now available!** We've rebuilt OpenSpec with a new artifact-guided workflow.
|
|
38
|
-
>
|
|
39
|
-
> Run `/opsx:propose "your idea"` to get started. → [Learn more here](docs/opsx.md)
|
|
40
|
-
|
|
41
36
|
<p align="center">
|
|
42
37
|
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
|
|
43
38
|
</p>
|
|
@@ -112,13 +107,13 @@ OpenSpec is built with OpenSpec — browse this repo's live [specs](openspec/spe
|
|
|
112
107
|
|
|
113
108
|
Solo, OpenSpec keeps you and your AI honest on a single repo. On a team, the hard part moves: a feature spans the API server, the web app, and a shared library; requirements are owned by one team and consumed by others; planning starts before any code exists.
|
|
114
109
|
|
|
115
|
-
**[Stores](docs/stores
|
|
110
|
+
**[Stores](https://openspec.dev/docs/stores)** are the answer — planning in a repo of its own. The same `openspec/` shape you already know (specs and changes), shared by `git push` like anything else. One source of truth your whole team and every coding agent can read, across every repo.
|
|
116
111
|
|
|
117
112
|
- **Cross-repo features** — one change, one plan, even when the code lands in three repos.
|
|
118
113
|
- **Shared requirements** — a platform team owns the specs; product teams reference them read-only, right where their coding agent can read them. No drifting wiki.
|
|
119
114
|
- **Plan before code** — capture the plan in the store now; the code repos catch up later.
|
|
120
115
|
|
|
121
|
-
> Stores are in **beta**. Start with the [Stores
|
|
116
|
+
> Stores are in **beta**. Start with the [Stores guide](https://openspec.dev/docs/stores).
|
|
122
117
|
|
|
123
118
|
## Quick Start
|
|
124
119
|
|
|
@@ -143,44 +138,33 @@ cd your-project
|
|
|
143
138
|
openspec init
|
|
144
139
|
```
|
|
145
140
|
|
|
146
|
-
> **Want your AI to do it?** Paste the [setup prompt](docs
|
|
141
|
+
> **Want your AI to do it?** Paste the [setup prompt](https://openspec.dev/docs/installation#install-with-your-ai-assistant) into your coding assistant — it installs the CLI, runs `openspec init`, and verifies the result.
|
|
147
142
|
|
|
148
143
|
Now talk to your AI:
|
|
149
144
|
|
|
150
|
-
- **Not sure what to build yet?** Start with `/opsx:explore`, a no-stakes thinking partner that reads your code, weighs options, and shapes a plan before any code gets written. ([
|
|
145
|
+
- **Not sure what to build yet?** Start with `/opsx:explore`, a no-stakes thinking partner that reads your code, weighs options, and shapes a plan before any code gets written. ([Quickstart](https://openspec.dev/docs/quickstart))
|
|
151
146
|
- **Already know what you want?** Go straight to `/opsx:propose <what-you-want-to-build>`.
|
|
152
147
|
|
|
153
148
|
Both are in the default profile. If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
|
|
154
149
|
|
|
155
|
-
`/opsx:propose` is the canonical name; your tool may spell it `/opsx-propose` (Cursor, GitHub Copilot), `@opsx-propose` (Amazon Q) or `$openspec-propose` (Codex). `openspec init` prints the right form for the tools you picked — see [
|
|
150
|
+
`/opsx:propose` is the canonical name; your tool may spell it `/opsx-propose` (Cursor, GitHub Copilot), `@opsx-propose` (Amazon Q) or `$openspec-propose` (Codex). `openspec init` prints the right form for the tools you picked — see [Supported Tools](https://openspec.dev/docs/supported-tools).
|
|
156
151
|
|
|
157
152
|
> [!NOTE]
|
|
158
|
-
> Not sure if your tool is supported? [View the full list](docs/supported-tools
|
|
153
|
+
> Not sure if your tool is supported? [View the full list](https://openspec.dev/docs/supported-tools) – we support 30+ tools and growing.
|
|
159
154
|
>
|
|
160
|
-
> Also works with Homebrew, pnpm, yarn, bun, and Nix. [See installation options](docs
|
|
155
|
+
> Also works with Homebrew, pnpm, yarn, bun, and Nix. [See installation options](https://openspec.dev/docs/installation).
|
|
161
156
|
|
|
162
157
|
## Docs
|
|
163
158
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
→ **[Getting Started](docs/getting-started.md)**: first steps<br>
|
|
167
|
-
→ **[Explore First](docs/explore.md)**: think it through with `/opsx:explore` before you commit<br>
|
|
168
|
-
→ **[How Commands Work](docs/how-commands-work.md)**: where slash commands run vs the CLI<br>
|
|
169
|
-
→ **[Core Concepts at a Glance](docs/overview.md)**: the whole mental model, one page<br>
|
|
170
|
-
→ **[Examples & Recipes](docs/examples.md)**: real changes, start to finish<br>
|
|
171
|
-
→ **[Workflows](docs/workflows.md)**: combos and patterns<br>
|
|
172
|
-
→ **[Existing Projects](docs/existing-projects.md)**: adopt OpenSpec on a brownfield codebase<br>
|
|
173
|
-
→ **[Editing a Change](docs/editing-changes.md)**: update artifacts, go back, reconcile manual edits<br>
|
|
174
|
-
→ **[Commands](docs/commands.md)**: slash commands & skills<br>
|
|
175
|
-
→ **[CLI](docs/cli.md)**: terminal reference<br>
|
|
176
|
-
→ **[Stores](docs/stores-beta/user-guide.md)**: plan in a separate repo, shared across your team (beta)<br>
|
|
177
|
-
→ **[Supported Tools](docs/supported-tools.md)**: tool integrations & install paths<br>
|
|
178
|
-
→ **[Concepts](docs/concepts.md)**: how it all fits<br>
|
|
179
|
-
→ **[Multi-Language](docs/multi-language.md)**: multi-language support<br>
|
|
180
|
-
→ **[Customization](docs/customization.md)**: make it yours<br>
|
|
181
|
-
→ **[Community Showcase](docs/community.md)**: projects and resources built with and for OpenSpec<br>
|
|
182
|
-
→ **[FAQ](docs/faq.md)** · **[Troubleshooting](docs/troubleshooting.md)** · **[Glossary](docs/glossary.md)**: quick help
|
|
159
|
+
Full documentation lives at **[openspec.dev/docs](https://openspec.dev/docs)**.
|
|
183
160
|
|
|
161
|
+
→ **[Quickstart](https://openspec.dev/docs/quickstart)**: your first change, start to finish<br>
|
|
162
|
+
→ **[Set up your project](https://openspec.dev/docs/setup)**: what `openspec init` installs<br>
|
|
163
|
+
→ **[CLI](https://openspec.dev/docs/cli)**: terminal reference<br>
|
|
164
|
+
→ **[Supported Tools](https://openspec.dev/docs/supported-tools)**: tool integrations & install paths<br>
|
|
165
|
+
→ **[Customize](https://openspec.dev/docs/customize)**: profiles, project config, and schemas<br>
|
|
166
|
+
→ **[Stores](https://openspec.dev/docs/stores)**: plan in a separate repo, shared across your team (beta)<br>
|
|
167
|
+
→ **[Community Showcase](docs/community.md)**: projects and resources built with and for OpenSpec
|
|
184
168
|
|
|
185
169
|
## Community schemas
|
|
186
170
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* Register the config command and all its subcommands.
|
|
4
|
+
*
|
|
5
|
+
* @param program - The Commander program instance
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerConfigCommand(program: Command): void;
|
|
8
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register the config command and all its subcommands.
|
|
3
|
+
*
|
|
4
|
+
* @param program - The Commander program instance
|
|
5
|
+
*/
|
|
6
|
+
export function registerConfigCommand(program) {
|
|
7
|
+
const configCmd = program
|
|
8
|
+
.command('config')
|
|
9
|
+
.description('View and modify global OpenSpec configuration')
|
|
10
|
+
.option('--scope <scope>', 'Config scope (only "global" supported currently)')
|
|
11
|
+
.hook('preAction', (thisCommand) => {
|
|
12
|
+
const opts = thisCommand.opts();
|
|
13
|
+
if (opts.scope && opts.scope !== 'global') {
|
|
14
|
+
console.error('Error: Project-local config is not yet implemented');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
// config path
|
|
19
|
+
configCmd
|
|
20
|
+
.command('path')
|
|
21
|
+
.description('Show config file location')
|
|
22
|
+
.action(async () => {
|
|
23
|
+
const { configPathCommand } = await import('../../commands/config.js');
|
|
24
|
+
configPathCommand();
|
|
25
|
+
});
|
|
26
|
+
// config list
|
|
27
|
+
configCmd
|
|
28
|
+
.command('list')
|
|
29
|
+
.description('Show all current settings')
|
|
30
|
+
.option('--json', 'Output as JSON')
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
const { configListCommand } = await import('../../commands/config.js');
|
|
33
|
+
configListCommand(options);
|
|
34
|
+
});
|
|
35
|
+
// config get
|
|
36
|
+
configCmd
|
|
37
|
+
.command('get <key>')
|
|
38
|
+
.description('Get a specific value (raw, scriptable)')
|
|
39
|
+
.action(async (key) => {
|
|
40
|
+
const { configGetCommand } = await import('../../commands/config.js');
|
|
41
|
+
configGetCommand(key);
|
|
42
|
+
});
|
|
43
|
+
// config set
|
|
44
|
+
configCmd
|
|
45
|
+
.command('set <key> <value>')
|
|
46
|
+
.description('Set a value (auto-coerce types)')
|
|
47
|
+
.option('--string', 'Force value to be stored as string')
|
|
48
|
+
.option('--allow-unknown', 'Allow setting unknown keys')
|
|
49
|
+
.action(async (key, value, options) => {
|
|
50
|
+
const { configSetCommand } = await import('../../commands/config.js');
|
|
51
|
+
configSetCommand(key, value, options);
|
|
52
|
+
});
|
|
53
|
+
// config unset
|
|
54
|
+
configCmd
|
|
55
|
+
.command('unset <key>')
|
|
56
|
+
.description('Remove a key (revert to default)')
|
|
57
|
+
.action(async (key) => {
|
|
58
|
+
const { configUnsetCommand } = await import('../../commands/config.js');
|
|
59
|
+
configUnsetCommand(key);
|
|
60
|
+
});
|
|
61
|
+
// config reset
|
|
62
|
+
configCmd
|
|
63
|
+
.command('reset')
|
|
64
|
+
.description('Reset configuration to defaults')
|
|
65
|
+
.option('--all', 'Reset all configuration (required)')
|
|
66
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
const { configResetCommand } = await import('../../commands/config.js');
|
|
69
|
+
await configResetCommand(options);
|
|
70
|
+
});
|
|
71
|
+
// config edit
|
|
72
|
+
configCmd
|
|
73
|
+
.command('edit')
|
|
74
|
+
.description('Open config in $EDITOR')
|
|
75
|
+
.action(async () => {
|
|
76
|
+
const { configEditCommand } = await import('../../commands/config.js');
|
|
77
|
+
await configEditCommand();
|
|
78
|
+
});
|
|
79
|
+
// config profile [preset]
|
|
80
|
+
configCmd
|
|
81
|
+
.command('profile [preset]')
|
|
82
|
+
.description('Configure workflow profile (interactive picker or preset shortcut)')
|
|
83
|
+
.action(async (preset) => {
|
|
84
|
+
const { configProfileCommand } = await import('../../commands/config.js');
|
|
85
|
+
await configProfileCommand(preset);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
2
|
+
import { COMMAND_REGISTRY } from '../../core/completions/command-registry.js';
|
|
3
|
+
import { COMMON_FLAGS } from '../../core/completions/shared-flags.js';
|
|
4
|
+
export function registerContextCommand(program) {
|
|
5
|
+
const description = COMMAND_REGISTRY.find((entry) => entry.name === 'context')?.description ??
|
|
6
|
+
'Print the working context for the resolved OpenSpec root';
|
|
7
|
+
program
|
|
8
|
+
.command('context')
|
|
9
|
+
.description(description)
|
|
10
|
+
.option('--store <id>', COMMON_FLAGS.store.description)
|
|
11
|
+
.addOption(new Option('--store-path <path>', 'Removed; register the store and use --store').hideHelp())
|
|
12
|
+
.option('--json', 'Output the agent brief as JSON')
|
|
13
|
+
.option('--code-workspace <path>', 'Also write a VS Code workspace file for the set')
|
|
14
|
+
.option('--force', 'Overwrite an existing --code-workspace file')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
const { contextCommand } = await import('../../commands/context.js');
|
|
17
|
+
await contextCommand(options);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
2
|
+
import { COMMAND_REGISTRY } from '../../core/completions/command-registry.js';
|
|
3
|
+
import { COMMON_FLAGS } from '../../core/completions/shared-flags.js';
|
|
4
|
+
export function registerDoctorCommand(program) {
|
|
5
|
+
const description = COMMAND_REGISTRY.find((entry) => entry.name === 'doctor')?.description ??
|
|
6
|
+
'Report relationship health for the resolved OpenSpec root';
|
|
7
|
+
program
|
|
8
|
+
.command('doctor')
|
|
9
|
+
.description(description)
|
|
10
|
+
.option('--store <id>', COMMON_FLAGS.store.description)
|
|
11
|
+
.addOption(new Option('--store-path <path>', 'Removed; register the store and use --store').hideHelp())
|
|
12
|
+
.option('--json', 'Output as JSON')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const { doctorCommand } = await import('../../commands/doctor.js');
|
|
15
|
+
await doctorCommand(options);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register the schema command and all its subcommands.
|
|
3
|
+
*/
|
|
4
|
+
export function registerSchemaCommand(program) {
|
|
5
|
+
const schemaCmd = program
|
|
6
|
+
.command('schema')
|
|
7
|
+
.description('Manage workflow schemas [experimental]');
|
|
8
|
+
// Experimental warning
|
|
9
|
+
schemaCmd.hook('preAction', () => {
|
|
10
|
+
console.error('Note: Schema commands are experimental and may change.');
|
|
11
|
+
});
|
|
12
|
+
// schema which
|
|
13
|
+
schemaCmd
|
|
14
|
+
.command('which [name]')
|
|
15
|
+
.description('Show where a schema resolves from')
|
|
16
|
+
.option('--json', 'Output as JSON')
|
|
17
|
+
.option('--all', 'List all schemas with their resolution sources')
|
|
18
|
+
.action(async (name, options) => {
|
|
19
|
+
const { schemaWhichCommand } = await import('../../commands/schema.js');
|
|
20
|
+
await schemaWhichCommand(name, options);
|
|
21
|
+
});
|
|
22
|
+
// schema validate
|
|
23
|
+
schemaCmd
|
|
24
|
+
.command('validate [name]')
|
|
25
|
+
.description('Validate a schema structure and templates')
|
|
26
|
+
.option('--json', 'Output as JSON')
|
|
27
|
+
.option('--verbose', 'Show detailed validation steps')
|
|
28
|
+
.action(async (name, options) => {
|
|
29
|
+
const { schemaValidateCommand } = await import('../../commands/schema.js');
|
|
30
|
+
await schemaValidateCommand(name, options);
|
|
31
|
+
});
|
|
32
|
+
// schema fork
|
|
33
|
+
schemaCmd
|
|
34
|
+
.command('fork <source> [name]')
|
|
35
|
+
.description('Copy an existing schema to project for customization')
|
|
36
|
+
.option('--json', 'Output as JSON')
|
|
37
|
+
.option('--force', 'Overwrite existing destination')
|
|
38
|
+
.action(async (source, name, options) => {
|
|
39
|
+
const { schemaForkCommand } = await import('../../commands/schema.js');
|
|
40
|
+
await schemaForkCommand(source, name, options);
|
|
41
|
+
});
|
|
42
|
+
// schema init
|
|
43
|
+
schemaCmd
|
|
44
|
+
.command('init <name>')
|
|
45
|
+
.description('Create a new project-local schema')
|
|
46
|
+
.option('--json', 'Output as JSON')
|
|
47
|
+
.option('--description <text>', 'Schema description')
|
|
48
|
+
.option('--artifacts <list>', 'Comma-separated artifact IDs (proposal,specs,design,tasks)')
|
|
49
|
+
.option('--default', 'Set as project default schema')
|
|
50
|
+
.option('--no-default', 'Do not prompt to set as default')
|
|
51
|
+
.option('--force', 'Overwrite existing schema')
|
|
52
|
+
.action(async (name, options) => {
|
|
53
|
+
const { schemaInitCommand } = await import('../../commands/schema.js');
|
|
54
|
+
await schemaInitCommand(name, options);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function registerSpecCommand(rootProgram) {
|
|
2
|
+
const specCommand = rootProgram
|
|
3
|
+
.command('spec')
|
|
4
|
+
.description('Manage and view OpenSpec specifications');
|
|
5
|
+
// Deprecation notice for noun-based commands
|
|
6
|
+
specCommand.hook('preAction', () => {
|
|
7
|
+
console.error('Warning: The "openspec spec ..." commands are deprecated. Prefer verb-first commands (e.g., "openspec show", "openspec validate --specs").');
|
|
8
|
+
});
|
|
9
|
+
specCommand
|
|
10
|
+
.command('show [spec-id]')
|
|
11
|
+
.description('Display a specific specification')
|
|
12
|
+
.option('--json', 'Output as JSON')
|
|
13
|
+
.option('--requirements', 'JSON only: Show only requirements (exclude scenarios)')
|
|
14
|
+
.option('--no-scenarios', 'JSON only: Exclude scenario content')
|
|
15
|
+
.option('-r, --requirement <id>', 'JSON only: Show specific requirement by ID (1-based)')
|
|
16
|
+
.option('--no-interactive', 'Disable interactive prompts')
|
|
17
|
+
.action(async (specId, options) => {
|
|
18
|
+
const { specShowCommand } = await import('../../commands/spec.js');
|
|
19
|
+
await specShowCommand(specId, options);
|
|
20
|
+
});
|
|
21
|
+
specCommand
|
|
22
|
+
.command('list')
|
|
23
|
+
.description('List all available specifications')
|
|
24
|
+
.option('--json', 'Output as JSON')
|
|
25
|
+
.option('--long', 'Show id and title with counts')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
const { specListCommand } = await import('../../commands/spec.js');
|
|
28
|
+
await specListCommand(options);
|
|
29
|
+
});
|
|
30
|
+
specCommand
|
|
31
|
+
.command('validate [spec-id]')
|
|
32
|
+
.description('Validate a specification structure')
|
|
33
|
+
.option('--strict', 'Enable strict validation mode')
|
|
34
|
+
.option('--json', 'Output validation report as JSON')
|
|
35
|
+
.option('--no-interactive', 'Disable interactive prompts')
|
|
36
|
+
.action(async (specId, options) => {
|
|
37
|
+
const { specValidateCommand } = await import('../../commands/spec.js');
|
|
38
|
+
await specValidateCommand(specId, options);
|
|
39
|
+
});
|
|
40
|
+
return specCommand;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=spec.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { COMMAND_REGISTRY } from '../../core/completions/command-registry.js';
|
|
2
|
+
import { printJson } from '../../commands/shared-output.js';
|
|
3
|
+
async function loadStoreCommand() {
|
|
4
|
+
const { StoreCommand } = await import('../../commands/store.js');
|
|
5
|
+
return new StoreCommand();
|
|
6
|
+
}
|
|
7
|
+
export function registerStoreCommand(program) {
|
|
8
|
+
// One source for the locked group one-liner: the completions registry
|
|
9
|
+
// entry, which shell completion scripts also consume.
|
|
10
|
+
const storeGroupDescription = COMMAND_REGISTRY.find((entry) => entry.name === 'store')?.description ??
|
|
11
|
+
'Create and manage stores - standalone OpenSpec repos you register on this machine';
|
|
12
|
+
const store = program.command('store').description(storeGroupDescription);
|
|
13
|
+
store
|
|
14
|
+
.command('setup [id]')
|
|
15
|
+
.description('Create and register a local store')
|
|
16
|
+
.option('--path <path>', 'Folder where the store should live (for example ~/openspec/<id>)')
|
|
17
|
+
.option('--init-git', 'Initialize a Git repository with an initial commit (default)')
|
|
18
|
+
.option('--no-init-git', 'Skip every Git action: no init, no initial commit')
|
|
19
|
+
.option('--remote <url>', 'Canonical clone source recorded in store.yaml')
|
|
20
|
+
.option('--json', 'Output as JSON')
|
|
21
|
+
.action(async (id, options) => {
|
|
22
|
+
const storeCommand = await loadStoreCommand();
|
|
23
|
+
await storeCommand.setup(id, options);
|
|
24
|
+
});
|
|
25
|
+
store
|
|
26
|
+
.command('register [path]')
|
|
27
|
+
.description('Register an existing local store')
|
|
28
|
+
.option('--id <id>', 'Store id; defaults to metadata or folder name')
|
|
29
|
+
.option('--yes', 'Confirm creating store identity metadata for a healthy OpenSpec root')
|
|
30
|
+
.option('--json', 'Output as JSON')
|
|
31
|
+
.action(async (inputPath, options) => {
|
|
32
|
+
const storeCommand = await loadStoreCommand();
|
|
33
|
+
await storeCommand.register(inputPath, options);
|
|
34
|
+
});
|
|
35
|
+
store
|
|
36
|
+
.command('unregister <id>')
|
|
37
|
+
.description('Forget a local store registration without deleting files')
|
|
38
|
+
.option('--json', 'Output as JSON')
|
|
39
|
+
.action(async (id, options) => {
|
|
40
|
+
const storeCommand = await loadStoreCommand();
|
|
41
|
+
await storeCommand.unregister(id, options);
|
|
42
|
+
});
|
|
43
|
+
store
|
|
44
|
+
.command('remove <id>')
|
|
45
|
+
.description('Forget a local store registration and delete its local folder')
|
|
46
|
+
.option('--yes', 'Confirm local store folder deletion')
|
|
47
|
+
.option('--json', 'Output as JSON')
|
|
48
|
+
.action(async (id, options) => {
|
|
49
|
+
const storeCommand = await loadStoreCommand();
|
|
50
|
+
await storeCommand.remove(id, options);
|
|
51
|
+
});
|
|
52
|
+
store
|
|
53
|
+
.command('list')
|
|
54
|
+
.alias('ls')
|
|
55
|
+
.description('List locally registered stores')
|
|
56
|
+
.option('--json', 'Output as JSON')
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
const storeCommand = await loadStoreCommand();
|
|
59
|
+
await storeCommand.list(options);
|
|
60
|
+
});
|
|
61
|
+
store
|
|
62
|
+
.command('doctor [id]')
|
|
63
|
+
.description('Check local store registration and metadata')
|
|
64
|
+
.option('--json', 'Output as JSON')
|
|
65
|
+
.action(async (id, options) => {
|
|
66
|
+
const storeCommand = await loadStoreCommand();
|
|
67
|
+
await storeCommand.doctor(id, options);
|
|
68
|
+
});
|
|
69
|
+
const lifecycleRedirects = new Set(COMMAND_REGISTRY.filter((entry) => entry.flags.some((flag) => flag.name === 'store') ||
|
|
70
|
+
(entry.subcommands ?? []).some((subcommand) => subcommand.flags.some((flag) => flag.name === 'store'))).map((entry) => entry.name));
|
|
71
|
+
const storeSubcommandsLine = store.commands
|
|
72
|
+
.map((subcommand) => {
|
|
73
|
+
const aliases = subcommand.aliases();
|
|
74
|
+
return aliases.length > 0 ? `${subcommand.name()} (${aliases.join(', ')})` : subcommand.name();
|
|
75
|
+
})
|
|
76
|
+
.join(', ');
|
|
77
|
+
// One group action owns missing AND unknown subcommands. Known
|
|
78
|
+
// subcommands dispatch above; everything else — including a bare
|
|
79
|
+
// `store --json` with no operand — lands here, so the handler owns the
|
|
80
|
+
// entire message and exit path (same text for human and --json). The
|
|
81
|
+
// permissive flags route unknown operands/options here instead of
|
|
82
|
+
// letting Commander emit a raw error before the action runs. We detect
|
|
83
|
+
// `--json` in the residual args rather than declaring a group option,
|
|
84
|
+
// which would otherwise shadow each subcommand's own `--json` flag.
|
|
85
|
+
store.allowExcessArguments(true);
|
|
86
|
+
store.allowUnknownOption(true);
|
|
87
|
+
store.action(() => {
|
|
88
|
+
const operands = store.args;
|
|
89
|
+
// Flag values are indistinguishable from operands without a full
|
|
90
|
+
// parse, so the verbatim echo only applies to plain-operand input.
|
|
91
|
+
const attempted = operands.filter((operand) => !operand.startsWith('-'));
|
|
92
|
+
const hasFlagLikeToken = operands.some((operand) => operand.startsWith('-'));
|
|
93
|
+
// The agent contract: --json failures emit one JSON document.
|
|
94
|
+
if (operands.includes('--json')) {
|
|
95
|
+
const message = attempted.length > 0
|
|
96
|
+
? `Unknown command '${attempted[0]}' for 'openspec store'. Store subcommands: ${storeSubcommandsLine}.`
|
|
97
|
+
: `Missing subcommand for 'openspec store'. Store subcommands: ${storeSubcommandsLine}.`;
|
|
98
|
+
printJson({
|
|
99
|
+
status: [
|
|
100
|
+
{
|
|
101
|
+
severity: 'error',
|
|
102
|
+
code: 'unknown_store_subcommand',
|
|
103
|
+
message,
|
|
104
|
+
fix: 'Run a store subcommand, or use the lifecycle command with --store <id>.',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
let example = 'openspec new change <change-id> --store <id>';
|
|
112
|
+
if (!hasFlagLikeToken && attempted.length > 0 && lifecycleRedirects.has(attempted[0])) {
|
|
113
|
+
if (attempted[0] === 'new') {
|
|
114
|
+
const changeId = attempted[1] === 'change' && attempted[2] ? attempted[2] : '<change-id>';
|
|
115
|
+
example = `openspec new change ${changeId} --store <id>`;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
example = `openspec ${attempted.join(' ')} --store <id>`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.error(attempted.length > 0
|
|
122
|
+
? `Error: unknown command '${attempted[0]}' for 'openspec store'.`
|
|
123
|
+
: "Error: missing subcommand for 'openspec store'.");
|
|
124
|
+
console.error(`Store subcommands manage store registration: ${storeSubcommandsLine}.`);
|
|
125
|
+
console.error('To create or work on a change in a store, use the normal command with --store, for example:');
|
|
126
|
+
console.error(` ${example}`);
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
2
|
+
import { COMMAND_REGISTRY } from '../../core/completions/command-registry.js';
|
|
3
|
+
import { printJson } from '../../commands/shared-output.js';
|
|
4
|
+
async function loadWorksetCommand() {
|
|
5
|
+
const { WorksetCommand } = await import('../../commands/workset.js');
|
|
6
|
+
return new WorksetCommand();
|
|
7
|
+
}
|
|
8
|
+
function collectMember(value, previous) {
|
|
9
|
+
return [...previous, value];
|
|
10
|
+
}
|
|
11
|
+
export function registerWorksetCommand(program) {
|
|
12
|
+
const groupDescription = COMMAND_REGISTRY.find((entry) => entry.name === 'workset')?.description ??
|
|
13
|
+
'Compose, keep, and open personal working views (purely local)';
|
|
14
|
+
const workset = program.command('workset').description(groupDescription);
|
|
15
|
+
// Parsed at the group level so `openspec workset --json` keeps the
|
|
16
|
+
// one-JSON-document contract instead of a raw Commander error. The
|
|
17
|
+
// parent option matches anywhere; actions read optsWithGlobals().
|
|
18
|
+
workset.addOption(new Option('--json', 'Output as JSON').hideHelp());
|
|
19
|
+
workset
|
|
20
|
+
.command('create [name]')
|
|
21
|
+
.description('Compose and save a named working view of folders you choose')
|
|
22
|
+
.option('--member <member>', 'Member folder as <path> or <name>=<path>; repeatable, first is the primary', collectMember, [])
|
|
23
|
+
.option('--tool <id>', 'Preferred tool to open this workset with')
|
|
24
|
+
.option('--json', 'Output as JSON')
|
|
25
|
+
.action(async (name, _options, command) => {
|
|
26
|
+
const worksetCommand = await loadWorksetCommand();
|
|
27
|
+
await worksetCommand.create(name, command.optsWithGlobals());
|
|
28
|
+
});
|
|
29
|
+
workset
|
|
30
|
+
.command('list')
|
|
31
|
+
.alias('ls')
|
|
32
|
+
.description('Show saved worksets with their members')
|
|
33
|
+
.option('--json', 'Output as JSON')
|
|
34
|
+
.action(async (_options, command) => {
|
|
35
|
+
const worksetCommand = await loadWorksetCommand();
|
|
36
|
+
await worksetCommand.list(command.optsWithGlobals());
|
|
37
|
+
});
|
|
38
|
+
workset
|
|
39
|
+
.command('open <name>')
|
|
40
|
+
.description('Open a saved workset in your tool (editor window or agent session)')
|
|
41
|
+
.option('--tool <id>', 'Open with this tool just this once')
|
|
42
|
+
.addOption(
|
|
43
|
+
// Parsed so Commander never owns the error; rejected in the
|
|
44
|
+
// action with one JSON document. Hidden because help should not
|
|
45
|
+
// advertise a mode that only rejects.
|
|
46
|
+
new Option('--json', 'Not supported for open').hideHelp())
|
|
47
|
+
.action(async (name, _options, command) => {
|
|
48
|
+
const worksetCommand = await loadWorksetCommand();
|
|
49
|
+
await worksetCommand.open(name, command.optsWithGlobals());
|
|
50
|
+
});
|
|
51
|
+
workset
|
|
52
|
+
.command('remove <name>')
|
|
53
|
+
.description('Delete a saved workset (member folders are never touched)')
|
|
54
|
+
.option('--yes', 'Confirm removal non-interactively')
|
|
55
|
+
.option('--json', 'Output as JSON')
|
|
56
|
+
.action(async (name, _options, command) => {
|
|
57
|
+
const worksetCommand = await loadWorksetCommand();
|
|
58
|
+
await worksetCommand.remove(name, command.optsWithGlobals());
|
|
59
|
+
});
|
|
60
|
+
const subcommandsLine = workset.commands
|
|
61
|
+
.map((subcommand) => {
|
|
62
|
+
const aliases = subcommand.aliases();
|
|
63
|
+
return aliases.length > 0
|
|
64
|
+
? `${subcommand.name()} (${aliases.join(', ')})`
|
|
65
|
+
: subcommand.name();
|
|
66
|
+
})
|
|
67
|
+
.join(', ');
|
|
68
|
+
// One handler owns missing AND unknown subcommands: known
|
|
69
|
+
// subcommands dispatch above; everything else lands in this action
|
|
70
|
+
// (allowExcessArguments routes the unknown operand here), keeping
|
|
71
|
+
// the one-JSON-document contract for `--json` probes.
|
|
72
|
+
workset.allowExcessArguments(true);
|
|
73
|
+
workset.action(() => {
|
|
74
|
+
const attempted = workset.args.filter((operand) => !operand.startsWith('-'));
|
|
75
|
+
const message = attempted.length > 0
|
|
76
|
+
? `Unknown command '${attempted[0]}' for 'openspec workset'. Workset subcommands: ${subcommandsLine}.`
|
|
77
|
+
: `Missing subcommand for 'openspec workset'. Workset subcommands: ${subcommandsLine}.`;
|
|
78
|
+
if (workset.opts().json) {
|
|
79
|
+
printJson({
|
|
80
|
+
status: [
|
|
81
|
+
{
|
|
82
|
+
severity: 'error',
|
|
83
|
+
code: 'unknown_workset_subcommand',
|
|
84
|
+
message,
|
|
85
|
+
fix: 'Run one of the workset subcommands.',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.error(`Error: ${message}`);
|
|
92
|
+
}
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=workset.js.map
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -27,6 +27,11 @@ export declare function isJsonRun(command: Command): boolean;
|
|
|
27
27
|
* completions is noise, and `__complete` would burn the one-shot tip invisibly.
|
|
28
28
|
*/
|
|
29
29
|
export declare function isCompletionRun(commandPath: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* True for the hidden `__complete` resolver alone. It runs on every Tab press,
|
|
32
|
+
* so the hooks skip telemetry and the completion tip for it entirely.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isTabCompletionRun(commandPath: string): boolean;
|
|
30
35
|
/**
|
|
31
36
|
* True when the first-run completions tip must be deferred rather than shown.
|
|
32
37
|
*
|