@fission-ai/openspec 1.9.0 → 1.11.0
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/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.js +60 -9
- package/dist/commands/change.d.ts +24 -0
- package/dist/commands/change.js +223 -0
- package/dist/commands/config.js +14 -3
- package/dist/commands/feedback.js +28 -7
- package/dist/commands/schema.d.ts +5 -0
- package/dist/commands/schema.js +201 -35
- package/dist/commands/show.js +1 -1
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/status.d.ts +2 -0
- package/dist/commands/workflow/status.js +76 -12
- package/dist/core/archive.js +64 -7
- package/dist/core/artifact-graph/instruction-loader.js +2 -7
- package/dist/core/artifact-graph/outputs.d.ts +4 -0
- package/dist/core/artifact-graph/outputs.js +7 -0
- package/dist/core/artifact-graph/resolver.js +15 -9
- package/dist/core/available-tools.js +16 -1
- package/dist/core/command-generation/adapters/antigravity.d.ts +1 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -2
- package/dist/core/command-generation/adapters/opencode.d.ts +2 -1
- package/dist/core/command-generation/adapters/opencode.js +13 -2
- package/dist/core/completion-tip.d.ts +15 -0
- package/dist/core/completion-tip.js +133 -0
- package/dist/core/completions/command-registry.js +20 -0
- package/dist/core/completions/factory.d.ts +11 -0
- package/dist/core/completions/generators/fish-generator.d.ts +12 -0
- package/dist/core/completions/generators/fish-generator.js +79 -47
- package/dist/core/completions/installers/bash-installer.d.ts +7 -0
- package/dist/core/completions/installers/bash-installer.js +15 -0
- package/dist/core/completions/installers/fish-installer.d.ts +7 -0
- package/dist/core/completions/installers/fish-installer.js +15 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +7 -0
- package/dist/core/completions/installers/powershell-installer.js +15 -0
- package/dist/core/completions/templates/fish-templates.d.ts +1 -1
- package/dist/core/completions/templates/fish-templates.js +107 -10
- package/dist/core/completions/types.d.ts +4 -0
- package/dist/core/config-prompts.js +19 -10
- package/dist/core/config-schema.d.ts +1 -0
- package/dist/core/config-schema.js +2 -0
- package/dist/core/config.js +7 -1
- package/dist/core/global-config.d.ts +2 -0
- package/dist/core/init.d.ts +5 -0
- package/dist/core/init.js +117 -14
- package/dist/core/legacy-cleanup.js +6 -0
- package/dist/core/migration.js +42 -12
- package/dist/core/parsers/requirement-blocks.d.ts +1 -0
- package/dist/core/parsers/requirement-blocks.js +2 -0
- package/dist/core/profiles.d.ts +1 -1
- package/dist/core/profiles.js +11 -2
- package/dist/core/shared/tool-detection.js +9 -3
- package/dist/core/shared-skill-target.d.ts +13 -0
- package/dist/core/shared-skill-target.js +43 -1
- package/dist/core/specs-apply.js +16 -8
- package/dist/core/templates/workflows/explore.js +79 -73
- package/dist/core/templates/workflows/feedback.js +1 -0
- package/dist/core/templates/workflows/onboard.js +4 -4
- package/dist/core/update.js +39 -26
- package/dist/core/validation/constants.d.ts +3 -0
- package/dist/core/validation/constants.js +12 -0
- package/dist/core/validation/purpose-placeholder.d.ts +45 -0
- package/dist/core/validation/purpose-placeholder.js +117 -0
- package/dist/core/validation/validator.js +16 -1
- package/dist/telemetry/index.js +3 -2
- package/dist/utils/change-metadata.js +17 -13
- package/dist/utils/change-utils.js +5 -0
- package/dist/utils/requirement-diff.d.ts +42 -0
- package/dist/utils/requirement-diff.js +61 -0
- package/package.json +4 -6
- package/schemas/spec-driven/schema.yaml +17 -7
- package/scripts/postinstall.js +0 -83
|
@@ -49,6 +49,21 @@ export function getAvailableTools(projectPath) {
|
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
const activeProjectTools = new Set(reconcileSharedSkillTargets(projectPath, available.filter((tool) => tool.skillsDir)).map((tool) => tool.value));
|
|
52
|
-
|
|
52
|
+
const hasIndependentDetectionPath = (tool) => (tool.detectionPaths ?? []).some((detectionPath) => {
|
|
53
|
+
// Skill roots still go through managed-content reconciliation below;
|
|
54
|
+
// their mere existence is not an independent tool signal.
|
|
55
|
+
if (detectionPath.endsWith('/skills'))
|
|
56
|
+
return false;
|
|
57
|
+
try {
|
|
58
|
+
fs.statSync(path.join(projectPath, detectionPath));
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return available.filter((tool) => tool.globalSkillsDir ||
|
|
66
|
+
hasIndependentDetectionPath(tool) ||
|
|
67
|
+
activeProjectTools.has(tool.value));
|
|
53
68
|
}
|
|
54
69
|
//# sourceMappingURL=available-tools.js.map
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { ToolCommandAdapter } from '../types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Antigravity adapter for command generation.
|
|
9
|
-
* File path: .
|
|
9
|
+
* File path: .agents/workflows/opsx-<id>.md
|
|
10
10
|
* Frontmatter: description
|
|
11
11
|
*/
|
|
12
12
|
export declare const antigravityAdapter: ToolCommandAdapter;
|
|
@@ -7,13 +7,13 @@ import path from 'path';
|
|
|
7
7
|
import { escapeYamlValue } from '../yaml.js';
|
|
8
8
|
/**
|
|
9
9
|
* Antigravity adapter for command generation.
|
|
10
|
-
* File path: .
|
|
10
|
+
* File path: .agents/workflows/opsx-<id>.md
|
|
11
11
|
* Frontmatter: description
|
|
12
12
|
*/
|
|
13
13
|
export const antigravityAdapter = {
|
|
14
14
|
toolId: 'antigravity',
|
|
15
15
|
getFilePath(commandId) {
|
|
16
|
-
return path.join('.
|
|
16
|
+
return path.join('.agents', 'workflows', `opsx-${commandId}.md`);
|
|
17
17
|
},
|
|
18
18
|
formatFile(content) {
|
|
19
19
|
return `---
|
|
@@ -7,7 +7,8 @@ import type { ToolCommandAdapter } from '../types.js';
|
|
|
7
7
|
/**
|
|
8
8
|
* OpenCode adapter for command generation.
|
|
9
9
|
* File path: .opencode/commands/opsx-<id>.md
|
|
10
|
-
* Frontmatter: description
|
|
10
|
+
* Frontmatter: description. $ARGUMENTS is injected after the complete input
|
|
11
|
+
* contract because OpenCode only passes arguments through explicit placeholders.
|
|
11
12
|
*/
|
|
12
13
|
export declare const opencodeAdapter: ToolCommandAdapter;
|
|
13
14
|
//# sourceMappingURL=opencode.d.ts.map
|
|
@@ -5,10 +5,21 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { escapeYamlValue } from '../yaml.js';
|
|
8
|
+
const OPENCODE_INPUT_BLOCK = /^\*\*Input\*\*:[^\r\n]*(?:\r?\n(?!\r?\n)[^\r\n]*)*/m;
|
|
9
|
+
const OPENCODE_NO_INPUT = /^\*\*Input\*\*:\s*None required\b/im;
|
|
10
|
+
const OPENCODE_ARGUMENT_PLACEHOLDER = /\$(?:ARGUMENTS\b|[1-9]\d*\b)/;
|
|
11
|
+
function injectOpenCodeArgs(body) {
|
|
12
|
+
if (OPENCODE_ARGUMENT_PLACEHOLDER.test(body) || OPENCODE_NO_INPUT.test(body)) {
|
|
13
|
+
return body;
|
|
14
|
+
}
|
|
15
|
+
const eol = body.includes('\r\n') ? '\r\n' : '\n';
|
|
16
|
+
return body.replace(OPENCODE_INPUT_BLOCK, (input) => `${input}${eol}**Provided arguments**: $ARGUMENTS`);
|
|
17
|
+
}
|
|
8
18
|
/**
|
|
9
19
|
* OpenCode adapter for command generation.
|
|
10
20
|
* File path: .opencode/commands/opsx-<id>.md
|
|
11
|
-
* Frontmatter: description
|
|
21
|
+
* Frontmatter: description. $ARGUMENTS is injected after the complete input
|
|
22
|
+
* contract because OpenCode only passes arguments through explicit placeholders.
|
|
12
23
|
*/
|
|
13
24
|
export const opencodeAdapter = {
|
|
14
25
|
toolId: 'opencode',
|
|
@@ -20,7 +31,7 @@ export const opencodeAdapter = {
|
|
|
20
31
|
description: ${escapeYamlValue(content.description)}
|
|
21
32
|
---
|
|
22
33
|
|
|
23
|
-
${content.body}
|
|
34
|
+
${injectOpenCodeArgs(content.body)}
|
|
24
35
|
`;
|
|
25
36
|
},
|
|
26
37
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const COMPLETION_TIP_MESSAGE = "Tip: Run 'openspec completion install' for shell completions";
|
|
2
|
+
export interface CompletionTipOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Skip printing without marking the tip as seen, so it still appears on the
|
|
5
|
+
* user's first later run that can safely carry it. Used for runs nobody would
|
|
6
|
+
* read the tip from: JSON output, and stderr that is not a terminal.
|
|
7
|
+
*/
|
|
8
|
+
silent?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Print the completion tip once, the first time the CLI runs.
|
|
12
|
+
* Never throws — a hint must not break a command.
|
|
13
|
+
*/
|
|
14
|
+
export declare function maybeShowCompletionTip(options?: CompletionTipOptions): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=completion-tip.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-run hint pointing users at opt-in shell completions.
|
|
3
|
+
*
|
|
4
|
+
* This hint used to be an npm `postinstall` script. Printing it from the CLI
|
|
5
|
+
* instead lets the package ship with no install scripts at all, so `npm install`
|
|
6
|
+
* no longer emits an `allow-scripts` warning. Completions stay opt-in: the tip
|
|
7
|
+
* only names the command, it never installs anything.
|
|
8
|
+
*
|
|
9
|
+
* The tip goes to stderr, never stdout, so it cannot contaminate piped command
|
|
10
|
+
* output.
|
|
11
|
+
*
|
|
12
|
+
* The tip is suppressed when:
|
|
13
|
+
* - CI is set (any value npm/telemetry would treat as CI)
|
|
14
|
+
* - OPENSPEC_NO_COMPLETIONS=1
|
|
15
|
+
* - completions are already installed, or the shell is one the installer would
|
|
16
|
+
* reject
|
|
17
|
+
* - the caller passes `silent` — JSON runs, `openspec completion ...`, and
|
|
18
|
+
* non-TTY runs, which are deferred rather than consumed (see `silent`)
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import { getGlobalConfigPath } from './global-config.js';
|
|
23
|
+
import { isCiEnvironment } from '../utils/ci.js';
|
|
24
|
+
import { detectShell } from '../utils/shell-detection.js';
|
|
25
|
+
import { CompletionFactory } from './completions/factory.js';
|
|
26
|
+
export const COMPLETION_TIP_MESSAGE = "Tip: Run 'openspec completion install' for shell completions";
|
|
27
|
+
function isSuppressedByEnv() {
|
|
28
|
+
// isCiEnvironment, not a CI==='true' string check: providers set CI to "True",
|
|
29
|
+
// "yes", "on", and the tip should be as quiet in those builds as telemetry is.
|
|
30
|
+
return isCiEnvironment() || process.env.OPENSPEC_NO_COMPLETIONS === '1';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether the tip is worth showing, once we know it is owed and readable.
|
|
34
|
+
*
|
|
35
|
+
* "retire" consumes the tip without printing: the user either already has
|
|
36
|
+
* completions, or is on a shell `openspec completion install` would refuse.
|
|
37
|
+
*
|
|
38
|
+
* Without the installed check the tip tells people to install completions they
|
|
39
|
+
* installed long ago — including on the run right after `completion install`,
|
|
40
|
+
* whose own run only defers the tip. An undetected or unsupported shell retires
|
|
41
|
+
* it too: `completion install` exits 1 for those users, so pointing them at it
|
|
42
|
+
* is a dead end, and this tip is the only message about completions they would
|
|
43
|
+
* ever get.
|
|
44
|
+
*
|
|
45
|
+
* Not free: detectShell() forks `ps` to read the parent process (except on
|
|
46
|
+
* Windows), so this costs a spawn plus a stat. It runs only on interactive runs
|
|
47
|
+
* that still owe the tip, which is normally exactly one — but a config that
|
|
48
|
+
* cannot be written never records the flag, and then every interactive run pays
|
|
49
|
+
* it. On any unexpected error we show the tip rather than swallow it.
|
|
50
|
+
*/
|
|
51
|
+
async function decideTip() {
|
|
52
|
+
try {
|
|
53
|
+
const { shell } = detectShell();
|
|
54
|
+
if (!shell) {
|
|
55
|
+
return 'retire';
|
|
56
|
+
}
|
|
57
|
+
return (await CompletionFactory.createInstaller(shell).isInstalled())
|
|
58
|
+
? 'retire'
|
|
59
|
+
: 'show';
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return 'show';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Read the global config exactly as it sits on disk.
|
|
67
|
+
*
|
|
68
|
+
* Deliberately NOT `getGlobalConfig()`: that merges in defaults, and writing the
|
|
69
|
+
* merged result back would stamp `profile`/`delivery` into a file the user never
|
|
70
|
+
* set them in. `migrateIfNeeded` treats a raw `profile` as "already migrated",
|
|
71
|
+
* so that stamp would permanently suppress the one-time profile migration and
|
|
72
|
+
* cost users their installed workflow skills.
|
|
73
|
+
*
|
|
74
|
+
* Returns null when the file exists but cannot be read or parsed — a config we
|
|
75
|
+
* cannot understand is left strictly alone rather than overwritten.
|
|
76
|
+
*/
|
|
77
|
+
function readRawConfig() {
|
|
78
|
+
const configPath = getGlobalConfigPath();
|
|
79
|
+
if (!fs.existsSync(configPath)) {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
83
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Record the flag, re-reading the config first and replacing the file by rename.
|
|
90
|
+
*
|
|
91
|
+
* Deciding whether to show the tip costs a `ps` spawn and a stat, and a sibling
|
|
92
|
+
* `openspec` process can write the same file in that window — on a first run
|
|
93
|
+
* that is exactly when telemetry mints `anonymousId`. Re-reading here keeps the
|
|
94
|
+
* write down to this one key, and the rename keeps a reader from ever seeing a
|
|
95
|
+
* half-written config.
|
|
96
|
+
*/
|
|
97
|
+
function markTipSeen() {
|
|
98
|
+
const configPath = getGlobalConfigPath();
|
|
99
|
+
const current = readRawConfig() ?? {};
|
|
100
|
+
const tempPath = `${configPath}.${process.pid}.tmp`;
|
|
101
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
102
|
+
fs.writeFileSync(tempPath, JSON.stringify({ ...current, completionTipSeen: true }, null, 2) + '\n', 'utf-8');
|
|
103
|
+
fs.renameSync(tempPath, configPath);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Print the completion tip once, the first time the CLI runs.
|
|
107
|
+
* Never throws — a hint must not break a command.
|
|
108
|
+
*/
|
|
109
|
+
export async function maybeShowCompletionTip(options = {}) {
|
|
110
|
+
if (isSuppressedByEnv()) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const raw = readRawConfig();
|
|
115
|
+
if (raw === null || raw.completionTipSeen === true) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (options.silent) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const decision = await decideTip();
|
|
122
|
+
// Record before printing: if the flag cannot be persisted, staying quiet
|
|
123
|
+
// beats reprinting the tip on every future run.
|
|
124
|
+
markTipSeen();
|
|
125
|
+
if (decision === 'show') {
|
|
126
|
+
console.error(`\n${COMPLETION_TIP_MESSAGE}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Silent failure - a hint should never break the CLI.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=completion-tip.js.map
|
|
@@ -12,6 +12,11 @@ export const COMMAND_REGISTRY = [
|
|
|
12
12
|
description: 'Configure AI tools non-interactively (e.g., "all", "none", or comma-separated tool IDs)',
|
|
13
13
|
takesValue: true,
|
|
14
14
|
},
|
|
15
|
+
{
|
|
16
|
+
name: 'language',
|
|
17
|
+
description: 'Write new OpenSpec artifacts in this language',
|
|
18
|
+
takesValue: true,
|
|
19
|
+
},
|
|
15
20
|
{
|
|
16
21
|
name: 'force',
|
|
17
22
|
description: 'Auto-cleanup legacy files without prompting',
|
|
@@ -131,6 +136,10 @@ export const COMMAND_REGISTRY = [
|
|
|
131
136
|
name: 'requirements-only',
|
|
132
137
|
description: 'Alias for --deltas-only (deprecated, change-specific)',
|
|
133
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: 'diff',
|
|
141
|
+
description: 'Show per-requirement diffs for delta specs (change-specific)',
|
|
142
|
+
},
|
|
134
143
|
{
|
|
135
144
|
name: 'requirements',
|
|
136
145
|
description: 'Show only requirements, exclude scenarios (JSON only, spec-specific)',
|
|
@@ -184,6 +193,10 @@ export const COMMAND_REGISTRY = [
|
|
|
184
193
|
description: 'Change name to show status for',
|
|
185
194
|
takesValue: true,
|
|
186
195
|
},
|
|
196
|
+
{
|
|
197
|
+
name: 'all',
|
|
198
|
+
description: 'Show status for all active changes',
|
|
199
|
+
},
|
|
187
200
|
{
|
|
188
201
|
name: 'schema',
|
|
189
202
|
description: 'Schema override',
|
|
@@ -280,6 +293,7 @@ export const COMMAND_REGISTRY = [
|
|
|
280
293
|
name: 'path',
|
|
281
294
|
description: 'Directory to use for the store',
|
|
282
295
|
takesValue: true,
|
|
296
|
+
completionType: 'path',
|
|
283
297
|
},
|
|
284
298
|
{
|
|
285
299
|
name: 'init-git',
|
|
@@ -372,6 +386,7 @@ export const COMMAND_REGISTRY = [
|
|
|
372
386
|
name: 'code-workspace',
|
|
373
387
|
description: 'Also write a VS Code workspace file for the set',
|
|
374
388
|
takesValue: true,
|
|
389
|
+
completionType: 'path',
|
|
375
390
|
},
|
|
376
391
|
{
|
|
377
392
|
name: 'force',
|
|
@@ -402,6 +417,7 @@ export const COMMAND_REGISTRY = [
|
|
|
402
417
|
name: 'member',
|
|
403
418
|
description: 'Member folder as <path> or <name>=<path>; repeatable, first is the primary',
|
|
404
419
|
takesValue: true,
|
|
420
|
+
completionType: 'path',
|
|
405
421
|
},
|
|
406
422
|
{
|
|
407
423
|
name: 'tool',
|
|
@@ -483,6 +499,10 @@ export const COMMAND_REGISTRY = [
|
|
|
483
499
|
name: 'requirements-only',
|
|
484
500
|
description: 'Alias for --deltas-only (deprecated)',
|
|
485
501
|
},
|
|
502
|
+
{
|
|
503
|
+
name: 'diff',
|
|
504
|
+
description: 'Show per-requirement diffs for delta specs',
|
|
505
|
+
},
|
|
486
506
|
COMMON_FLAGS.noInteractive,
|
|
487
507
|
],
|
|
488
508
|
},
|
|
@@ -24,6 +24,17 @@ export interface CompletionInstaller {
|
|
|
24
24
|
success: boolean;
|
|
25
25
|
message: string;
|
|
26
26
|
}>;
|
|
27
|
+
/**
|
|
28
|
+
* True when a completion script file is present at the install path.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately just the script: bash and PowerShell also need a sourcing
|
|
31
|
+
* line in the user's profile, and `install()` adds that on a best-effort
|
|
32
|
+
* basis (it is skipped by OPENSPEC_NO_AUTO_CONFIG=1 or an unwritable
|
|
33
|
+
* profile, printing manual instructions instead). Someone in that state has
|
|
34
|
+
* already met the installer, so callers that use this to decide whether to
|
|
35
|
+
* *advertise* completions should not advertise again.
|
|
36
|
+
*/
|
|
37
|
+
isInstalled(): Promise<boolean>;
|
|
27
38
|
}
|
|
28
39
|
/**
|
|
29
40
|
* Factory for creating completion generators and installers
|
|
@@ -24,6 +24,18 @@ export declare class FishGenerator implements CompletionGenerator {
|
|
|
24
24
|
* Generate positional argument completion
|
|
25
25
|
*/
|
|
26
26
|
private generatePositionalCompletion;
|
|
27
|
+
/**
|
|
28
|
+
* Generate indexed positional completions.
|
|
29
|
+
*/
|
|
30
|
+
private generateIndexedPositionalCompletions;
|
|
31
|
+
/**
|
|
32
|
+
* Collect the long and short names for flags that consume the next token.
|
|
33
|
+
*/
|
|
34
|
+
private collectValueFlags;
|
|
35
|
+
/**
|
|
36
|
+
* Build a Fish condition that matches command words in their actual slots.
|
|
37
|
+
*/
|
|
38
|
+
private commandPathCondition;
|
|
27
39
|
/**
|
|
28
40
|
* Escape description text for Fish
|
|
29
41
|
*/
|
|
@@ -12,30 +12,26 @@ export class FishGenerator {
|
|
|
12
12
|
* @returns Fish completion script as a string
|
|
13
13
|
*/
|
|
14
14
|
generate(commands) {
|
|
15
|
-
// Build top-level commands using push() for loop clarity
|
|
16
15
|
const topLevelLines = [];
|
|
17
16
|
for (const cmd of commands) {
|
|
18
17
|
topLevelLines.push(`# ${cmd.name} command`);
|
|
19
|
-
topLevelLines.push(`complete -c openspec -n '__fish_openspec_no_subcommand' -a '${cmd.name}' -d '${this.escapeDescription(cmd.description)}'`);
|
|
18
|
+
topLevelLines.push(`complete -c openspec -n '__fish_openspec_no_subcommand' -f -a '${cmd.name}' -d '${this.escapeDescription(cmd.description)}'`);
|
|
20
19
|
}
|
|
21
20
|
const topLevelCommands = topLevelLines.join('\n');
|
|
22
|
-
// Build command-specific completions using push() for loop clarity
|
|
23
21
|
const commandCompletionLines = [];
|
|
24
22
|
for (const cmd of commands) {
|
|
25
23
|
commandCompletionLines.push(...this.generateCommandCompletions(cmd));
|
|
26
24
|
commandCompletionLines.push('');
|
|
27
25
|
}
|
|
28
26
|
const commandCompletions = commandCompletionLines.join('\n');
|
|
29
|
-
// Static helper functions from template
|
|
30
27
|
const helperFunctions = FISH_STATIC_HELPERS;
|
|
31
|
-
// Dynamic completion helpers from template
|
|
32
28
|
const dynamicHelpers = FISH_DYNAMIC_HELPERS;
|
|
33
|
-
// Assemble final script with template literal
|
|
34
29
|
return `# Fish completion script for OpenSpec CLI
|
|
35
30
|
# Auto-generated - do not edit manually
|
|
36
31
|
|
|
37
32
|
${helperFunctions}
|
|
38
33
|
${dynamicHelpers}
|
|
34
|
+
complete -c openspec -l no-color -f -d 'Disable color output'
|
|
39
35
|
${topLevelCommands}
|
|
40
36
|
|
|
41
37
|
${commandCompletions}`;
|
|
@@ -45,38 +41,45 @@ ${commandCompletions}`;
|
|
|
45
41
|
*/
|
|
46
42
|
generateCommandCompletions(cmd) {
|
|
47
43
|
const lines = [];
|
|
48
|
-
// If command has subcommands
|
|
49
44
|
if (cmd.subcommands && cmd.subcommands.length > 0) {
|
|
50
|
-
|
|
45
|
+
const commandCondition = this.commandPathCondition([cmd.name]);
|
|
46
|
+
const parentValueFlags = this.collectValueFlags(cmd.flags);
|
|
47
|
+
const noSubcommandCondition = cmd.subcommands
|
|
48
|
+
.map((subcmd) => `not ${this.commandPathCondition([cmd.name, subcmd.name], parentValueFlags)}`)
|
|
49
|
+
.join('; and ');
|
|
51
50
|
for (const subcmd of cmd.subcommands) {
|
|
52
|
-
lines.push(`complete -c openspec -n '
|
|
51
|
+
lines.push(`complete -c openspec -n '${commandCondition}; and ${noSubcommandCondition}' -f -a '${subcmd.name}' -d '${this.escapeDescription(subcmd.description)}'`);
|
|
53
52
|
}
|
|
54
53
|
lines.push('');
|
|
55
|
-
// Add flags for parent command
|
|
56
54
|
for (const flag of cmd.flags) {
|
|
57
|
-
lines.push(...this.generateFlagCompletion(flag,
|
|
55
|
+
lines.push(...this.generateFlagCompletion(flag, commandCondition));
|
|
58
56
|
}
|
|
59
|
-
// Add completions for each subcommand
|
|
60
57
|
for (const subcmd of cmd.subcommands) {
|
|
58
|
+
const subcommandCondition = this.commandPathCondition([cmd.name, subcmd.name], parentValueFlags);
|
|
61
59
|
lines.push(`# ${cmd.name} ${subcmd.name} flags`);
|
|
60
|
+
lines.push(`complete -c openspec -n '${subcommandCondition}' -f`);
|
|
62
61
|
for (const flag of subcmd.flags) {
|
|
63
|
-
lines.push(...this.generateFlagCompletion(flag,
|
|
62
|
+
lines.push(...this.generateFlagCompletion(flag, subcommandCondition));
|
|
64
63
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
if (subcmd.positionals?.length) {
|
|
65
|
+
lines.push(...this.generateIndexedPositionalCompletions(subcmd.positionals, subcommandCondition, this.collectValueFlags(cmd.flags, subcmd.flags), 2));
|
|
66
|
+
}
|
|
67
|
+
else if (subcmd.acceptsPositional) {
|
|
68
|
+
lines.push(...this.generatePositionalCompletion(subcmd.positionalType, subcommandCondition));
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
else {
|
|
72
|
-
// Command without subcommands
|
|
73
73
|
lines.push(`# ${cmd.name} flags`);
|
|
74
|
+
lines.push(`complete -c openspec -n '__fish_openspec_using_command_path ${cmd.name}' -f`);
|
|
74
75
|
for (const flag of cmd.flags) {
|
|
75
|
-
lines.push(...this.generateFlagCompletion(flag, `
|
|
76
|
+
lines.push(...this.generateFlagCompletion(flag, `__fish_openspec_using_command_path ${cmd.name}`));
|
|
77
|
+
}
|
|
78
|
+
if (cmd.positionals?.length) {
|
|
79
|
+
lines.push(...this.generateIndexedPositionalCompletions(cmd.positionals, `__fish_openspec_using_command_path ${cmd.name}`, this.collectValueFlags(cmd.flags), 1));
|
|
76
80
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
lines.push(...this.generatePositionalCompletion(cmd.positionalType, `__fish_openspec_using_subcommand ${cmd.name}`));
|
|
81
|
+
else if (cmd.acceptsPositional) {
|
|
82
|
+
lines.push(...this.generatePositionalCompletion(cmd.positionalType, `__fish_openspec_using_command_path ${cmd.name}`));
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
return lines;
|
|
@@ -86,36 +89,26 @@ ${commandCompletions}`;
|
|
|
86
89
|
*/
|
|
87
90
|
generateFlagCompletion(flag, condition) {
|
|
88
91
|
const lines = [];
|
|
89
|
-
const
|
|
90
|
-
const shortFlag = flag.short ?
|
|
92
|
+
const description = this.escapeDescription(flag.description);
|
|
93
|
+
const shortFlag = flag.short ? `-s ${flag.short} ` : '';
|
|
94
|
+
const flagOptions = `${shortFlag}-l ${flag.name}`;
|
|
91
95
|
if (flag.takesValue && flag.values) {
|
|
92
|
-
// Flag with enum values
|
|
93
96
|
for (const value of flag.values) {
|
|
94
|
-
|
|
95
|
-
lines.push(`complete -c openspec -n '${condition}' -s ${flag.short} -l ${flag.name} -a '${value}' -d '${this.escapeDescription(flag.description)}'`);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
lines.push(`complete -c openspec -n '${condition}' -l ${flag.name} -a '${value}' -d '${this.escapeDescription(flag.description)}'`);
|
|
99
|
-
}
|
|
97
|
+
lines.push(`complete -c openspec -n '${condition}' ${flagOptions} -r -f -a '${value}' -d '${description}'`);
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
100
|
else if (flag.takesValue) {
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
lines.push(`complete -c openspec -n '${condition}' ${flagOptions} -r -f -d '${description}'`);
|
|
102
|
+
if (flag.completionType === 'path') {
|
|
103
|
+
const optionNames = [`--${flag.name}`, ...(flag.short ? [`-${flag.short}`] : [])];
|
|
104
|
+
lines.push(`complete -c openspec -n '${condition}; and __fish_openspec_completing_option_value ${optionNames.join(' ')}' ${flagOptions} -r -F -d '${description}'`);
|
|
105
|
+
if (flag.short) {
|
|
106
|
+
lines.push(`complete -c openspec -n '${condition}' ${flagOptions} -r -f -a '(__fish_openspec_complete_attached_short_path -${flag.short})' -d '${description}'`);
|
|
107
|
+
}
|
|
109
108
|
}
|
|
110
109
|
}
|
|
111
110
|
else {
|
|
112
|
-
|
|
113
|
-
if (shortFlag) {
|
|
114
|
-
lines.push(`complete -c openspec -n '${condition}' -s ${flag.short} -l ${flag.name} -d '${this.escapeDescription(flag.description)}'`);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
lines.push(`complete -c openspec -n '${condition}' -l ${flag.name} -d '${this.escapeDescription(flag.description)}'`);
|
|
118
|
-
}
|
|
111
|
+
lines.push(`complete -c openspec -n '${condition}' ${flagOptions} -f -d '${description}'`);
|
|
119
112
|
}
|
|
120
113
|
return lines;
|
|
121
114
|
}
|
|
@@ -141,20 +134,59 @@ ${commandCompletions}`;
|
|
|
141
134
|
lines.push(`complete -c openspec -n '${condition}' -a 'zsh bash fish powershell' -f`);
|
|
142
135
|
break;
|
|
143
136
|
case 'path':
|
|
144
|
-
//
|
|
137
|
+
// -F re-enables filesystem completion: sibling rules in the same
|
|
138
|
+
// context carry -f, and Fish never restores files without --force-files.
|
|
139
|
+
lines.push(`complete -c openspec -n '${condition}' -F`);
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
lines.push(`complete -c openspec -n '${condition}' -f`);
|
|
145
143
|
break;
|
|
146
144
|
}
|
|
147
145
|
return lines;
|
|
148
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Generate indexed positional completions.
|
|
149
|
+
*/
|
|
150
|
+
generateIndexedPositionalCompletions(positionals, condition, valueFlags, depth) {
|
|
151
|
+
const lines = [];
|
|
152
|
+
for (const [index, positional] of positionals.entries()) {
|
|
153
|
+
const indexCondition = `${condition}; and __fish_openspec_positional_index ${index} ${depth}${valueFlags.length ? ` ${valueFlags.join(' ')}` : ''}`;
|
|
154
|
+
lines.push(...this.generatePositionalCompletion(positional.type, indexCondition));
|
|
155
|
+
}
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Collect the long and short names for flags that consume the next token.
|
|
160
|
+
*/
|
|
161
|
+
collectValueFlags(...flagGroups) {
|
|
162
|
+
const flags = new Set();
|
|
163
|
+
for (const group of flagGroups) {
|
|
164
|
+
for (const flag of group) {
|
|
165
|
+
if (!flag.takesValue) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
flags.add(`--${flag.name}`);
|
|
169
|
+
if (flag.short) {
|
|
170
|
+
flags.add(`-${flag.short}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return [...flags];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Build a Fish condition that matches command words in their actual slots.
|
|
178
|
+
*/
|
|
179
|
+
commandPathCondition(path, valueFlags = []) {
|
|
180
|
+
const valueFlagArguments = valueFlags.length ? ` -- ${valueFlags.join(' ')}` : '';
|
|
181
|
+
return `__fish_openspec_using_command_path ${path.join(' ')}${valueFlagArguments}`;
|
|
182
|
+
}
|
|
149
183
|
/**
|
|
150
184
|
* Escape description text for Fish
|
|
151
185
|
*/
|
|
152
186
|
escapeDescription(description) {
|
|
153
187
|
return description
|
|
154
188
|
.replace(/\\/g, '\\\\') // Backslashes first
|
|
155
|
-
.replace(/'/g, "\\'") // Single quotes
|
|
156
|
-
.replace(/\$/g, '\\$') // Dollar signs (prevents $())
|
|
157
|
-
.replace(/`/g, '\\`'); // Backticks
|
|
189
|
+
.replace(/'/g, "\\'"); // Single quotes
|
|
158
190
|
}
|
|
159
191
|
}
|
|
160
192
|
//# sourceMappingURL=fish-generator.js.map
|
|
@@ -22,6 +22,13 @@ export declare class BashInstaller {
|
|
|
22
22
|
* @returns Installation path
|
|
23
23
|
*/
|
|
24
24
|
getInstallationPath(): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a completion script is currently installed.
|
|
27
|
+
* Mirrors ZshInstaller.isInstalled so callers can ask any installer.
|
|
28
|
+
*
|
|
29
|
+
* @returns true if the completion script exists
|
|
30
|
+
*/
|
|
31
|
+
isInstalled(): Promise<boolean>;
|
|
25
32
|
/**
|
|
26
33
|
* Backup an existing completion file if it exists
|
|
27
34
|
*
|
|
@@ -55,6 +55,21 @@ export class BashInstaller {
|
|
|
55
55
|
// For user installation, use local directory
|
|
56
56
|
return path.join(localCompletionDir, 'openspec');
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if a completion script is currently installed.
|
|
60
|
+
* Mirrors ZshInstaller.isInstalled so callers can ask any installer.
|
|
61
|
+
*
|
|
62
|
+
* @returns true if the completion script exists
|
|
63
|
+
*/
|
|
64
|
+
async isInstalled() {
|
|
65
|
+
try {
|
|
66
|
+
// stat, not access: a directory at the install path is not a script.
|
|
67
|
+
return (await fs.stat(await this.getInstallationPath())).isFile();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
58
73
|
/**
|
|
59
74
|
* Backup an existing completion file if it exists
|
|
60
75
|
*
|
|
@@ -12,6 +12,13 @@ export declare class FishInstaller {
|
|
|
12
12
|
* @returns Installation path
|
|
13
13
|
*/
|
|
14
14
|
getInstallationPath(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Check if a completion script is currently installed.
|
|
17
|
+
* Mirrors ZshInstaller.isInstalled so callers can ask any installer.
|
|
18
|
+
*
|
|
19
|
+
* @returns true if the completion script exists
|
|
20
|
+
*/
|
|
21
|
+
isInstalled(): Promise<boolean>;
|
|
15
22
|
/**
|
|
16
23
|
* Backup an existing completion file if it exists
|
|
17
24
|
*
|
|
@@ -19,6 +19,21 @@ export class FishInstaller {
|
|
|
19
19
|
getInstallationPath() {
|
|
20
20
|
return path.join(this.homeDir, '.config', 'fish', 'completions', 'openspec.fish');
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if a completion script is currently installed.
|
|
24
|
+
* Mirrors ZshInstaller.isInstalled so callers can ask any installer.
|
|
25
|
+
*
|
|
26
|
+
* @returns true if the completion script exists
|
|
27
|
+
*/
|
|
28
|
+
async isInstalled() {
|
|
29
|
+
try {
|
|
30
|
+
// stat, not access: a directory at the install path is not a script.
|
|
31
|
+
return (await fs.stat(this.getInstallationPath())).isFile();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
22
37
|
/**
|
|
23
38
|
* Backup an existing completion file if it exists
|
|
24
39
|
*
|
|
@@ -43,6 +43,13 @@ export declare class PowerShellInstaller {
|
|
|
43
43
|
* @returns Installation path
|
|
44
44
|
*/
|
|
45
45
|
getInstallationPath(): string;
|
|
46
|
+
/**
|
|
47
|
+
* Check if a completion script is currently installed.
|
|
48
|
+
* Mirrors ZshInstaller.isInstalled so callers can ask any installer.
|
|
49
|
+
*
|
|
50
|
+
* @returns true if the completion script exists
|
|
51
|
+
*/
|
|
52
|
+
isInstalled(): Promise<boolean>;
|
|
46
53
|
/**
|
|
47
54
|
* Backup an existing completion file if it exists
|
|
48
55
|
*
|