@fission-ai/openspec 0.11.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 +1 -0
- package/dist/cli/index.js +8 -2
- package/dist/core/config.js +1 -0
- package/dist/core/configurators/slash/base.d.ts +1 -0
- package/dist/core/configurators/slash/base.js +5 -2
- package/dist/core/configurators/slash/factory.d.ts +10 -0
- package/dist/core/configurators/slash/factory.js +35 -0
- package/dist/core/configurators/slash/registry.js +3 -0
- package/dist/core/init.d.ts +4 -0
- package/dist/core/init.js +53 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ 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/`) |
|
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
|
-
.
|
|
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) {
|
package/dist/core/config.js
CHANGED
|
@@ -6,6 +6,7 @@ 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,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 =
|
|
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 =
|
|
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
|
|
@@ -6,6 +6,7 @@ import { OpenCodeSlashCommandConfigurator } from './opencode.js';
|
|
|
6
6
|
import { CodexSlashCommandConfigurator } from './codex.js';
|
|
7
7
|
import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
|
|
8
8
|
import { AmazonQSlashCommandConfigurator } from './amazon-q.js';
|
|
9
|
+
import { FactorySlashCommandConfigurator } from './factory.js';
|
|
9
10
|
export class SlashCommandRegistry {
|
|
10
11
|
static configurators = new Map();
|
|
11
12
|
static {
|
|
@@ -17,6 +18,7 @@ export class SlashCommandRegistry {
|
|
|
17
18
|
const codex = new CodexSlashCommandConfigurator();
|
|
18
19
|
const githubCopilot = new GitHubCopilotSlashCommandConfigurator();
|
|
19
20
|
const amazonQ = new AmazonQSlashCommandConfigurator();
|
|
21
|
+
const factory = new FactorySlashCommandConfigurator();
|
|
20
22
|
this.configurators.set(claude.toolId, claude);
|
|
21
23
|
this.configurators.set(cursor.toolId, cursor);
|
|
22
24
|
this.configurators.set(windsurf.toolId, windsurf);
|
|
@@ -25,6 +27,7 @@ export class SlashCommandRegistry {
|
|
|
25
27
|
this.configurators.set(codex.toolId, codex);
|
|
26
28
|
this.configurators.set(githubCopilot.toolId, githubCopilot);
|
|
27
29
|
this.configurators.set(amazonQ.toolId, amazonQ);
|
|
30
|
+
this.configurators.set(factory.toolId, factory);
|
|
28
31
|
}
|
|
29
32
|
static register(configurator) {
|
|
30
33
|
this.configurators.set(configurator.toolId, configurator);
|
package/dist/core/init.d.ts
CHANGED
|
@@ -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.
|
|
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
|