@fission-ai/openspec 0.16.0 → 0.17.1

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 (39) hide show
  1. package/README.md +21 -14
  2. package/dist/cli/index.js +67 -2
  3. package/dist/commands/change.js +4 -3
  4. package/dist/commands/completion.d.ts +72 -0
  5. package/dist/commands/completion.js +221 -0
  6. package/dist/commands/config.d.ts +8 -0
  7. package/dist/commands/config.js +198 -0
  8. package/dist/commands/show.js +3 -2
  9. package/dist/commands/spec.js +4 -3
  10. package/dist/commands/validate.js +21 -2
  11. package/dist/core/archive.js +4 -1
  12. package/dist/core/completions/command-registry.d.ts +7 -0
  13. package/dist/core/completions/command-registry.js +362 -0
  14. package/dist/core/completions/completion-provider.d.ts +60 -0
  15. package/dist/core/completions/completion-provider.js +102 -0
  16. package/dist/core/completions/factory.d.ts +51 -0
  17. package/dist/core/completions/factory.js +57 -0
  18. package/dist/core/completions/generators/zsh-generator.d.ts +58 -0
  19. package/dist/core/completions/generators/zsh-generator.js +319 -0
  20. package/dist/core/completions/installers/zsh-installer.d.ts +136 -0
  21. package/dist/core/completions/installers/zsh-installer.js +449 -0
  22. package/dist/core/completions/types.d.ts +78 -0
  23. package/dist/core/completions/types.js +2 -0
  24. package/dist/core/config-schema.d.ts +76 -0
  25. package/dist/core/config-schema.js +200 -0
  26. package/dist/core/configurators/slash/opencode.js +0 -3
  27. package/dist/core/global-config.d.ts +29 -0
  28. package/dist/core/global-config.js +87 -0
  29. package/dist/core/index.d.ts +1 -1
  30. package/dist/core/index.js +2 -1
  31. package/dist/utils/file-system.js +19 -3
  32. package/dist/utils/interactive.d.ts +12 -1
  33. package/dist/utils/interactive.js +7 -2
  34. package/dist/utils/item-discovery.d.ts +1 -0
  35. package/dist/utils/item-discovery.js +23 -0
  36. package/dist/utils/shell-detection.d.ts +20 -0
  37. package/dist/utils/shell-detection.js +41 -0
  38. package/package.json +7 -1
  39. package/scripts/postinstall.js +147 -0
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Generates Zsh completion scripts for the OpenSpec CLI.
3
+ * Follows Zsh completion system conventions using the _openspec function.
4
+ */
5
+ export class ZshGenerator {
6
+ shell = 'zsh';
7
+ /**
8
+ * Generate a Zsh completion script
9
+ *
10
+ * @param commands - Command definitions to generate completions for
11
+ * @returns Zsh completion script as a string
12
+ */
13
+ generate(commands) {
14
+ const script = [];
15
+ // Header comment
16
+ script.push('#compdef openspec');
17
+ script.push('');
18
+ script.push('# Zsh completion script for OpenSpec CLI');
19
+ script.push('# Auto-generated - do not edit manually');
20
+ script.push('');
21
+ // Main completion function
22
+ script.push('_openspec() {');
23
+ script.push(' local context state line');
24
+ script.push(' typeset -A opt_args');
25
+ script.push('');
26
+ // Generate main command argument specification
27
+ script.push(' local -a commands');
28
+ script.push(' commands=(');
29
+ for (const cmd of commands) {
30
+ const escapedDesc = this.escapeDescription(cmd.description);
31
+ script.push(` '${cmd.name}:${escapedDesc}'`);
32
+ }
33
+ script.push(' )');
34
+ script.push('');
35
+ // Main _arguments call
36
+ script.push(' _arguments -C \\');
37
+ script.push(' "1: :->command" \\');
38
+ script.push(' "*::arg:->args"');
39
+ script.push('');
40
+ // Command dispatch logic
41
+ script.push(' case $state in');
42
+ script.push(' command)');
43
+ script.push(' _describe "openspec command" commands');
44
+ script.push(' ;;');
45
+ script.push(' args)');
46
+ script.push(' case $words[1] in');
47
+ // Generate completion for each command
48
+ for (const cmd of commands) {
49
+ script.push(` ${cmd.name})`);
50
+ script.push(` _openspec_${this.sanitizeFunctionName(cmd.name)}`);
51
+ script.push(' ;;');
52
+ }
53
+ script.push(' esac');
54
+ script.push(' ;;');
55
+ script.push(' esac');
56
+ script.push('}');
57
+ script.push('');
58
+ // Generate individual command completion functions
59
+ for (const cmd of commands) {
60
+ script.push(...this.generateCommandFunction(cmd));
61
+ script.push('');
62
+ }
63
+ // Add dynamic completion helper functions
64
+ script.push(...this.generateDynamicCompletionHelpers());
65
+ // Register the completion function
66
+ script.push('compdef _openspec openspec');
67
+ script.push('');
68
+ return script.join('\n');
69
+ }
70
+ /**
71
+ * Generate a single completion function
72
+ *
73
+ * @param functionName - Name of the completion function
74
+ * @param varName - Name of the local array variable
75
+ * @param varLabel - Label for the completion items
76
+ * @param commandLines - Command line(s) to populate the array
77
+ * @param comment - Optional comment describing the function
78
+ */
79
+ generateCompletionFunction(functionName, varName, varLabel, commandLines, comment) {
80
+ const lines = [];
81
+ if (comment) {
82
+ lines.push(comment);
83
+ }
84
+ lines.push(`${functionName}() {`);
85
+ lines.push(` local -a ${varName}`);
86
+ if (commandLines.length === 1) {
87
+ lines.push(` ${commandLines[0]}`);
88
+ }
89
+ else {
90
+ lines.push(` ${varName}=(`);
91
+ for (let i = 0; i < commandLines.length; i++) {
92
+ const suffix = i < commandLines.length - 1 ? ' \\' : '';
93
+ lines.push(` ${commandLines[i]}${suffix}`);
94
+ }
95
+ lines.push(' )');
96
+ }
97
+ lines.push(` _describe "${varLabel}" ${varName}`);
98
+ lines.push('}');
99
+ lines.push('');
100
+ return lines;
101
+ }
102
+ /**
103
+ * Generate dynamic completion helper functions for change and spec IDs
104
+ */
105
+ generateDynamicCompletionHelpers() {
106
+ const lines = [];
107
+ lines.push('# Dynamic completion helpers');
108
+ lines.push('');
109
+ // Helper function for completing change IDs
110
+ lines.push('# Use openspec __complete to get available changes');
111
+ lines.push('_openspec_complete_changes() {');
112
+ lines.push(' local -a changes');
113
+ lines.push(' while IFS=$\'\\t\' read -r id desc; do');
114
+ lines.push(' changes+=("$id:$desc")');
115
+ lines.push(' done < <(openspec __complete changes 2>/dev/null)');
116
+ lines.push(' _describe "change" changes');
117
+ lines.push('}');
118
+ lines.push('');
119
+ // Helper function for completing spec IDs
120
+ lines.push('# Use openspec __complete to get available specs');
121
+ lines.push('_openspec_complete_specs() {');
122
+ lines.push(' local -a specs');
123
+ lines.push(' while IFS=$\'\\t\' read -r id desc; do');
124
+ lines.push(' specs+=("$id:$desc")');
125
+ lines.push(' done < <(openspec __complete specs 2>/dev/null)');
126
+ lines.push(' _describe "spec" specs');
127
+ lines.push('}');
128
+ lines.push('');
129
+ // Helper function for completing both changes and specs
130
+ lines.push('# Get both changes and specs');
131
+ lines.push('_openspec_complete_items() {');
132
+ lines.push(' local -a items');
133
+ lines.push(' while IFS=$\'\\t\' read -r id desc; do');
134
+ lines.push(' items+=("$id:$desc")');
135
+ lines.push(' done < <(openspec __complete changes 2>/dev/null)');
136
+ lines.push(' while IFS=$\'\\t\' read -r id desc; do');
137
+ lines.push(' items+=("$id:$desc")');
138
+ lines.push(' done < <(openspec __complete specs 2>/dev/null)');
139
+ lines.push(' _describe "item" items');
140
+ lines.push('}');
141
+ lines.push('');
142
+ return lines;
143
+ }
144
+ /**
145
+ * Generate completion function for a specific command
146
+ */
147
+ generateCommandFunction(cmd) {
148
+ const funcName = `_openspec_${this.sanitizeFunctionName(cmd.name)}`;
149
+ const lines = [];
150
+ lines.push(`${funcName}() {`);
151
+ // If command has subcommands, handle them
152
+ if (cmd.subcommands && cmd.subcommands.length > 0) {
153
+ lines.push(' local context state line');
154
+ lines.push(' typeset -A opt_args');
155
+ lines.push('');
156
+ lines.push(' local -a subcommands');
157
+ lines.push(' subcommands=(');
158
+ for (const subcmd of cmd.subcommands) {
159
+ const escapedDesc = this.escapeDescription(subcmd.description);
160
+ lines.push(` '${subcmd.name}:${escapedDesc}'`);
161
+ }
162
+ lines.push(' )');
163
+ lines.push('');
164
+ lines.push(' _arguments -C \\');
165
+ // Add command flags
166
+ for (const flag of cmd.flags) {
167
+ lines.push(' ' + this.generateFlagSpec(flag) + ' \\');
168
+ }
169
+ lines.push(' "1: :->subcommand" \\');
170
+ lines.push(' "*::arg:->args"');
171
+ lines.push('');
172
+ lines.push(' case $state in');
173
+ lines.push(' subcommand)');
174
+ lines.push(' _describe "subcommand" subcommands');
175
+ lines.push(' ;;');
176
+ lines.push(' args)');
177
+ lines.push(' case $words[1] in');
178
+ for (const subcmd of cmd.subcommands) {
179
+ lines.push(` ${subcmd.name})`);
180
+ lines.push(` _openspec_${this.sanitizeFunctionName(cmd.name)}_${this.sanitizeFunctionName(subcmd.name)}`);
181
+ lines.push(' ;;');
182
+ }
183
+ lines.push(' esac');
184
+ lines.push(' ;;');
185
+ lines.push(' esac');
186
+ }
187
+ else {
188
+ // Command without subcommands
189
+ lines.push(' _arguments \\');
190
+ // Add flags
191
+ for (const flag of cmd.flags) {
192
+ lines.push(' ' + this.generateFlagSpec(flag) + ' \\');
193
+ }
194
+ // Add positional argument completion
195
+ if (cmd.acceptsPositional) {
196
+ const positionalSpec = this.generatePositionalSpec(cmd.positionalType);
197
+ lines.push(' ' + positionalSpec);
198
+ }
199
+ else {
200
+ // Remove trailing backslash from last flag
201
+ if (lines[lines.length - 1].endsWith(' \\')) {
202
+ lines[lines.length - 1] = lines[lines.length - 1].slice(0, -2);
203
+ }
204
+ }
205
+ }
206
+ lines.push('}');
207
+ // Generate subcommand functions if they exist
208
+ if (cmd.subcommands) {
209
+ for (const subcmd of cmd.subcommands) {
210
+ lines.push('');
211
+ lines.push(...this.generateSubcommandFunction(cmd.name, subcmd));
212
+ }
213
+ }
214
+ return lines;
215
+ }
216
+ /**
217
+ * Generate completion function for a subcommand
218
+ */
219
+ generateSubcommandFunction(parentName, subcmd) {
220
+ const funcName = `_openspec_${this.sanitizeFunctionName(parentName)}_${this.sanitizeFunctionName(subcmd.name)}`;
221
+ const lines = [];
222
+ lines.push(`${funcName}() {`);
223
+ lines.push(' _arguments \\');
224
+ // Add flags
225
+ for (const flag of subcmd.flags) {
226
+ lines.push(' ' + this.generateFlagSpec(flag) + ' \\');
227
+ }
228
+ // Add positional argument completion
229
+ if (subcmd.acceptsPositional) {
230
+ const positionalSpec = this.generatePositionalSpec(subcmd.positionalType);
231
+ lines.push(' ' + positionalSpec);
232
+ }
233
+ else {
234
+ // Remove trailing backslash from last flag
235
+ if (lines[lines.length - 1].endsWith(' \\')) {
236
+ lines[lines.length - 1] = lines[lines.length - 1].slice(0, -2);
237
+ }
238
+ }
239
+ lines.push('}');
240
+ return lines;
241
+ }
242
+ /**
243
+ * Generate flag specification for _arguments
244
+ */
245
+ generateFlagSpec(flag) {
246
+ const parts = [];
247
+ // Handle mutually exclusive short and long forms
248
+ if (flag.short) {
249
+ parts.push(`'(-${flag.short} --${flag.name})'{-${flag.short},--${flag.name}}'`);
250
+ }
251
+ else {
252
+ parts.push(`'--${flag.name}`);
253
+ }
254
+ // Add description
255
+ const escapedDesc = this.escapeDescription(flag.description);
256
+ parts.push(`[${escapedDesc}]`);
257
+ // Add value completion if flag takes a value
258
+ if (flag.takesValue) {
259
+ if (flag.values && flag.values.length > 0) {
260
+ // Provide specific value completions
261
+ const valueList = flag.values.map(v => this.escapeValue(v)).join(' ');
262
+ parts.push(`:value:(${valueList})`);
263
+ }
264
+ else {
265
+ // Generic value placeholder
266
+ parts.push(':value:');
267
+ }
268
+ }
269
+ // Close the quote (needed for both short and long forms)
270
+ parts.push("'");
271
+ return parts.join('');
272
+ }
273
+ /**
274
+ * Generate positional argument specification
275
+ */
276
+ generatePositionalSpec(positionalType) {
277
+ switch (positionalType) {
278
+ case 'change-id':
279
+ return "'*: :_openspec_complete_changes'";
280
+ case 'spec-id':
281
+ return "'*: :_openspec_complete_specs'";
282
+ case 'change-or-spec-id':
283
+ return "'*: :_openspec_complete_items'";
284
+ case 'path':
285
+ return "'*:path:_files'";
286
+ case 'shell':
287
+ return "'*:shell:(zsh)'";
288
+ default:
289
+ return "'*: :_default'";
290
+ }
291
+ }
292
+ /**
293
+ * Escape special characters in descriptions
294
+ */
295
+ escapeDescription(desc) {
296
+ return desc
297
+ .replace(/\\/g, '\\\\')
298
+ .replace(/'/g, "\\'")
299
+ .replace(/\[/g, '\\[')
300
+ .replace(/]/g, '\\]')
301
+ .replace(/:/g, '\\:');
302
+ }
303
+ /**
304
+ * Escape special characters in values
305
+ */
306
+ escapeValue(value) {
307
+ return value
308
+ .replace(/\\/g, '\\\\')
309
+ .replace(/'/g, "\\'")
310
+ .replace(/ /g, '\\ ');
311
+ }
312
+ /**
313
+ * Sanitize command names for use in function names
314
+ */
315
+ sanitizeFunctionName(name) {
316
+ return name.replace(/-/g, '_');
317
+ }
318
+ }
319
+ //# sourceMappingURL=zsh-generator.js.map
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Installation result information
3
+ */
4
+ export interface InstallationResult {
5
+ success: boolean;
6
+ installedPath?: string;
7
+ backupPath?: string;
8
+ isOhMyZsh: boolean;
9
+ zshrcConfigured?: boolean;
10
+ message: string;
11
+ instructions?: string[];
12
+ }
13
+ /**
14
+ * Installer for Zsh completion scripts.
15
+ * Supports both Oh My Zsh and standard Zsh configurations.
16
+ */
17
+ export declare class ZshInstaller {
18
+ private readonly homeDir;
19
+ /**
20
+ * Markers for .zshrc configuration management
21
+ */
22
+ private readonly ZSHRC_MARKERS;
23
+ constructor(homeDir?: string);
24
+ /**
25
+ * Check if Oh My Zsh is installed
26
+ *
27
+ * @returns true if Oh My Zsh is detected via $ZSH env var or directory exists
28
+ */
29
+ isOhMyZshInstalled(): Promise<boolean>;
30
+ /**
31
+ * Get the appropriate installation path for the completion script
32
+ *
33
+ * @returns Object with installation path and whether it's Oh My Zsh
34
+ */
35
+ getInstallationPath(): Promise<{
36
+ path: string;
37
+ isOhMyZsh: boolean;
38
+ }>;
39
+ /**
40
+ * Backup an existing completion file if it exists
41
+ *
42
+ * @param targetPath - Path to the file to backup
43
+ * @returns Path to the backup file, or undefined if no backup was needed
44
+ */
45
+ backupExistingFile(targetPath: string): Promise<string | undefined>;
46
+ /**
47
+ * Get the path to .zshrc file
48
+ *
49
+ * @returns Path to .zshrc
50
+ */
51
+ private getZshrcPath;
52
+ /**
53
+ * Generate .zshrc configuration content
54
+ *
55
+ * @param completionsDir - Directory containing completion scripts
56
+ * @returns Configuration content
57
+ */
58
+ private generateZshrcConfig;
59
+ /**
60
+ * Configure .zshrc to enable completions
61
+ * Only applies to standard Zsh (not Oh My Zsh)
62
+ *
63
+ * @param completionsDir - Directory containing completion scripts
64
+ * @returns true if configured successfully, false otherwise
65
+ */
66
+ configureZshrc(completionsDir: string): Promise<boolean>;
67
+ /**
68
+ * Check if .zshrc has OpenSpec configuration markers
69
+ *
70
+ * @returns true if .zshrc exists and has markers
71
+ */
72
+ private hasZshrcConfig;
73
+ /**
74
+ * Check if fpath configuration is needed for a given directory
75
+ * Used to verify if Oh My Zsh (or other) completions directory is already in fpath
76
+ *
77
+ * @param completionsDir - Directory to check for in fpath
78
+ * @returns true if configuration is needed, false if directory is already referenced
79
+ */
80
+ private needsFpathConfig;
81
+ /**
82
+ * Remove .zshrc configuration
83
+ * Used during uninstallation
84
+ *
85
+ * @returns true if removed successfully, false otherwise
86
+ */
87
+ removeZshrcConfig(): Promise<boolean>;
88
+ /**
89
+ * Install the completion script
90
+ *
91
+ * @param completionScript - The completion script content to install
92
+ * @returns Installation result with status and instructions
93
+ */
94
+ install(completionScript: string): Promise<InstallationResult>;
95
+ /**
96
+ * Generate Oh My Zsh fpath verification guidance
97
+ *
98
+ * @param completionsDir - Custom completions directory path
99
+ * @returns Array of guidance strings, or undefined if not needed
100
+ */
101
+ private generateOhMyZshFpathGuidance;
102
+ /**
103
+ * Generate user instructions for enabling completions
104
+ *
105
+ * @param isOhMyZsh - Whether Oh My Zsh is being used
106
+ * @param installedPath - Path where the script was installed
107
+ * @returns Array of instruction strings
108
+ */
109
+ private generateInstructions;
110
+ /**
111
+ * Uninstall the completion script
112
+ *
113
+ * @returns true if uninstalled successfully, false otherwise
114
+ */
115
+ uninstall(): Promise<{
116
+ success: boolean;
117
+ message: string;
118
+ }>;
119
+ /**
120
+ * Check if completion script is currently installed
121
+ *
122
+ * @returns true if the completion script exists
123
+ */
124
+ isInstalled(): Promise<boolean>;
125
+ /**
126
+ * Get information about the current installation
127
+ *
128
+ * @returns Installation status information
129
+ */
130
+ getInstallationInfo(): Promise<{
131
+ installed: boolean;
132
+ path?: string;
133
+ isOhMyZsh?: boolean;
134
+ }>;
135
+ }
136
+ //# sourceMappingURL=zsh-installer.d.ts.map