@fission-ai/openspec 0.18.0 โ†’ 0.20.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.
Files changed (47) hide show
  1. package/README.md +59 -0
  2. package/dist/cli/index.js +32 -2
  3. package/dist/commands/artifact-workflow.js +11 -1
  4. package/dist/commands/completion.js +42 -6
  5. package/dist/core/completions/command-registry.js +7 -1
  6. package/dist/core/completions/factory.d.ts +15 -2
  7. package/dist/core/completions/factory.js +19 -1
  8. package/dist/core/completions/generators/bash-generator.d.ts +32 -0
  9. package/dist/core/completions/generators/bash-generator.js +174 -0
  10. package/dist/core/completions/generators/fish-generator.d.ts +32 -0
  11. package/dist/core/completions/generators/fish-generator.js +157 -0
  12. package/dist/core/completions/generators/powershell-generator.d.ts +33 -0
  13. package/dist/core/completions/generators/powershell-generator.js +207 -0
  14. package/dist/core/completions/generators/zsh-generator.d.ts +0 -14
  15. package/dist/core/completions/generators/zsh-generator.js +55 -124
  16. package/dist/core/completions/installers/bash-installer.d.ts +87 -0
  17. package/dist/core/completions/installers/bash-installer.js +318 -0
  18. package/dist/core/completions/installers/fish-installer.d.ts +43 -0
  19. package/dist/core/completions/installers/fish-installer.js +143 -0
  20. package/dist/core/completions/installers/powershell-installer.d.ts +88 -0
  21. package/dist/core/completions/installers/powershell-installer.js +327 -0
  22. package/dist/core/completions/installers/zsh-installer.d.ts +1 -12
  23. package/dist/core/completions/templates/bash-templates.d.ts +6 -0
  24. package/dist/core/completions/templates/bash-templates.js +24 -0
  25. package/dist/core/completions/templates/fish-templates.d.ts +7 -0
  26. package/dist/core/completions/templates/fish-templates.js +39 -0
  27. package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
  28. package/dist/core/completions/templates/powershell-templates.js +25 -0
  29. package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
  30. package/dist/core/completions/templates/zsh-templates.js +36 -0
  31. package/dist/core/config.js +1 -0
  32. package/dist/core/configurators/slash/codebuddy.js +6 -9
  33. package/dist/core/configurators/slash/continue.d.ts +9 -0
  34. package/dist/core/configurators/slash/continue.js +46 -0
  35. package/dist/core/configurators/slash/registry.js +3 -0
  36. package/dist/core/templates/agents-template.d.ts +1 -1
  37. package/dist/core/templates/agents-template.js +7 -7
  38. package/dist/core/templates/skill-templates.d.ts +19 -0
  39. package/dist/core/templates/skill-templates.js +817 -20
  40. package/dist/core/templates/slash-command-templates.js +2 -2
  41. package/dist/telemetry/config.d.ts +32 -0
  42. package/dist/telemetry/config.js +68 -0
  43. package/dist/telemetry/index.d.ts +31 -0
  44. package/dist/telemetry/index.js +145 -0
  45. package/dist/utils/file-system.d.ts +6 -0
  46. package/dist/utils/file-system.js +43 -2
  47. package/package.json +3 -2
package/README.md CHANGED
@@ -26,6 +26,10 @@
26
26
  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.
27
27
  </p>
28
28
 
29
+ <p align="center">
30
+ <sub>๐Ÿงช <strong>New:</strong> <a href="docs/experimental-workflow.md">Experimental Workflow (OPSX)</a> โ€” schema-driven, hackable, fluid. Iterate on workflows without code changes.</sub>
31
+ </p>
32
+
29
33
  # OpenSpec
30
34
 
31
35
  OpenSpec aligns humans and AI coding assistants with spec-driven development so you agree on what to build before any code is written. **No API keys required.**
@@ -99,6 +103,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
99
103
  | **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
100
104
  | **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) โ€” see [docs](https://www.codebuddy.ai/cli) |
101
105
  | **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
106
+ | **Continue** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.continue/prompts/`) |
102
107
  | **CoStrict** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.cospec/openspec/commands/`) โ€” see [docs](https://costrict.ai)|
103
108
  | **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
104
109
  | **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
@@ -368,6 +373,53 @@ Run `openspec update` whenever someone switches tools so your agents pick up the
368
373
  2. **Refresh agent instructions**
369
374
  - Run `openspec update` inside each project to regenerate AI guidance and ensure the latest slash commands are active.
370
375
 
376
+ ## Experimental Features
377
+
378
+ <details>
379
+ <summary><strong>๐Ÿงช OPSX: Fluid, Iterative Workflow</strong> (Claude Code only)</summary>
380
+
381
+ **Why this exists:**
382
+ - Standard workflow is locked down โ€” you can't tweak instructions or customize
383
+ - When AI output is bad, you can't improve the prompts yourself
384
+ - Same workflow for everyone, no way to match how your team works
385
+
386
+ **What's different:**
387
+ - **Hackable** โ€” edit templates and schemas yourself, test immediately, no rebuild
388
+ - **Granular** โ€” each artifact has its own instructions, test and tweak individually
389
+ - **Customizable** โ€” define your own workflows, artifacts, and dependencies
390
+ - **Fluid** โ€” no phase gates, update any artifact anytime
391
+
392
+ ```
393
+ You can always go back:
394
+
395
+ proposal โ”€โ”€โ†’ specs โ”€โ”€โ†’ design โ”€โ”€โ†’ tasks โ”€โ”€โ†’ implement
396
+ โ–ฒ โ–ฒ โ–ฒ โ”‚
397
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
398
+ ```
399
+
400
+ | Command | What it does |
401
+ |---------|--------------|
402
+ | `/opsx:new` | Start a new change |
403
+ | `/opsx:continue` | Create the next artifact (based on what's ready) |
404
+ | `/opsx:ff` | Fast-forward (all planning artifacts at once) |
405
+ | `/opsx:apply` | Implement tasks, updating artifacts as needed |
406
+ | `/opsx:archive` | Archive when done |
407
+
408
+ **Setup:** `openspec artifact-experimental-setup`
409
+
410
+ [Full documentation โ†’](docs/experimental-workflow.md)
411
+
412
+ </details>
413
+
414
+ <details>
415
+ <summary><strong>Telemetry</strong> โ€“ OpenSpec collects anonymous usage stats (opt-out: <code>OPENSPEC_TELEMETRY=0</code>)</summary>
416
+
417
+ We collect only command names and version to understand usage patterns. No arguments, paths, content, or PII. Automatically disabled in CI.
418
+
419
+ **Opt-out:** `export OPENSPEC_TELEMETRY=0` or `export DO_NOT_TRACK=1`
420
+
421
+ </details>
422
+
371
423
  ## Contributing
372
424
 
373
425
  - Install dependencies: `pnpm install`
@@ -376,6 +428,13 @@ Run `openspec update` whenever someone switches tools so your agents pick up the
376
428
  - Develop CLI locally: `pnpm run dev` or `pnpm run dev:cli`
377
429
  - Conventional commits (one-line): `type(scope): subject`
378
430
 
431
+ <details>
432
+ <summary><strong>Maintainers & Advisors</strong></summary>
433
+
434
+ See [MAINTAINERS.md](MAINTAINERS.md) for the list of core maintainers and advisors who help guide the project.
435
+
436
+ </details>
437
+
379
438
  ## License
380
439
 
381
440
  MIT
package/dist/cli/index.js CHANGED
@@ -15,21 +15,51 @@ import { ShowCommand } from '../commands/show.js';
15
15
  import { CompletionCommand } from '../commands/completion.js';
16
16
  import { registerConfigCommand } from '../commands/config.js';
17
17
  import { registerArtifactWorkflowCommands } from '../commands/artifact-workflow.js';
18
+ import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/index.js';
18
19
  const program = new Command();
19
20
  const require = createRequire(import.meta.url);
20
21
  const { version } = require('../../package.json');
22
+ /**
23
+ * Get the full command path for nested commands.
24
+ * For example: 'change show' -> 'change:show'
25
+ */
26
+ function getCommandPath(command) {
27
+ const names = [];
28
+ let current = command;
29
+ while (current) {
30
+ const name = current.name();
31
+ // Skip the root 'openspec' command
32
+ if (name && name !== 'openspec') {
33
+ names.unshift(name);
34
+ }
35
+ current = current.parent;
36
+ }
37
+ return names.join(':') || 'openspec';
38
+ }
21
39
  program
22
40
  .name('openspec')
23
41
  .description('AI-native system for spec-driven development')
24
42
  .version(version);
25
43
  // Global options
26
44
  program.option('--no-color', 'Disable color output');
27
- // Apply global flags before any command runs
28
- program.hook('preAction', (thisCommand) => {
45
+ // Apply global flags and telemetry before any command runs
46
+ // Note: preAction receives (thisCommand, actionCommand) where:
47
+ // - thisCommand: the command where hook was added (root program)
48
+ // - actionCommand: the command actually being executed (subcommand)
49
+ program.hook('preAction', async (thisCommand, actionCommand) => {
29
50
  const opts = thisCommand.opts();
30
51
  if (opts.color === false) {
31
52
  process.env.NO_COLOR = '1';
32
53
  }
54
+ // Show first-run telemetry notice (if not seen)
55
+ await maybeShowTelemetryNotice();
56
+ // Track command execution (use actionCommand to get the actual subcommand)
57
+ const commandPath = getCommandPath(actionCommand);
58
+ await trackCommand(commandPath, version);
59
+ });
60
+ // Shutdown telemetry after command completes
61
+ program.hook('postAction', async () => {
62
+ await shutdown();
33
63
  });
34
64
  const availableToolIds = AI_TOOLS.filter((tool) => tool.available).map((tool) => tool.value);
35
65
  const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}`;
@@ -14,7 +14,7 @@ import path from 'path';
14
14
  import * as fs from 'fs';
15
15
  import { loadChangeContext, formatChangeStatus, generateInstructions, listSchemas, listSchemasWithInfo, getSchemaDir, resolveSchema, ArtifactGraph, } from '../core/artifact-graph/index.js';
16
16
  import { createChange, validateChangeName } from '../utils/change-utils.js';
17
- import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate } from '../core/templates/skill-templates.js';
17
+ import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxVerifyCommandTemplate } from '../core/templates/skill-templates.js';
18
18
  import { FileSystemUtils } from '../utils/file-system.js';
19
19
  const DEFAULT_SCHEMA = 'spec-driven';
20
20
  /**
@@ -580,27 +580,33 @@ async function artifactExperimentalSetupCommand() {
580
580
  const skillsDir = path.join(projectRoot, '.claude', 'skills');
581
581
  const commandsDir = path.join(projectRoot, '.claude', 'commands', 'opsx');
582
582
  // Get skill templates
583
+ const exploreSkill = getExploreSkillTemplate();
583
584
  const newChangeSkill = getNewChangeSkillTemplate();
584
585
  const continueChangeSkill = getContinueChangeSkillTemplate();
585
586
  const applyChangeSkill = getApplyChangeSkillTemplate();
586
587
  const ffChangeSkill = getFfChangeSkillTemplate();
587
588
  const syncSpecsSkill = getSyncSpecsSkillTemplate();
588
589
  const archiveChangeSkill = getArchiveChangeSkillTemplate();
590
+ const verifyChangeSkill = getVerifyChangeSkillTemplate();
589
591
  // Get command templates
592
+ const exploreCommand = getOpsxExploreCommandTemplate();
590
593
  const newCommand = getOpsxNewCommandTemplate();
591
594
  const continueCommand = getOpsxContinueCommandTemplate();
592
595
  const applyCommand = getOpsxApplyCommandTemplate();
593
596
  const ffCommand = getOpsxFfCommandTemplate();
594
597
  const syncCommand = getOpsxSyncCommandTemplate();
595
598
  const archiveCommand = getOpsxArchiveCommandTemplate();
599
+ const verifyCommand = getOpsxVerifyCommandTemplate();
596
600
  // Create skill directories and SKILL.md files
597
601
  const skills = [
602
+ { template: exploreSkill, dirName: 'openspec-explore' },
598
603
  { template: newChangeSkill, dirName: 'openspec-new-change' },
599
604
  { template: continueChangeSkill, dirName: 'openspec-continue-change' },
600
605
  { template: applyChangeSkill, dirName: 'openspec-apply-change' },
601
606
  { template: ffChangeSkill, dirName: 'openspec-ff-change' },
602
607
  { template: syncSpecsSkill, dirName: 'openspec-sync-specs' },
603
608
  { template: archiveChangeSkill, dirName: 'openspec-archive-change' },
609
+ { template: verifyChangeSkill, dirName: 'openspec-verify-change' },
604
610
  ];
605
611
  const createdSkillFiles = [];
606
612
  for (const { template, dirName } of skills) {
@@ -620,12 +626,14 @@ ${template.instructions}
620
626
  }
621
627
  // Create slash command files
622
628
  const commands = [
629
+ { template: exploreCommand, fileName: 'explore.md' },
623
630
  { template: newCommand, fileName: 'new.md' },
624
631
  { template: continueCommand, fileName: 'continue.md' },
625
632
  { template: applyCommand, fileName: 'apply.md' },
626
633
  { template: ffCommand, fileName: 'ff.md' },
627
634
  { template: syncCommand, fileName: 'sync.md' },
628
635
  { template: archiveCommand, fileName: 'archive.md' },
636
+ { template: verifyCommand, fileName: 'verify.md' },
629
637
  ];
630
638
  const createdCommandFiles = [];
631
639
  for (const { template, fileName } of commands) {
@@ -672,11 +680,13 @@ ${template.content}
672
680
  console.log(' โ€ข "Implement the tasks for this change"');
673
681
  console.log();
674
682
  console.log(' ' + chalk.cyan('Slash Commands') + ' for explicit invocation:');
683
+ console.log(' โ€ข /opsx:explore - Think through ideas, investigate problems');
675
684
  console.log(' โ€ข /opsx:new - Start a new change');
676
685
  console.log(' โ€ข /opsx:continue - Create the next artifact');
677
686
  console.log(' โ€ข /opsx:apply - Implement tasks');
678
687
  console.log(' โ€ข /opsx:ff - Fast-forward: create all artifacts at once');
679
688
  console.log(' โ€ข /opsx:sync - Sync delta specs to main specs');
689
+ console.log(' โ€ข /opsx:verify - Verify implementation matches artifacts');
680
690
  console.log(' โ€ข /opsx:archive - Archive a completed change');
681
691
  console.log();
682
692
  console.log(chalk.yellow('๐Ÿ’ก This is an experimental feature.'));
@@ -107,8 +107,24 @@ export class CompletionCommand {
107
107
  if (result.backupPath) {
108
108
  console.log(` Backup created: ${result.backupPath}`);
109
109
  }
110
- if (result.zshrcConfigured) {
111
- console.log(` ~/.zshrc configured automatically`);
110
+ // Check if any shell config was updated
111
+ const configWasUpdated = result.zshrcConfigured || result.bashrcConfigured || result.profileConfigured;
112
+ if (configWasUpdated) {
113
+ const configPaths = {
114
+ zsh: '~/.zshrc',
115
+ bash: '~/.bashrc',
116
+ fish: '~/.config/fish/config.fish',
117
+ powershell: '$PROFILE',
118
+ };
119
+ const configPath = configPaths[shell] || 'config file';
120
+ console.log(` ${configPath} configured automatically`);
121
+ }
122
+ }
123
+ // Display warnings if present
124
+ if (result.warnings && result.warnings.length > 0) {
125
+ console.log('');
126
+ for (const warning of result.warnings) {
127
+ console.log(warning);
112
128
  }
113
129
  }
114
130
  // Print instructions (only shown if .zshrc wasn't auto-configured)
@@ -118,9 +134,21 @@ export class CompletionCommand {
118
134
  console.log(instruction);
119
135
  }
120
136
  }
121
- else if (result.zshrcConfigured) {
122
- console.log('');
123
- console.log('Restart your shell or run: exec zsh');
137
+ else {
138
+ // Check if any shell config was updated (InstallationResult has: zshrcConfigured, bashrcConfigured, profileConfigured)
139
+ const configWasUpdated = result.zshrcConfigured || result.bashrcConfigured || result.profileConfigured;
140
+ if (configWasUpdated) {
141
+ console.log('');
142
+ // Shell-specific reload instructions
143
+ const reloadCommands = {
144
+ zsh: 'exec zsh',
145
+ bash: 'exec bash',
146
+ fish: 'exec fish',
147
+ powershell: '. $PROFILE',
148
+ };
149
+ const reloadCmd = reloadCommands[shell] || `restart your ${shell} shell`;
150
+ console.log(`Restart your shell or run: ${reloadCmd}`);
151
+ }
124
152
  }
125
153
  }
126
154
  else {
@@ -142,8 +170,16 @@ export class CompletionCommand {
142
170
  // Prompt for confirmation unless --yes flag is provided
143
171
  if (!skipConfirmation) {
144
172
  const { confirm } = await import('@inquirer/prompts');
173
+ // Get shell-specific config file path
174
+ const configPaths = {
175
+ zsh: '~/.zshrc',
176
+ bash: '~/.bashrc',
177
+ fish: 'Fish configuration', // Fish doesn't modify profile, just removes script file
178
+ powershell: '$PROFILE',
179
+ };
180
+ const configPath = configPaths[shell] || `${shell} configuration`;
145
181
  const confirmed = await confirm({
146
- message: 'Remove OpenSpec configuration from ~/.zshrc?',
182
+ message: `Remove OpenSpec configuration from ${configPath}?`,
147
183
  default: false,
148
184
  });
149
185
  if (!confirmed) {
@@ -281,7 +281,13 @@ export const COMMAND_REGISTRY = [
281
281
  description: 'Uninstall completion script for a shell',
282
282
  acceptsPositional: true,
283
283
  positionalType: 'shell',
284
- flags: [],
284
+ flags: [
285
+ {
286
+ name: 'yes',
287
+ short: 'y',
288
+ description: 'Skip confirmation prompts',
289
+ },
290
+ ],
285
291
  },
286
292
  ],
287
293
  },
@@ -1,6 +1,20 @@
1
1
  import { CompletionGenerator } from './types.js';
2
- import { InstallationResult } from './installers/zsh-installer.js';
3
2
  import { SupportedShell } from '../../utils/shell-detection.js';
3
+ /**
4
+ * Common installation result interface
5
+ */
6
+ export interface InstallationResult {
7
+ success: boolean;
8
+ installedPath?: string;
9
+ backupPath?: string;
10
+ message: string;
11
+ instructions?: string[];
12
+ warnings?: string[];
13
+ isOhMyZsh?: boolean;
14
+ zshrcConfigured?: boolean;
15
+ bashrcConfigured?: boolean;
16
+ profileConfigured?: boolean;
17
+ }
4
18
  /**
5
19
  * Interface for completion installers
6
20
  */
@@ -11,7 +25,6 @@ export interface CompletionInstaller {
11
25
  message: string;
12
26
  }>;
13
27
  }
14
- export type { InstallationResult };
15
28
  /**
16
29
  * Factory for creating completion generators and installers
17
30
  * This design makes it easy to add support for additional shells
@@ -1,11 +1,17 @@
1
1
  import { ZshGenerator } from './generators/zsh-generator.js';
2
+ import { BashGenerator } from './generators/bash-generator.js';
3
+ import { FishGenerator } from './generators/fish-generator.js';
4
+ import { PowerShellGenerator } from './generators/powershell-generator.js';
2
5
  import { ZshInstaller } from './installers/zsh-installer.js';
6
+ import { BashInstaller } from './installers/bash-installer.js';
7
+ import { FishInstaller } from './installers/fish-installer.js';
8
+ import { PowerShellInstaller } from './installers/powershell-installer.js';
3
9
  /**
4
10
  * Factory for creating completion generators and installers
5
11
  * This design makes it easy to add support for additional shells
6
12
  */
7
13
  export class CompletionFactory {
8
- static SUPPORTED_SHELLS = ['zsh'];
14
+ static SUPPORTED_SHELLS = ['zsh', 'bash', 'fish', 'powershell'];
9
15
  /**
10
16
  * Create a completion generator for the specified shell
11
17
  *
@@ -17,6 +23,12 @@ export class CompletionFactory {
17
23
  switch (shell) {
18
24
  case 'zsh':
19
25
  return new ZshGenerator();
26
+ case 'bash':
27
+ return new BashGenerator();
28
+ case 'fish':
29
+ return new FishGenerator();
30
+ case 'powershell':
31
+ return new PowerShellGenerator();
20
32
  default:
21
33
  throw new Error(`Unsupported shell: ${shell}`);
22
34
  }
@@ -32,6 +44,12 @@ export class CompletionFactory {
32
44
  switch (shell) {
33
45
  case 'zsh':
34
46
  return new ZshInstaller();
47
+ case 'bash':
48
+ return new BashInstaller();
49
+ case 'fish':
50
+ return new FishInstaller();
51
+ case 'powershell':
52
+ return new PowerShellInstaller();
35
53
  default:
36
54
  throw new Error(`Unsupported shell: ${shell}`);
37
55
  }
@@ -0,0 +1,32 @@
1
+ import { CompletionGenerator, CommandDefinition } from '../types.js';
2
+ /**
3
+ * Generates Bash completion scripts for the OpenSpec CLI.
4
+ * Follows Bash completion conventions using complete builtin and COMPREPLY array.
5
+ */
6
+ export declare class BashGenerator implements CompletionGenerator {
7
+ readonly shell: "bash";
8
+ /**
9
+ * Generate a Bash completion script
10
+ *
11
+ * @param commands - Command definitions to generate completions for
12
+ * @returns Bash completion script as a string
13
+ */
14
+ generate(commands: CommandDefinition[]): string;
15
+ /**
16
+ * Generate completion case logic for a command
17
+ */
18
+ private generateCommandCase;
19
+ /**
20
+ * Generate argument completion (flags and positional arguments)
21
+ */
22
+ private generateArgumentCompletion;
23
+ /**
24
+ * Generate positional argument completion based on type
25
+ */
26
+ private generatePositionalCompletion;
27
+ /**
28
+ * Escape command/subcommand names for safe use in Bash scripts
29
+ */
30
+ private escapeCommandName;
31
+ }
32
+ //# sourceMappingURL=bash-generator.d.ts.map
@@ -0,0 +1,174 @@
1
+ import { BASH_DYNAMIC_HELPERS } from '../templates/bash-templates.js';
2
+ /**
3
+ * Generates Bash completion scripts for the OpenSpec CLI.
4
+ * Follows Bash completion conventions using complete builtin and COMPREPLY array.
5
+ */
6
+ export class BashGenerator {
7
+ shell = 'bash';
8
+ /**
9
+ * Generate a Bash completion script
10
+ *
11
+ * @param commands - Command definitions to generate completions for
12
+ * @returns Bash completion script as a string
13
+ */
14
+ generate(commands) {
15
+ // Build command list for top-level completions
16
+ const commandList = commands.map(c => this.escapeCommandName(c.name)).join(' ');
17
+ // Build command cases using push() for loop clarity
18
+ const caseLines = [];
19
+ for (const cmd of commands) {
20
+ caseLines.push(` ${cmd.name})`);
21
+ caseLines.push(...this.generateCommandCase(cmd, ' '));
22
+ caseLines.push(' ;;');
23
+ }
24
+ const commandCases = caseLines.join('\n');
25
+ // Dynamic completion helpers from template
26
+ const helpers = BASH_DYNAMIC_HELPERS;
27
+ // Assemble final script with template literal
28
+ return `# Bash completion script for OpenSpec CLI
29
+ # Auto-generated - do not edit manually
30
+
31
+ _openspec_completion() {
32
+ local cur prev words cword
33
+
34
+ # Use _init_completion if available (from bash-completion package)
35
+ # The -n : option prevents colons from being treated as word separators
36
+ # (important for spec/change IDs that may contain colons)
37
+ # Otherwise, fall back to manual initialization
38
+ if declare -F _init_completion >/dev/null 2>&1; then
39
+ _init_completion -n : || return
40
+ else
41
+ # Manual fallback when bash-completion is not installed
42
+ COMPREPLY=()
43
+ cur="\${COMP_WORDS[COMP_CWORD]}"
44
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
45
+ words=("\${COMP_WORDS[@]}")
46
+ cword=$COMP_CWORD
47
+ fi
48
+
49
+ local cmd="\${words[1]}"
50
+ local subcmd="\${words[2]}"
51
+
52
+ # Top-level commands
53
+ if [[ $cword -eq 1 ]]; then
54
+ local commands="${commandList}"
55
+ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
56
+ return 0
57
+ fi
58
+
59
+ # Command-specific completion
60
+ case "$cmd" in
61
+ ${commandCases}
62
+ esac
63
+
64
+ return 0
65
+ }
66
+
67
+ ${helpers}
68
+ complete -F _openspec_completion openspec
69
+ `;
70
+ }
71
+ /**
72
+ * Generate completion case logic for a command
73
+ */
74
+ generateCommandCase(cmd, indent) {
75
+ const lines = [];
76
+ // Handle subcommands
77
+ if (cmd.subcommands && cmd.subcommands.length > 0) {
78
+ // First, check if user is typing a flag for the parent command
79
+ if (cmd.flags.length > 0) {
80
+ lines.push(`${indent}if [[ "$cur" == -* ]]; then`);
81
+ const flags = cmd.flags.map(f => {
82
+ const parts = [];
83
+ if (f.short)
84
+ parts.push(`-${f.short}`);
85
+ parts.push(`--${f.name}`);
86
+ return parts.join(' ');
87
+ }).join(' ');
88
+ lines.push(`${indent} local flags="${flags}"`);
89
+ lines.push(`${indent} COMPREPLY=($(compgen -W "$flags" -- "$cur"))`);
90
+ lines.push(`${indent} return 0`);
91
+ lines.push(`${indent}fi`);
92
+ lines.push('');
93
+ }
94
+ lines.push(`${indent}if [[ $cword -eq 2 ]]; then`);
95
+ lines.push(`${indent} local subcommands="` + cmd.subcommands.map(s => this.escapeCommandName(s.name)).join(' ') + '"');
96
+ lines.push(`${indent} COMPREPLY=($(compgen -W "$subcommands" -- "$cur"))`);
97
+ lines.push(`${indent} return 0`);
98
+ lines.push(`${indent}fi`);
99
+ lines.push('');
100
+ lines.push(`${indent}case "$subcmd" in`);
101
+ for (const subcmd of cmd.subcommands) {
102
+ lines.push(`${indent} ${subcmd.name})`);
103
+ lines.push(...this.generateArgumentCompletion(subcmd, indent + ' '));
104
+ lines.push(`${indent} ;;`);
105
+ }
106
+ lines.push(`${indent}esac`);
107
+ }
108
+ else {
109
+ // No subcommands, just complete arguments
110
+ lines.push(...this.generateArgumentCompletion(cmd, indent));
111
+ }
112
+ return lines;
113
+ }
114
+ /**
115
+ * Generate argument completion (flags and positional arguments)
116
+ */
117
+ generateArgumentCompletion(cmd, indent) {
118
+ const lines = [];
119
+ // Check for flag completion
120
+ if (cmd.flags.length > 0) {
121
+ lines.push(`${indent}if [[ "$cur" == -* ]]; then`);
122
+ const flags = cmd.flags.map(f => {
123
+ const parts = [];
124
+ if (f.short)
125
+ parts.push(`-${f.short}`);
126
+ parts.push(`--${f.name}`);
127
+ return parts.join(' ');
128
+ }).join(' ');
129
+ lines.push(`${indent} local flags="${flags}"`);
130
+ lines.push(`${indent} COMPREPLY=($(compgen -W "$flags" -- "$cur"))`);
131
+ lines.push(`${indent} return 0`);
132
+ lines.push(`${indent}fi`);
133
+ lines.push('');
134
+ }
135
+ // Handle positional completions
136
+ if (cmd.acceptsPositional) {
137
+ lines.push(...this.generatePositionalCompletion(cmd.positionalType, indent));
138
+ }
139
+ return lines;
140
+ }
141
+ /**
142
+ * Generate positional argument completion based on type
143
+ */
144
+ generatePositionalCompletion(positionalType, indent) {
145
+ const lines = [];
146
+ switch (positionalType) {
147
+ case 'change-id':
148
+ lines.push(`${indent}_openspec_complete_changes`);
149
+ break;
150
+ case 'spec-id':
151
+ lines.push(`${indent}_openspec_complete_specs`);
152
+ break;
153
+ case 'change-or-spec-id':
154
+ lines.push(`${indent}_openspec_complete_items`);
155
+ break;
156
+ case 'shell':
157
+ lines.push(`${indent}local shells="zsh bash fish powershell"`);
158
+ lines.push(`${indent}COMPREPLY=($(compgen -W "$shells" -- "$cur"))`);
159
+ break;
160
+ case 'path':
161
+ lines.push(`${indent}COMPREPLY=($(compgen -f -- "$cur"))`);
162
+ break;
163
+ }
164
+ return lines;
165
+ }
166
+ /**
167
+ * Escape command/subcommand names for safe use in Bash scripts
168
+ */
169
+ escapeCommandName(name) {
170
+ // Escape shell metacharacters to prevent command injection
171
+ return name.replace(/["\$`\\]/g, '\\$&');
172
+ }
173
+ }
174
+ //# sourceMappingURL=bash-generator.js.map
@@ -0,0 +1,32 @@
1
+ import { CompletionGenerator, CommandDefinition } from '../types.js';
2
+ /**
3
+ * Generates Fish completion scripts for the OpenSpec CLI.
4
+ * Follows Fish completion conventions using the complete command.
5
+ */
6
+ export declare class FishGenerator implements CompletionGenerator {
7
+ readonly shell: "fish";
8
+ /**
9
+ * Generate a Fish completion script
10
+ *
11
+ * @param commands - Command definitions to generate completions for
12
+ * @returns Fish completion script as a string
13
+ */
14
+ generate(commands: CommandDefinition[]): string;
15
+ /**
16
+ * Generate completions for a specific command
17
+ */
18
+ private generateCommandCompletions;
19
+ /**
20
+ * Generate flag completion
21
+ */
22
+ private generateFlagCompletion;
23
+ /**
24
+ * Generate positional argument completion
25
+ */
26
+ private generatePositionalCompletion;
27
+ /**
28
+ * Escape description text for Fish
29
+ */
30
+ private escapeDescription;
31
+ }
32
+ //# sourceMappingURL=fish-generator.d.ts.map