@fission-ai/openspec 1.10.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.js +11 -2
- package/dist/commands/change.d.ts +24 -0
- package/dist/commands/change.js +223 -0
- 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/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/completions/command-registry.js +15 -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/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.js +6 -1
- package/dist/core/init.js +50 -31
- 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/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/update.js +24 -22
- 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/utils/requirement-diff.d.ts +42 -0
- package/dist/utils/requirement-diff.js +61 -0
- package/package.json +2 -1
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
import { type ChangeStatus } from '../../core/artifact-graph/index.js';
|
|
7
7
|
export interface StatusOptions {
|
|
8
8
|
change?: string;
|
|
9
|
+
all?: boolean;
|
|
9
10
|
schema?: string;
|
|
10
11
|
store?: string;
|
|
11
12
|
storePath?: string;
|
|
12
13
|
json?: boolean;
|
|
13
14
|
}
|
|
15
|
+
export declare const BATCH_STATUS_FAILURE_PAYLOAD: Record<string, unknown>;
|
|
14
16
|
export declare function statusCommand(options: StatusOptions): Promise<void>;
|
|
15
17
|
export declare function printStatusText(status: ChangeStatus): void;
|
|
16
18
|
//# sourceMappingURL=status.d.ts.map
|
|
@@ -8,14 +8,26 @@ import chalk from 'chalk';
|
|
|
8
8
|
import { getChangeDir } from '../../core/planning-home.js';
|
|
9
9
|
import { resolveRootForCommand, toPlanningHome, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../../core/root-selection.js';
|
|
10
10
|
import { loadChangeContext, formatChangeStatus, } from '../../core/artifact-graph/index.js';
|
|
11
|
+
import { asStatus } from '../shared-output.js';
|
|
11
12
|
import { validateChangeExists, validateSchemaExists, getAvailableChanges, getStatusIndicator, getStatusColor, } from './shared.js';
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// The --all --json failure null-shape. Root-selection failures (handled in
|
|
14
|
+
// resolveRootForCommand) and thrown errors (caught by the CLI wrapper) must
|
|
15
|
+
// emit the same shape, so both call sites reference this one constant.
|
|
16
|
+
export const BATCH_STATUS_FAILURE_PAYLOAD = {
|
|
17
|
+
changes: [],
|
|
18
|
+
root: null,
|
|
19
|
+
};
|
|
15
20
|
export async function statusCommand(options) {
|
|
21
|
+
if (options.all && options.change) {
|
|
22
|
+
throw new Error('The --all and --change options are mutually exclusive.');
|
|
23
|
+
}
|
|
16
24
|
// The root resolves (and the store banner prints) before the spinner starts
|
|
17
|
-
// so the two do not fight over stderr.
|
|
18
|
-
|
|
25
|
+
// so the two do not fight over stderr. The batch null-shape rides along so
|
|
26
|
+
// a root-selection failure under --all --json still carries `changes: []`.
|
|
27
|
+
const root = await resolveRootForCommand(options, {
|
|
28
|
+
json: options.json,
|
|
29
|
+
failurePayload: options.all ? BATCH_STATUS_FAILURE_PAYLOAD : undefined,
|
|
30
|
+
});
|
|
19
31
|
if (!root) {
|
|
20
32
|
return;
|
|
21
33
|
}
|
|
@@ -25,9 +37,20 @@ export async function statusCommand(options) {
|
|
|
25
37
|
const projectRoot = root.path;
|
|
26
38
|
const rootOutput = toRootOutput(root);
|
|
27
39
|
const newChangeHint = withStoreFlag(root, 'openspec new change <name>');
|
|
40
|
+
// Single definition of "load one change's status" so the batch and
|
|
41
|
+
// single-change payloads can never drift apart.
|
|
42
|
+
const loadStatus = (changeName) => formatChangeStatus(loadChangeContext(projectRoot, changeName, options.schema, {
|
|
43
|
+
changeDir: getChangeDir(planningHome, changeName),
|
|
44
|
+
planningHome,
|
|
45
|
+
}), isStoreSelectedRoot(root) ? { storeId: root.storeId } : {});
|
|
28
46
|
// Handle no-changes case gracefully — status is informational,
|
|
29
47
|
// so "no changes" is a valid state, not an error.
|
|
30
48
|
if (!options.change) {
|
|
49
|
+
// Validate before the no-changes early return so a bogus --schema
|
|
50
|
+
// fails the same way whether or not any change exists yet.
|
|
51
|
+
if (options.all && options.schema) {
|
|
52
|
+
validateSchemaExists(options.schema, projectRoot);
|
|
53
|
+
}
|
|
31
54
|
const available = await getAvailableChanges(projectRoot, root.changesDir);
|
|
32
55
|
if (available.length === 0) {
|
|
33
56
|
spinner?.stop();
|
|
@@ -38,9 +61,54 @@ export async function statusCommand(options) {
|
|
|
38
61
|
console.log(`No active changes. Create one with: ${newChangeHint}`);
|
|
39
62
|
return;
|
|
40
63
|
}
|
|
41
|
-
|
|
64
|
+
if (options.all) {
|
|
65
|
+
// readdir order is platform-dependent; sort for deterministic output,
|
|
66
|
+
// with the same comparator validate --all uses so the two batch
|
|
67
|
+
// commands order a given change set identically.
|
|
68
|
+
const entries = [];
|
|
69
|
+
for (const changeName of available.sort((a, b) => a.localeCompare(b))) {
|
|
70
|
+
try {
|
|
71
|
+
entries.push(loadStatus(changeName));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// One malformed change must not blank the sweep; carry its
|
|
75
|
+
// diagnostic in place and keep going.
|
|
76
|
+
entries.push({ changeName, status: [asStatus(error, 'change_error')] });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
spinner?.stop();
|
|
80
|
+
const failed = entries.some((entry) => !('artifacts' in entry));
|
|
81
|
+
if (options.json) {
|
|
82
|
+
console.log(JSON.stringify({ changes: entries, root: rootOutput }, null, 2));
|
|
83
|
+
if (failed) {
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
entries.forEach((entry, index) => {
|
|
89
|
+
if (index > 0) {
|
|
90
|
+
console.log();
|
|
91
|
+
}
|
|
92
|
+
if ('artifacts' in entry) {
|
|
93
|
+
printStatusText(entry);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(chalk.red(`✗ ${entry.changeName}: ${entry.status[0]?.message}`));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// A partial load is still a failed command in both output modes;
|
|
100
|
+
// JSON callers can parse the complete envelope independently of
|
|
101
|
+
// the process exit code.
|
|
102
|
+
if (failed) {
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Changes exist but neither --change nor --all provided. Name --all
|
|
108
|
+
// here too: it is the other way to answer this prompt, and a caller
|
|
109
|
+
// who wants every change should not have to find it in --help.
|
|
42
110
|
spinner?.stop();
|
|
43
|
-
throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
|
|
111
|
+
throw new Error(`Missing required option --change (or --all for every active change). Available changes:\n ${available.join('\n ')}`);
|
|
44
112
|
}
|
|
45
113
|
const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint });
|
|
46
114
|
// Validate schema if explicitly provided
|
|
@@ -48,11 +116,7 @@ export async function statusCommand(options) {
|
|
|
48
116
|
validateSchemaExists(options.schema, projectRoot);
|
|
49
117
|
}
|
|
50
118
|
// loadChangeContext will auto-detect schema from metadata if not provided
|
|
51
|
-
const
|
|
52
|
-
changeDir: getChangeDir(planningHome, changeName),
|
|
53
|
-
planningHome,
|
|
54
|
-
});
|
|
55
|
-
const status = formatChangeStatus(context, isStoreSelectedRoot(root) ? { storeId: root.storeId } : {});
|
|
119
|
+
const status = loadStatus(changeName);
|
|
56
120
|
spinner?.stop();
|
|
57
121
|
if (options.json) {
|
|
58
122
|
console.log(JSON.stringify({ ...status, root: rootOutput }, null, 2));
|
|
@@ -53,18 +53,24 @@ export function getProjectSchemasDir(projectRoot) {
|
|
|
53
53
|
* @param entry - The directory entry from `fs.readdirSync(..., { withFileTypes: true })`
|
|
54
54
|
*/
|
|
55
55
|
/**
|
|
56
|
-
* Directories `schema fork`
|
|
57
|
-
* place: a staging copy (`.fork-staging-<rand
|
|
58
|
-
* backup of the previous
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
56
|
+
* Directories `schema fork` and `schema init` create transiently while swapping
|
|
57
|
+
* a schema into place: a staging copy (`.fork-staging-<rand>` /
|
|
58
|
+
* `.init-staging-<rand>`, created via mkdtemp) and a backup of the previous
|
|
59
|
+
* destination (`<name>.fork-backup-<pid>-<ts>` /
|
|
60
|
+
* `<name>.init-backup-<pid>-<ts>`). Either can briefly coexist with real
|
|
61
|
+
* schemas in the schemas dir, and a backup outlives the run when its cleanup is
|
|
62
|
+
* blocked, so discovery must never surface them. Real schema names are
|
|
63
|
+
* kebab-case (no dots), so excluding these dot-bearing temp names can never
|
|
64
|
+
* hide a legitimate schema.
|
|
62
65
|
*/
|
|
63
|
-
function
|
|
64
|
-
return name.startsWith('.fork-staging-') ||
|
|
66
|
+
function isOwnedTransientSchemaDir(name) {
|
|
67
|
+
return (name.startsWith('.fork-staging-') ||
|
|
68
|
+
name.includes('.fork-backup-') ||
|
|
69
|
+
name.startsWith('.init-staging-') ||
|
|
70
|
+
name.includes('.init-backup-'));
|
|
65
71
|
}
|
|
66
72
|
export function isSchemaDir(parentDir, entry) {
|
|
67
|
-
if (
|
|
73
|
+
if (isOwnedTransientSchemaDir(entry.name)) {
|
|
68
74
|
return false;
|
|
69
75
|
}
|
|
70
76
|
if (entry.isDirectory()) {
|
|
@@ -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 `---
|
|
@@ -136,6 +136,10 @@ export const COMMAND_REGISTRY = [
|
|
|
136
136
|
name: 'requirements-only',
|
|
137
137
|
description: 'Alias for --deltas-only (deprecated, change-specific)',
|
|
138
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: 'diff',
|
|
141
|
+
description: 'Show per-requirement diffs for delta specs (change-specific)',
|
|
142
|
+
},
|
|
139
143
|
{
|
|
140
144
|
name: 'requirements',
|
|
141
145
|
description: 'Show only requirements, exclude scenarios (JSON only, spec-specific)',
|
|
@@ -189,6 +193,10 @@ export const COMMAND_REGISTRY = [
|
|
|
189
193
|
description: 'Change name to show status for',
|
|
190
194
|
takesValue: true,
|
|
191
195
|
},
|
|
196
|
+
{
|
|
197
|
+
name: 'all',
|
|
198
|
+
description: 'Show status for all active changes',
|
|
199
|
+
},
|
|
192
200
|
{
|
|
193
201
|
name: 'schema',
|
|
194
202
|
description: 'Schema override',
|
|
@@ -285,6 +293,7 @@ export const COMMAND_REGISTRY = [
|
|
|
285
293
|
name: 'path',
|
|
286
294
|
description: 'Directory to use for the store',
|
|
287
295
|
takesValue: true,
|
|
296
|
+
completionType: 'path',
|
|
288
297
|
},
|
|
289
298
|
{
|
|
290
299
|
name: 'init-git',
|
|
@@ -377,6 +386,7 @@ export const COMMAND_REGISTRY = [
|
|
|
377
386
|
name: 'code-workspace',
|
|
378
387
|
description: 'Also write a VS Code workspace file for the set',
|
|
379
388
|
takesValue: true,
|
|
389
|
+
completionType: 'path',
|
|
380
390
|
},
|
|
381
391
|
{
|
|
382
392
|
name: 'force',
|
|
@@ -407,6 +417,7 @@ export const COMMAND_REGISTRY = [
|
|
|
407
417
|
name: 'member',
|
|
408
418
|
description: 'Member folder as <path> or <name>=<path>; repeatable, first is the primary',
|
|
409
419
|
takesValue: true,
|
|
420
|
+
completionType: 'path',
|
|
410
421
|
},
|
|
411
422
|
{
|
|
412
423
|
name: 'tool',
|
|
@@ -488,6 +499,10 @@ export const COMMAND_REGISTRY = [
|
|
|
488
499
|
name: 'requirements-only',
|
|
489
500
|
description: 'Alias for --deltas-only (deprecated)',
|
|
490
501
|
},
|
|
502
|
+
{
|
|
503
|
+
name: 'diff',
|
|
504
|
+
description: 'Show per-requirement diffs for delta specs',
|
|
505
|
+
},
|
|
491
506
|
COMMON_FLAGS.noInteractive,
|
|
492
507
|
],
|
|
493
508
|
},
|
|
@@ -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
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Static template strings for Fish completion scripts.
|
|
3
3
|
* These are Fish-specific helper functions that never change.
|
|
4
4
|
*/
|
|
5
|
-
export declare const FISH_STATIC_HELPERS = "# Helper function to
|
|
5
|
+
export declare const FISH_STATIC_HELPERS = "# Helper function to match the command path at the start of the invocation\nfunction __fish_openspec_using_command_path\n set -l expected\n set -l value_flags\n set -l reading_value_flags 0\n for argument in $argv\n if test \"$argument\" = --\n set reading_value_flags 1\n continue\n end\n if test $reading_value_flags -eq 1\n set -a value_flags $argument\n else\n set -a expected $argument\n end\n end\n\n set -l tokens (commandline -opc)\n set -e tokens[1]\n set -l path\n set -l skip 0\n for token in $tokens\n if test $skip -eq 1\n set skip 0\n continue\n end\n if test \"$token\" = --no-color\n continue\n end\n if contains -- $token $value_flags\n set skip 1\n continue\n end\n if string match -q -- '-*' $token\n continue\n end\n set -a path $token\n if test (count $path) -eq (count $expected)\n break\n end\n end\n test (count $path) -eq (count $expected); or return 1\n for index in (seq (count $expected))\n test \"$path[$index]\" = \"$expected[$index]\"; or return 1\n end\n return 0\nend\n\nfunction __fish_openspec_no_subcommand\n set -l tokens (commandline -opc)\n set -e tokens[1]\n for token in $tokens\n if test \"$token\" != --no-color\n return 1\n end\n end\n return 0\nend\n\nfunction __fish_openspec_completing_option_value\n set -l current (commandline -ct)\n for option in $argv\n if string match -q -- \"$option=*\" \"$current\"\n return 0\n end\n end\n\n set -l tokens (commandline -opc)\n test (count $tokens) -gt 0; or return 1\n contains -- $tokens[-1] $argv\nend\n\nfunction __fish_openspec_complete_attached_short_path\n set -l option $argv[1]\n set -l current (commandline -ct)\n test \"$current\" != \"$option\"; or return 1\n string match -q -- \"$option*\" \"$current\"; or return 1\n set -l value (string sub -s (math (string length -- \"$option\") + 1) -- \"$current\")\n __fish_complete_path \"$value\"\nend\n\nfunction __fish_openspec_positional_index\n set -l target $argv[1]\n set -l depth $argv[2]\n set -l value_flags $argv[3..]\n set -l tokens (commandline -opc)\n set -e tokens[1]\n set -l count 0\n set -l options 1\n set -l skip 0\n for token in $tokens\n if test $skip -eq 1\n set skip 0\n continue\n end\n if test $options -eq 1\n if test \"$token\" = --\n set options 0\n continue\n end\n if contains -- $token $value_flags\n set skip 1\n continue\n end\n if string match -q -- '-*' $token\n continue\n end\n end\n set count (math $count + 1)\n end\n test $skip -eq 0; or return 1\n test $count -eq (math $target + $depth)\nend";
|
|
6
6
|
export declare const FISH_DYNAMIC_HELPERS = "# Dynamic completion helpers\n\nfunction __fish_openspec_changes\n openspec __complete changes 2>/dev/null | while read -l id desc\n printf '%s\\t%s\\n' \"$id\" \"$desc\"\n end\nend\n\nfunction __fish_openspec_specs\n openspec __complete specs 2>/dev/null | while read -l id desc\n printf '%s\\t%s\\n' \"$id\" \"$desc\"\n end\nend\n\nfunction __fish_openspec_items\n __fish_openspec_changes\n __fish_openspec_specs\nend\n\nfunction __fish_openspec_schemas\n openspec __complete schemas 2>/dev/null | while read -l id desc\n printf '%s\\t%s\\n' \"$id\" \"$desc\"\n end\nend";
|
|
7
7
|
//# sourceMappingURL=fish-templates.d.ts.map
|
|
@@ -2,21 +2,118 @@
|
|
|
2
2
|
* Static template strings for Fish completion scripts.
|
|
3
3
|
* These are Fish-specific helper functions that never change.
|
|
4
4
|
*/
|
|
5
|
-
export const FISH_STATIC_HELPERS = `# Helper function to
|
|
6
|
-
function
|
|
7
|
-
set -l
|
|
8
|
-
set -
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
export const FISH_STATIC_HELPERS = `# Helper function to match the command path at the start of the invocation
|
|
6
|
+
function __fish_openspec_using_command_path
|
|
7
|
+
set -l expected
|
|
8
|
+
set -l value_flags
|
|
9
|
+
set -l reading_value_flags 0
|
|
10
|
+
for argument in $argv
|
|
11
|
+
if test "$argument" = --
|
|
12
|
+
set reading_value_flags 1
|
|
13
|
+
continue
|
|
14
|
+
end
|
|
15
|
+
if test $reading_value_flags -eq 1
|
|
16
|
+
set -a value_flags $argument
|
|
17
|
+
else
|
|
18
|
+
set -a expected $argument
|
|
12
19
|
end
|
|
13
20
|
end
|
|
14
|
-
|
|
21
|
+
|
|
22
|
+
set -l tokens (commandline -opc)
|
|
23
|
+
set -e tokens[1]
|
|
24
|
+
set -l path
|
|
25
|
+
set -l skip 0
|
|
26
|
+
for token in $tokens
|
|
27
|
+
if test $skip -eq 1
|
|
28
|
+
set skip 0
|
|
29
|
+
continue
|
|
30
|
+
end
|
|
31
|
+
if test "$token" = --no-color
|
|
32
|
+
continue
|
|
33
|
+
end
|
|
34
|
+
if contains -- $token $value_flags
|
|
35
|
+
set skip 1
|
|
36
|
+
continue
|
|
37
|
+
end
|
|
38
|
+
if string match -q -- '-*' $token
|
|
39
|
+
continue
|
|
40
|
+
end
|
|
41
|
+
set -a path $token
|
|
42
|
+
if test (count $path) -eq (count $expected)
|
|
43
|
+
break
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
test (count $path) -eq (count $expected); or return 1
|
|
47
|
+
for index in (seq (count $expected))
|
|
48
|
+
test "$path[$index]" = "$expected[$index]"; or return 1
|
|
49
|
+
end
|
|
50
|
+
return 0
|
|
15
51
|
end
|
|
16
52
|
|
|
17
53
|
function __fish_openspec_no_subcommand
|
|
18
|
-
set -l
|
|
19
|
-
|
|
54
|
+
set -l tokens (commandline -opc)
|
|
55
|
+
set -e tokens[1]
|
|
56
|
+
for token in $tokens
|
|
57
|
+
if test "$token" != --no-color
|
|
58
|
+
return 1
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
return 0
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
function __fish_openspec_completing_option_value
|
|
65
|
+
set -l current (commandline -ct)
|
|
66
|
+
for option in $argv
|
|
67
|
+
if string match -q -- "$option=*" "$current"
|
|
68
|
+
return 0
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
set -l tokens (commandline -opc)
|
|
73
|
+
test (count $tokens) -gt 0; or return 1
|
|
74
|
+
contains -- $tokens[-1] $argv
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
function __fish_openspec_complete_attached_short_path
|
|
78
|
+
set -l option $argv[1]
|
|
79
|
+
set -l current (commandline -ct)
|
|
80
|
+
test "$current" != "$option"; or return 1
|
|
81
|
+
string match -q -- "$option*" "$current"; or return 1
|
|
82
|
+
set -l value (string sub -s (math (string length -- "$option") + 1) -- "$current")
|
|
83
|
+
__fish_complete_path "$value"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
function __fish_openspec_positional_index
|
|
87
|
+
set -l target $argv[1]
|
|
88
|
+
set -l depth $argv[2]
|
|
89
|
+
set -l value_flags $argv[3..]
|
|
90
|
+
set -l tokens (commandline -opc)
|
|
91
|
+
set -e tokens[1]
|
|
92
|
+
set -l count 0
|
|
93
|
+
set -l options 1
|
|
94
|
+
set -l skip 0
|
|
95
|
+
for token in $tokens
|
|
96
|
+
if test $skip -eq 1
|
|
97
|
+
set skip 0
|
|
98
|
+
continue
|
|
99
|
+
end
|
|
100
|
+
if test $options -eq 1
|
|
101
|
+
if test "$token" = --
|
|
102
|
+
set options 0
|
|
103
|
+
continue
|
|
104
|
+
end
|
|
105
|
+
if contains -- $token $value_flags
|
|
106
|
+
set skip 1
|
|
107
|
+
continue
|
|
108
|
+
end
|
|
109
|
+
if string match -q -- '-*' $token
|
|
110
|
+
continue
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
set count (math $count + 1)
|
|
114
|
+
end
|
|
115
|
+
test $skip -eq 0; or return 1
|
|
116
|
+
test $count -eq (math $target + $depth)
|
|
20
117
|
end`;
|
|
21
118
|
export const FISH_DYNAMIC_HELPERS = `# Dynamic completion helpers
|
|
22
119
|
|