@fission-ai/openspec 0.10.0 → 0.12.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/README.md CHANGED
@@ -92,11 +92,13 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
92
92
  |------|----------|
93
93
  | **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
94
94
  | **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
95
+ | **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
95
96
  | **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
96
97
  | **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
97
98
  | **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
98
99
  | **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
99
100
  | **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
101
+ | **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
100
102
 
101
103
  Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
102
104
 
package/dist/cli/index.js CHANGED
@@ -4,6 +4,7 @@ import ora from 'ora';
4
4
  import path from 'path';
5
5
  import { promises as fs } from 'fs';
6
6
  import { InitCommand } from '../core/init.js';
7
+ import { AI_TOOLS } from '../core/config.js';
7
8
  import { UpdateCommand } from '../core/update.js';
8
9
  import { ListCommand } from '../core/list.js';
9
10
  import { ArchiveCommand } from '../core/archive.js';
@@ -28,10 +29,13 @@ program.hook('preAction', (thisCommand) => {
28
29
  process.env.NO_COLOR = '1';
29
30
  }
30
31
  });
32
+ const availableToolIds = AI_TOOLS.filter((tool) => tool.available).map((tool) => tool.value);
33
+ const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}`;
31
34
  program
32
35
  .command('init [path]')
33
36
  .description('Initialize OpenSpec in your project')
34
- .action(async (targetPath = '.') => {
37
+ .option('--tools <tools>', toolsOptionDescription)
38
+ .action(async (targetPath = '.', options) => {
35
39
  try {
36
40
  // Validate that the path is a valid directory
37
41
  const resolvedPath = path.resolve(targetPath);
@@ -53,7 +57,9 @@ program
53
57
  throw new Error(`Cannot access path "${targetPath}": ${error.message}`);
54
58
  }
55
59
  }
56
- const initCommand = new InitCommand();
60
+ const initCommand = new InitCommand({
61
+ tools: options?.tools,
62
+ });
57
63
  await initCommand.execute(targetPath);
58
64
  }
59
65
  catch (error) {
@@ -6,11 +6,13 @@ export const OPENSPEC_MARKERS = {
6
6
  export const AI_TOOLS = [
7
7
  { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
8
8
  { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
9
+ { name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
9
10
  { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
10
11
  { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
11
12
  { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
12
13
  { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
13
14
  { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
15
+ { name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
14
16
  { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
15
17
  ];
16
18
  //# sourceMappingURL=config.js.map
@@ -0,0 +1,9 @@
1
+ import { SlashCommandConfigurator } from './base.js';
2
+ import { SlashCommandId } from '../../templates/index.js';
3
+ export declare class AmazonQSlashCommandConfigurator extends SlashCommandConfigurator {
4
+ readonly toolId = "amazon-q";
5
+ readonly isAvailable = true;
6
+ protected getRelativePath(id: SlashCommandId): string;
7
+ protected getFrontmatter(id: SlashCommandId): string;
8
+ }
9
+ //# sourceMappingURL=amazon-q.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { SlashCommandConfigurator } from './base.js';
2
+ const FILE_PATHS = {
3
+ proposal: '.amazonq/prompts/openspec-proposal.md',
4
+ apply: '.amazonq/prompts/openspec-apply.md',
5
+ archive: '.amazonq/prompts/openspec-archive.md'
6
+ };
7
+ const FRONTMATTER = {
8
+ proposal: `---
9
+ description: Scaffold a new OpenSpec change and validate strictly.
10
+ ---
11
+
12
+ The user has requested the following change proposal. Use the openspec instructions to create their change proposal.
13
+
14
+ <UserRequest>
15
+ $ARGUMENTS
16
+ </UserRequest>`,
17
+ apply: `---
18
+ description: Implement an approved OpenSpec change and keep tasks in sync.
19
+ ---
20
+
21
+ The user wants to apply the following change. Use the openspec instructions to implement the approved change.
22
+
23
+ <ChangeId>
24
+ $ARGUMENTS
25
+ </ChangeId>`,
26
+ archive: `---
27
+ description: Archive a deployed OpenSpec change and update specs.
28
+ ---
29
+
30
+ The user wants to archive the following deployed change. Use the openspec instructions to archive the change and update specs.
31
+
32
+ <ChangeId>
33
+ $ARGUMENTS
34
+ </ChangeId>`
35
+ };
36
+ export class AmazonQSlashCommandConfigurator extends SlashCommandConfigurator {
37
+ toolId = 'amazon-q';
38
+ isAvailable = true;
39
+ getRelativePath(id) {
40
+ return FILE_PATHS[id];
41
+ }
42
+ getFrontmatter(id) {
43
+ return FRONTMATTER[id];
44
+ }
45
+ }
46
+ //# sourceMappingURL=amazon-q.js.map
@@ -12,6 +12,7 @@ export declare abstract class SlashCommandConfigurator {
12
12
  updateExisting(projectPath: string, _openspecDir: string): Promise<string[]>;
13
13
  protected abstract getRelativePath(id: SlashCommandId): string;
14
14
  protected abstract getFrontmatter(id: SlashCommandId): string | undefined;
15
+ protected getBody(id: SlashCommandId): string;
15
16
  resolveAbsolutePath(projectPath: string, id: SlashCommandId): string;
16
17
  protected updateBody(filePath: string, body: string): Promise<void>;
17
18
  }
@@ -13,7 +13,7 @@ export class SlashCommandConfigurator {
13
13
  async generateAll(projectPath, _openspecDir) {
14
14
  const createdOrUpdated = [];
15
15
  for (const target of this.getTargets()) {
16
- const body = TemplateManager.getSlashCommandBody(target.id).trim();
16
+ const body = this.getBody(target.id);
17
17
  const filePath = FileSystemUtils.joinPath(projectPath, target.path);
18
18
  if (await FileSystemUtils.fileExists(filePath)) {
19
19
  await this.updateBody(filePath, body);
@@ -37,13 +37,16 @@ export class SlashCommandConfigurator {
37
37
  for (const target of this.getTargets()) {
38
38
  const filePath = FileSystemUtils.joinPath(projectPath, target.path);
39
39
  if (await FileSystemUtils.fileExists(filePath)) {
40
- const body = TemplateManager.getSlashCommandBody(target.id).trim();
40
+ const body = this.getBody(target.id);
41
41
  await this.updateBody(filePath, body);
42
42
  updated.push(target.path);
43
43
  }
44
44
  }
45
45
  return updated;
46
46
  }
47
+ getBody(id) {
48
+ return TemplateManager.getSlashCommandBody(id).trim();
49
+ }
47
50
  // Resolve absolute path for a given slash command target. Subclasses may override
48
51
  // to redirect to tool-specific locations (e.g., global directories).
49
52
  resolveAbsolutePath(projectPath, id) {
@@ -0,0 +1,10 @@
1
+ import { SlashCommandConfigurator } from './base.js';
2
+ import { SlashCommandId } from '../../templates/index.js';
3
+ export declare class FactorySlashCommandConfigurator extends SlashCommandConfigurator {
4
+ readonly toolId = "factory";
5
+ readonly isAvailable = true;
6
+ protected getRelativePath(id: SlashCommandId): string;
7
+ protected getFrontmatter(id: SlashCommandId): string;
8
+ protected getBody(id: SlashCommandId): string;
9
+ }
10
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { SlashCommandConfigurator } from './base.js';
2
+ const FILE_PATHS = {
3
+ proposal: '.factory/commands/openspec-proposal.md',
4
+ apply: '.factory/commands/openspec-apply.md',
5
+ archive: '.factory/commands/openspec-archive.md'
6
+ };
7
+ const FRONTMATTER = {
8
+ proposal: `---
9
+ description: Scaffold a new OpenSpec change and validate strictly.
10
+ argument-hint: request or feature description
11
+ ---`,
12
+ apply: `---
13
+ description: Implement an approved OpenSpec change and keep tasks in sync.
14
+ argument-hint: change-id
15
+ ---`,
16
+ archive: `---
17
+ description: Archive a deployed OpenSpec change and update specs.
18
+ argument-hint: change-id
19
+ ---`
20
+ };
21
+ export class FactorySlashCommandConfigurator extends SlashCommandConfigurator {
22
+ toolId = 'factory';
23
+ isAvailable = true;
24
+ getRelativePath(id) {
25
+ return FILE_PATHS[id];
26
+ }
27
+ getFrontmatter(id) {
28
+ return FRONTMATTER[id];
29
+ }
30
+ getBody(id) {
31
+ const baseBody = super.getBody(id);
32
+ return `${baseBody}\n\n$ARGUMENTS`;
33
+ }
34
+ }
35
+ //# sourceMappingURL=factory.js.map
@@ -5,6 +5,8 @@ import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
5
5
  import { OpenCodeSlashCommandConfigurator } from './opencode.js';
6
6
  import { CodexSlashCommandConfigurator } from './codex.js';
7
7
  import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
8
+ import { AmazonQSlashCommandConfigurator } from './amazon-q.js';
9
+ import { FactorySlashCommandConfigurator } from './factory.js';
8
10
  export class SlashCommandRegistry {
9
11
  static configurators = new Map();
10
12
  static {
@@ -15,6 +17,8 @@ export class SlashCommandRegistry {
15
17
  const opencode = new OpenCodeSlashCommandConfigurator();
16
18
  const codex = new CodexSlashCommandConfigurator();
17
19
  const githubCopilot = new GitHubCopilotSlashCommandConfigurator();
20
+ const amazonQ = new AmazonQSlashCommandConfigurator();
21
+ const factory = new FactorySlashCommandConfigurator();
18
22
  this.configurators.set(claude.toolId, claude);
19
23
  this.configurators.set(cursor.toolId, cursor);
20
24
  this.configurators.set(windsurf.toolId, windsurf);
@@ -22,6 +26,8 @@ export class SlashCommandRegistry {
22
26
  this.configurators.set(opencode.toolId, opencode);
23
27
  this.configurators.set(codex.toolId, codex);
24
28
  this.configurators.set(githubCopilot.toolId, githubCopilot);
29
+ this.configurators.set(amazonQ.toolId, amazonQ);
30
+ this.configurators.set(factory.toolId, factory);
25
31
  }
26
32
  static register(configurator) {
27
33
  this.configurators.set(configurator.toolId, configurator);
@@ -23,13 +23,17 @@ type ToolWizardConfig = {
23
23
  type ToolSelectionPrompt = (config: ToolWizardConfig) => Promise<string[]>;
24
24
  type InitCommandOptions = {
25
25
  prompt?: ToolSelectionPrompt;
26
+ tools?: string;
26
27
  };
27
28
  export declare class InitCommand {
28
29
  private readonly prompt;
30
+ private readonly toolsArg?;
29
31
  constructor(options?: InitCommandOptions);
30
32
  execute(targetPath: string): Promise<void>;
31
33
  private validate;
32
34
  private getConfiguration;
35
+ private getSelectedTools;
36
+ private resolveToolsArg;
33
37
  private promptForAITools;
34
38
  private getExistingToolStates;
35
39
  private isToolConfigured;
package/dist/core/init.js CHANGED
@@ -243,8 +243,10 @@ const toolSelectionWizard = createPrompt((config, done) => {
243
243
  });
244
244
  export class InitCommand {
245
245
  prompt;
246
+ toolsArg;
246
247
  constructor(options = {}) {
247
248
  this.prompt = options.prompt ?? ((config) => toolSelectionWizard(config));
249
+ this.toolsArg = options.tools;
248
250
  }
249
251
  async execute(targetPath) {
250
252
  const projectPath = path.resolve(targetPath);
@@ -295,9 +297,59 @@ export class InitCommand {
295
297
  return extendMode;
296
298
  }
297
299
  async getConfiguration(existingTools, extendMode) {
298
- const selectedTools = await this.promptForAITools(existingTools, extendMode);
300
+ const selectedTools = await this.getSelectedTools(existingTools, extendMode);
299
301
  return { aiTools: selectedTools };
300
302
  }
303
+ async getSelectedTools(existingTools, extendMode) {
304
+ const nonInteractiveSelection = this.resolveToolsArg();
305
+ if (nonInteractiveSelection !== null) {
306
+ return nonInteractiveSelection;
307
+ }
308
+ // Fall back to interactive mode
309
+ return this.promptForAITools(existingTools, extendMode);
310
+ }
311
+ resolveToolsArg() {
312
+ if (typeof this.toolsArg === 'undefined') {
313
+ return null;
314
+ }
315
+ const raw = this.toolsArg.trim();
316
+ if (raw.length === 0) {
317
+ throw new Error('The --tools option requires a value. Use "all", "none", or a comma-separated list of tool IDs.');
318
+ }
319
+ const availableTools = AI_TOOLS.filter((tool) => tool.available);
320
+ const availableValues = availableTools.map((tool) => tool.value);
321
+ const availableSet = new Set(availableValues);
322
+ const availableList = ['all', 'none', ...availableValues].join(', ');
323
+ const lowerRaw = raw.toLowerCase();
324
+ if (lowerRaw === 'all') {
325
+ return availableValues;
326
+ }
327
+ if (lowerRaw === 'none') {
328
+ return [];
329
+ }
330
+ const tokens = raw
331
+ .split(',')
332
+ .map((token) => token.trim())
333
+ .filter((token) => token.length > 0);
334
+ if (tokens.length === 0) {
335
+ throw new Error('The --tools option requires at least one tool ID when not using "all" or "none".');
336
+ }
337
+ const normalizedTokens = tokens.map((token) => token.toLowerCase());
338
+ if (normalizedTokens.some((token) => token === 'all' || token === 'none')) {
339
+ throw new Error('Cannot combine reserved values "all" or "none" with specific tool IDs.');
340
+ }
341
+ const invalidTokens = tokens.filter((_token, index) => !availableSet.has(normalizedTokens[index]));
342
+ if (invalidTokens.length > 0) {
343
+ throw new Error(`Invalid tool(s): ${invalidTokens.join(', ')}. Available values: ${availableList}`);
344
+ }
345
+ const deduped = [];
346
+ for (const token of normalizedTokens) {
347
+ if (!deduped.includes(token)) {
348
+ deduped.push(token);
349
+ }
350
+ }
351
+ return deduped;
352
+ }
301
353
  async promptForAITools(existingTools, extendMode) {
302
354
  const availableTools = AI_TOOLS.filter((tool) => tool.available);
303
355
  const baseMessage = extendMode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fission-ai/openspec",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",