@fission-ai/openspec 0.23.0 → 1.0.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 +111 -382
- package/dist/cli/index.js +120 -6
- package/dist/commands/workflow/index.d.ts +17 -0
- package/dist/commands/workflow/index.js +12 -0
- package/dist/commands/workflow/instructions.d.ts +29 -0
- package/dist/commands/workflow/instructions.js +381 -0
- package/dist/commands/workflow/new-change.d.ts +11 -0
- package/dist/commands/workflow/new-change.js +44 -0
- package/dist/commands/workflow/schemas.d.ts +10 -0
- package/dist/commands/workflow/schemas.js +34 -0
- package/dist/commands/workflow/shared.d.ts +52 -0
- package/dist/commands/workflow/shared.js +111 -0
- package/dist/commands/workflow/status.d.ts +14 -0
- package/dist/commands/workflow/status.js +58 -0
- package/dist/commands/workflow/templates.d.ts +16 -0
- package/dist/commands/workflow/templates.js +68 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +5 -1
- package/dist/core/artifact-graph/instruction-loader.js +8 -19
- package/dist/core/command-generation/adapters/amazon-q.d.ts +13 -0
- package/dist/core/command-generation/adapters/amazon-q.js +26 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +26 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +27 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +50 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +28 -0
- package/dist/core/command-generation/adapters/codex.d.ts +13 -0
- package/dist/core/command-generation/adapters/codex.js +27 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +28 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +27 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +44 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +27 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +26 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +26 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +29 -0
- package/dist/core/command-generation/adapters/index.d.ts +27 -0
- package/dist/core/command-generation/adapters/index.js +27 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +13 -0
- package/dist/core/command-generation/adapters/opencode.js +26 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +13 -0
- package/dist/core/command-generation/adapters/qwen.js +26 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- package/dist/core/command-generation/adapters/windsurf.d.ts +14 -0
- package/dist/core/command-generation/adapters/windsurf.js +51 -0
- package/dist/core/command-generation/generator.d.ts +21 -0
- package/dist/core/command-generation/generator.js +27 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- package/dist/core/command-generation/registry.d.ts +36 -0
- package/dist/core/command-generation/registry.js +88 -0
- package/dist/core/command-generation/types.d.ts +55 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +21 -21
- package/dist/core/init.d.ts +16 -36
- package/dist/core/init.js +323 -534
- package/dist/core/legacy-cleanup.d.ts +162 -0
- package/dist/core/legacy-cleanup.js +501 -0
- package/dist/core/shared/index.d.ts +8 -0
- package/dist/core/shared/index.js +8 -0
- package/dist/core/shared/skill-generation.d.ts +41 -0
- package/dist/core/shared/skill-generation.js +76 -0
- package/dist/core/shared/tool-detection.d.ts +66 -0
- package/dist/core/shared/tool-detection.js +140 -0
- package/dist/core/templates/index.d.ts +7 -16
- package/dist/core/templates/index.js +8 -36
- package/dist/core/templates/skill-templates.d.ts +13 -0
- package/dist/core/templates/skill-templates.js +627 -21
- package/dist/core/update.d.ts +38 -0
- package/dist/core/update.js +280 -62
- package/dist/prompts/searchable-multi-select.d.ts +27 -0
- package/dist/prompts/searchable-multi-select.js +149 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +10 -0
- package/dist/ui/welcome-screen.js +146 -0
- package/dist/utils/file-system.d.ts +11 -0
- package/dist/utils/file-system.js +65 -2
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +2 -0
- package/package.json +1 -1
- package/dist/commands/artifact-workflow.d.ts +0 -17
- package/dist/commands/artifact-workflow.js +0 -915
- package/dist/core/configurators/agents.d.ts +0 -8
- package/dist/core/configurators/agents.js +0 -15
- package/dist/core/configurators/base.d.ts +0 -7
- package/dist/core/configurators/base.js +0 -2
- package/dist/core/configurators/claude.d.ts +0 -8
- package/dist/core/configurators/claude.js +0 -15
- package/dist/core/configurators/cline.d.ts +0 -8
- package/dist/core/configurators/cline.js +0 -15
- package/dist/core/configurators/codebuddy.d.ts +0 -8
- package/dist/core/configurators/codebuddy.js +0 -15
- package/dist/core/configurators/costrict.d.ts +0 -8
- package/dist/core/configurators/costrict.js +0 -15
- package/dist/core/configurators/iflow.d.ts +0 -8
- package/dist/core/configurators/iflow.js +0 -15
- package/dist/core/configurators/qoder.d.ts +0 -30
- package/dist/core/configurators/qoder.js +0 -42
- package/dist/core/configurators/qwen.d.ts +0 -24
- package/dist/core/configurators/qwen.js +0 -37
- package/dist/core/configurators/registry.d.ts +0 -9
- package/dist/core/configurators/registry.js +0 -43
- package/dist/core/configurators/slash/amazon-q.d.ts +0 -9
- package/dist/core/configurators/slash/amazon-q.js +0 -46
- package/dist/core/configurators/slash/antigravity.d.ts +0 -9
- package/dist/core/configurators/slash/antigravity.js +0 -23
- package/dist/core/configurators/slash/auggie.d.ts +0 -9
- package/dist/core/configurators/slash/auggie.js +0 -31
- package/dist/core/configurators/slash/base.d.ts +0 -19
- package/dist/core/configurators/slash/base.js +0 -69
- package/dist/core/configurators/slash/claude.d.ts +0 -9
- package/dist/core/configurators/slash/claude.js +0 -37
- package/dist/core/configurators/slash/cline.d.ts +0 -9
- package/dist/core/configurators/slash/cline.js +0 -23
- package/dist/core/configurators/slash/codebuddy.d.ts +0 -9
- package/dist/core/configurators/slash/codebuddy.js +0 -34
- package/dist/core/configurators/slash/codex.d.ts +0 -14
- package/dist/core/configurators/slash/codex.js +0 -109
- package/dist/core/configurators/slash/continue.d.ts +0 -9
- package/dist/core/configurators/slash/continue.js +0 -46
- package/dist/core/configurators/slash/costrict.d.ts +0 -9
- package/dist/core/configurators/slash/costrict.js +0 -31
- package/dist/core/configurators/slash/crush.d.ts +0 -9
- package/dist/core/configurators/slash/crush.js +0 -37
- package/dist/core/configurators/slash/cursor.d.ts +0 -9
- package/dist/core/configurators/slash/cursor.js +0 -37
- package/dist/core/configurators/slash/factory.d.ts +0 -10
- package/dist/core/configurators/slash/factory.js +0 -35
- package/dist/core/configurators/slash/gemini.d.ts +0 -9
- package/dist/core/configurators/slash/gemini.js +0 -22
- package/dist/core/configurators/slash/github-copilot.d.ts +0 -9
- package/dist/core/configurators/slash/github-copilot.js +0 -34
- package/dist/core/configurators/slash/iflow.d.ts +0 -9
- package/dist/core/configurators/slash/iflow.js +0 -37
- package/dist/core/configurators/slash/kilocode.d.ts +0 -9
- package/dist/core/configurators/slash/kilocode.js +0 -17
- package/dist/core/configurators/slash/opencode.d.ts +0 -12
- package/dist/core/configurators/slash/opencode.js +0 -72
- package/dist/core/configurators/slash/qoder.d.ts +0 -35
- package/dist/core/configurators/slash/qoder.js +0 -76
- package/dist/core/configurators/slash/qwen.d.ts +0 -32
- package/dist/core/configurators/slash/qwen.js +0 -49
- package/dist/core/configurators/slash/registry.d.ts +0 -8
- package/dist/core/configurators/slash/registry.js +0 -78
- package/dist/core/configurators/slash/roocode.d.ts +0 -9
- package/dist/core/configurators/slash/roocode.js +0 -23
- package/dist/core/configurators/slash/toml-base.d.ts +0 -10
- package/dist/core/configurators/slash/toml-base.js +0 -53
- package/dist/core/configurators/slash/windsurf.d.ts +0 -9
- package/dist/core/configurators/slash/windsurf.js +0 -23
- package/dist/core/templates/agents-root-stub.d.ts +0 -2
- package/dist/core/templates/agents-root-stub.js +0 -17
- package/dist/core/templates/agents-template.d.ts +0 -2
- package/dist/core/templates/agents-template.js +0 -458
- package/dist/core/templates/claude-template.d.ts +0 -2
- package/dist/core/templates/claude-template.js +0 -2
- package/dist/core/templates/cline-template.d.ts +0 -2
- package/dist/core/templates/cline-template.js +0 -2
- package/dist/core/templates/costrict-template.d.ts +0 -2
- package/dist/core/templates/costrict-template.js +0 -2
- package/dist/core/templates/project-template.d.ts +0 -8
- package/dist/core/templates/project-template.js +0 -32
- package/dist/core/templates/slash-command-templates.d.ts +0 -4
- package/dist/core/templates/slash-command-templates.js +0 -49
package/dist/core/update.d.ts
CHANGED
|
@@ -1,4 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Command
|
|
3
|
+
*
|
|
4
|
+
* Refreshes OpenSpec skills and commands for configured tools.
|
|
5
|
+
* Supports smart update detection to skip updates when already current.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Options for the update command.
|
|
9
|
+
*/
|
|
10
|
+
export interface UpdateCommandOptions {
|
|
11
|
+
/** Force update even when tools are up to date */
|
|
12
|
+
force?: boolean;
|
|
13
|
+
}
|
|
1
14
|
export declare class UpdateCommand {
|
|
15
|
+
private readonly force;
|
|
16
|
+
constructor(options?: UpdateCommandOptions);
|
|
2
17
|
execute(projectPath: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Display message when all tools are up to date.
|
|
20
|
+
*/
|
|
21
|
+
private displayUpToDateMessage;
|
|
22
|
+
/**
|
|
23
|
+
* Display the update plan showing which tools need updating.
|
|
24
|
+
*/
|
|
25
|
+
private displayUpdatePlan;
|
|
26
|
+
/**
|
|
27
|
+
* Detect and handle legacy OpenSpec artifacts.
|
|
28
|
+
* Unlike init, update warns but continues if legacy files found in non-interactive mode.
|
|
29
|
+
* Returns array of tool IDs that were newly configured during legacy upgrade.
|
|
30
|
+
*/
|
|
31
|
+
private handleLegacyCleanup;
|
|
32
|
+
/**
|
|
33
|
+
* Perform cleanup of legacy artifacts.
|
|
34
|
+
*/
|
|
35
|
+
private performLegacyCleanup;
|
|
36
|
+
/**
|
|
37
|
+
* Upgrade legacy tools to new skills system.
|
|
38
|
+
* Returns array of tool IDs that were newly configured.
|
|
39
|
+
*/
|
|
40
|
+
private upgradeLegacyTools;
|
|
3
41
|
}
|
|
4
42
|
//# sourceMappingURL=update.d.ts.map
|
package/dist/core/update.js
CHANGED
|
@@ -1,88 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Command
|
|
3
|
+
*
|
|
4
|
+
* Refreshes OpenSpec skills and commands for configured tools.
|
|
5
|
+
* Supports smart update detection to skip updates when already current.
|
|
6
|
+
*/
|
|
1
7
|
import path from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { createRequire } from 'module';
|
|
2
11
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
3
|
-
import { OPENSPEC_DIR_NAME } from './config.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
12
|
+
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
|
13
|
+
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
14
|
+
import { getConfiguredTools, getAllToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
|
|
15
|
+
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, getToolsFromLegacyArtifacts, } from './legacy-cleanup.js';
|
|
16
|
+
import { isInteractive } from '../utils/interactive.js';
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
7
19
|
export class UpdateCommand {
|
|
20
|
+
force;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.force = options.force ?? false;
|
|
23
|
+
}
|
|
8
24
|
async execute(projectPath) {
|
|
9
25
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
10
|
-
const
|
|
11
|
-
const openspecPath = path.join(resolvedProjectPath, openspecDirName);
|
|
26
|
+
const openspecPath = path.join(resolvedProjectPath, OPENSPEC_DIR_NAME);
|
|
12
27
|
// 1. Check openspec directory exists
|
|
13
28
|
if (!await FileSystemUtils.directoryExists(openspecPath)) {
|
|
14
29
|
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
|
|
15
30
|
}
|
|
16
|
-
// 2.
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
// 2. Detect and handle legacy artifacts + upgrade legacy tools to new skills
|
|
32
|
+
const newlyConfiguredTools = await this.handleLegacyCleanup(resolvedProjectPath);
|
|
33
|
+
// 3. Find configured tools
|
|
34
|
+
const configuredTools = getConfiguredTools(resolvedProjectPath);
|
|
35
|
+
if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {
|
|
36
|
+
console.log(chalk.yellow('No configured tools found.'));
|
|
37
|
+
console.log(chalk.dim('Run "openspec init" to set up tools.'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// 4. Check version status for all configured tools
|
|
41
|
+
const toolStatuses = getAllToolVersionStatus(resolvedProjectPath, OPENSPEC_VERSION);
|
|
42
|
+
// 5. Smart update detection
|
|
43
|
+
const toolsNeedingUpdate = toolStatuses.filter((s) => s.needsUpdate);
|
|
44
|
+
const toolsUpToDate = toolStatuses.filter((s) => !s.needsUpdate);
|
|
45
|
+
if (!this.force && toolsNeedingUpdate.length === 0) {
|
|
46
|
+
// All tools are up to date
|
|
47
|
+
this.displayUpToDateMessage(toolStatuses);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// 6. Display update plan
|
|
51
|
+
if (this.force) {
|
|
52
|
+
console.log(`Force updating ${configuredTools.length} tool(s): ${configuredTools.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
this.displayUpdatePlan(toolsNeedingUpdate, toolsUpToDate);
|
|
56
|
+
}
|
|
57
|
+
console.log();
|
|
58
|
+
// 7. Prepare templates
|
|
59
|
+
const skillTemplates = getSkillTemplates();
|
|
60
|
+
const commandContents = getCommandContents();
|
|
61
|
+
// 8. Update tools (all if force, otherwise only those needing update)
|
|
62
|
+
const toolsToUpdate = this.force ? configuredTools : toolsNeedingUpdate.map((s) => s.toolId);
|
|
63
|
+
const updatedTools = [];
|
|
64
|
+
const failedTools = [];
|
|
65
|
+
for (const toolId of toolsToUpdate) {
|
|
66
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
67
|
+
if (!tool?.skillsDir)
|
|
32
68
|
continue;
|
|
33
|
-
}
|
|
69
|
+
const spinner = ora(`Updating ${tool.name}...`).start();
|
|
34
70
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
71
|
+
const skillsDir = path.join(resolvedProjectPath, tool.skillsDir, 'skills');
|
|
72
|
+
// Update skill files
|
|
73
|
+
for (const { template, dirName } of skillTemplates) {
|
|
74
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
75
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
76
|
+
const skillContent = generateSkillContent(template, OPENSPEC_VERSION);
|
|
77
|
+
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
37
78
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
79
|
+
// Update commands
|
|
80
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
81
|
+
if (adapter) {
|
|
82
|
+
const generatedCommands = generateCommands(commandContents, adapter);
|
|
83
|
+
for (const cmd of generatedCommands) {
|
|
84
|
+
const commandFile = path.join(resolvedProjectPath, cmd.path);
|
|
85
|
+
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
86
|
+
}
|
|
42
87
|
}
|
|
88
|
+
spinner.succeed(`Updated ${tool.name}`);
|
|
89
|
+
updatedTools.push(tool.name);
|
|
43
90
|
}
|
|
44
91
|
catch (error) {
|
|
45
|
-
|
|
46
|
-
|
|
92
|
+
spinner.fail(`Failed to update ${tool.name}`);
|
|
93
|
+
failedTools.push({
|
|
94
|
+
name: tool.name,
|
|
95
|
+
error: error instanceof Error ? error.message : String(error)
|
|
96
|
+
});
|
|
47
97
|
}
|
|
48
98
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
99
|
+
// 9. Summary
|
|
100
|
+
console.log();
|
|
101
|
+
if (updatedTools.length > 0) {
|
|
102
|
+
console.log(chalk.green(`✓ Updated: ${updatedTools.join(', ')} (v${OPENSPEC_VERSION})`));
|
|
103
|
+
}
|
|
104
|
+
if (failedTools.length > 0) {
|
|
105
|
+
console.log(chalk.red(`✗ Failed: ${failedTools.map(f => `${f.name} (${f.error})`).join(', ')}`));
|
|
106
|
+
}
|
|
107
|
+
// 10. Show onboarding message for newly configured tools from legacy upgrade
|
|
108
|
+
if (newlyConfiguredTools.length > 0) {
|
|
109
|
+
console.log();
|
|
110
|
+
console.log(chalk.bold('Getting started:'));
|
|
111
|
+
console.log(' /opsx:new Start a new change');
|
|
112
|
+
console.log(' /opsx:continue Create the next artifact');
|
|
113
|
+
console.log(' /opsx:apply Implement tasks');
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
116
|
+
}
|
|
117
|
+
console.log();
|
|
118
|
+
console.log(chalk.dim('Restart your IDE for changes to take effect.'));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Display message when all tools are up to date.
|
|
122
|
+
*/
|
|
123
|
+
displayUpToDateMessage(toolStatuses) {
|
|
124
|
+
const toolNames = toolStatuses.map((s) => s.toolId);
|
|
125
|
+
console.log(chalk.green(`✓ All ${toolStatuses.length} tool(s) up to date (v${OPENSPEC_VERSION})`));
|
|
126
|
+
console.log(chalk.dim(` Tools: ${toolNames.join(', ')}`));
|
|
127
|
+
console.log();
|
|
128
|
+
console.log(chalk.dim('Use --force to refresh skills anyway.'));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Display the update plan showing which tools need updating.
|
|
132
|
+
*/
|
|
133
|
+
displayUpdatePlan(needingUpdate, upToDate) {
|
|
134
|
+
const updates = needingUpdate.map((s) => {
|
|
135
|
+
const fromVersion = s.generatedByVersion ?? 'unknown';
|
|
136
|
+
return `${s.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`;
|
|
137
|
+
});
|
|
138
|
+
console.log(`Updating ${needingUpdate.length} tool(s): ${updates.join(', ')}`);
|
|
139
|
+
if (upToDate.length > 0) {
|
|
140
|
+
const upToDateNames = upToDate.map((s) => s.toolId);
|
|
141
|
+
console.log(chalk.dim(`Already up to date: ${upToDateNames.join(', ')}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Detect and handle legacy OpenSpec artifacts.
|
|
146
|
+
* Unlike init, update warns but continues if legacy files found in non-interactive mode.
|
|
147
|
+
* Returns array of tool IDs that were newly configured during legacy upgrade.
|
|
148
|
+
*/
|
|
149
|
+
async handleLegacyCleanup(projectPath) {
|
|
150
|
+
// Detect legacy artifacts
|
|
151
|
+
const detection = await detectLegacyArtifacts(projectPath);
|
|
152
|
+
if (!detection.hasLegacyArtifacts) {
|
|
153
|
+
return []; // No legacy artifacts found
|
|
154
|
+
}
|
|
155
|
+
// Show what was detected
|
|
156
|
+
console.log();
|
|
157
|
+
console.log(formatDetectionSummary(detection));
|
|
158
|
+
console.log();
|
|
159
|
+
const canPrompt = isInteractive();
|
|
160
|
+
if (this.force) {
|
|
161
|
+
// --force flag: proceed with cleanup automatically
|
|
162
|
+
await this.performLegacyCleanup(projectPath, detection);
|
|
163
|
+
// Then upgrade legacy tools to new skills
|
|
164
|
+
return this.upgradeLegacyTools(projectPath, detection, canPrompt);
|
|
165
|
+
}
|
|
166
|
+
if (!canPrompt) {
|
|
167
|
+
// Non-interactive mode without --force: warn and continue
|
|
168
|
+
// (Unlike init, update doesn't abort - user may just want to update skills)
|
|
169
|
+
console.log(chalk.yellow('⚠ Run with --force to auto-cleanup legacy files, or run interactively.'));
|
|
170
|
+
console.log();
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
// Interactive mode: prompt for confirmation
|
|
174
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
175
|
+
const shouldCleanup = await confirm({
|
|
176
|
+
message: 'Upgrade and clean up legacy files?',
|
|
177
|
+
default: true,
|
|
178
|
+
});
|
|
179
|
+
if (shouldCleanup) {
|
|
180
|
+
await this.performLegacyCleanup(projectPath, detection);
|
|
181
|
+
// Then upgrade legacy tools to new skills
|
|
182
|
+
return this.upgradeLegacyTools(projectPath, detection, canPrompt);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.log(chalk.dim('Skipping legacy cleanup. Continuing with skill update...'));
|
|
186
|
+
console.log();
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Perform cleanup of legacy artifacts.
|
|
192
|
+
*/
|
|
193
|
+
async performLegacyCleanup(projectPath, detection) {
|
|
194
|
+
const spinner = ora('Cleaning up legacy files...').start();
|
|
195
|
+
const result = await cleanupLegacyArtifacts(projectPath, detection);
|
|
196
|
+
spinner.succeed('Legacy files cleaned up');
|
|
197
|
+
const summary = formatCleanupSummary(result);
|
|
198
|
+
if (summary) {
|
|
199
|
+
console.log();
|
|
200
|
+
console.log(summary);
|
|
201
|
+
}
|
|
202
|
+
console.log();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Upgrade legacy tools to new skills system.
|
|
206
|
+
* Returns array of tool IDs that were newly configured.
|
|
207
|
+
*/
|
|
208
|
+
async upgradeLegacyTools(projectPath, detection, canPrompt) {
|
|
209
|
+
// Get tools that had legacy artifacts
|
|
210
|
+
const legacyTools = getToolsFromLegacyArtifacts(detection);
|
|
211
|
+
if (legacyTools.length === 0) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
// Get currently configured tools
|
|
215
|
+
const configuredTools = getConfiguredTools(projectPath);
|
|
216
|
+
const configuredSet = new Set(configuredTools);
|
|
217
|
+
// Filter to tools that aren't already configured
|
|
218
|
+
const unconfiguredLegacyTools = legacyTools.filter((t) => !configuredSet.has(t));
|
|
219
|
+
if (unconfiguredLegacyTools.length === 0) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
// Get valid tools (those with skillsDir)
|
|
223
|
+
const validToolIds = new Set(getToolsWithSkillsDir());
|
|
224
|
+
const validUnconfiguredTools = unconfiguredLegacyTools.filter((t) => validToolIds.has(t));
|
|
225
|
+
if (validUnconfiguredTools.length === 0) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
// Show what tools were detected from legacy artifacts
|
|
229
|
+
console.log(chalk.bold('Tools detected from legacy artifacts:'));
|
|
230
|
+
for (const toolId of validUnconfiguredTools) {
|
|
231
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
232
|
+
console.log(` • ${tool?.name || toolId}`);
|
|
233
|
+
}
|
|
234
|
+
console.log();
|
|
235
|
+
let selectedTools;
|
|
236
|
+
if (this.force || !canPrompt) {
|
|
237
|
+
// Non-interactive with --force: auto-select detected tools
|
|
238
|
+
selectedTools = validUnconfiguredTools;
|
|
239
|
+
console.log(`Setting up skills for: ${selectedTools.join(', ')}`);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Interactive mode: prompt for tool selection with detected tools pre-selected
|
|
243
|
+
const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js');
|
|
244
|
+
const sortedChoices = validUnconfiguredTools.map((toolId) => {
|
|
245
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
246
|
+
return {
|
|
247
|
+
name: tool?.name || toolId,
|
|
248
|
+
value: toolId,
|
|
249
|
+
configured: false,
|
|
250
|
+
preSelected: true, // Pre-select all detected legacy tools
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
selectedTools = await searchableMultiSelect({
|
|
254
|
+
message: 'Select tools to set up with the new skill system:',
|
|
255
|
+
pageSize: 15,
|
|
256
|
+
choices: sortedChoices,
|
|
257
|
+
validate: (_selected) => true, // Allow empty selection (user can skip)
|
|
258
|
+
});
|
|
259
|
+
if (selectedTools.length === 0) {
|
|
260
|
+
console.log(chalk.dim('Skipping tool setup.'));
|
|
261
|
+
console.log();
|
|
262
|
+
return [];
|
|
52
263
|
}
|
|
264
|
+
}
|
|
265
|
+
// Create skills for selected tools
|
|
266
|
+
const newlyConfigured = [];
|
|
267
|
+
const skillTemplates = getSkillTemplates();
|
|
268
|
+
const commandContents = getCommandContents();
|
|
269
|
+
for (const toolId of selectedTools) {
|
|
270
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
271
|
+
if (!tool?.skillsDir)
|
|
272
|
+
continue;
|
|
273
|
+
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
53
274
|
try {
|
|
54
|
-
const
|
|
55
|
-
|
|
275
|
+
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
276
|
+
// Create skill files
|
|
277
|
+
for (const { template, dirName } of skillTemplates) {
|
|
278
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
279
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
280
|
+
const skillContent = generateSkillContent(template, OPENSPEC_VERSION);
|
|
281
|
+
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
282
|
+
}
|
|
283
|
+
// Create commands
|
|
284
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
285
|
+
if (adapter) {
|
|
286
|
+
const generatedCommands = generateCommands(commandContents, adapter);
|
|
287
|
+
for (const cmd of generatedCommands) {
|
|
288
|
+
const commandFile = path.join(projectPath, cmd.path);
|
|
289
|
+
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
293
|
+
newlyConfigured.push(toolId);
|
|
56
294
|
}
|
|
57
295
|
catch (error) {
|
|
58
|
-
|
|
59
|
-
console.
|
|
296
|
+
spinner.fail(`Failed to set up ${tool.name}`);
|
|
297
|
+
console.log(chalk.red(` ${error instanceof Error ? error.message : String(error)}`));
|
|
60
298
|
}
|
|
61
299
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
summaryParts.push(`Updated OpenSpec instructions (${instructionFiles.join(', ')})`);
|
|
68
|
-
const aiToolFiles = updatedFiles.filter((file) => file !== 'AGENTS.md');
|
|
69
|
-
if (aiToolFiles.length > 0) {
|
|
70
|
-
summaryParts.push(`Updated AI tool files: ${aiToolFiles.join(', ')}`);
|
|
71
|
-
}
|
|
72
|
-
if (updatedSlashFiles.length > 0) {
|
|
73
|
-
// Normalize to forward slashes for cross-platform log consistency
|
|
74
|
-
const normalized = updatedSlashFiles.map((p) => FileSystemUtils.toPosixPath(p));
|
|
75
|
-
summaryParts.push(`Updated slash commands: ${normalized.join(', ')}`);
|
|
76
|
-
}
|
|
77
|
-
const failedItems = [
|
|
78
|
-
...failedFiles,
|
|
79
|
-
...failedSlashTools.map((toolId) => `slash command refresh (${toolId})`),
|
|
80
|
-
];
|
|
81
|
-
if (failedItems.length > 0) {
|
|
82
|
-
summaryParts.push(`Failed to update: ${failedItems.join(', ')}`);
|
|
83
|
-
}
|
|
84
|
-
console.log(summaryParts.join(' | '));
|
|
85
|
-
// No additional notes
|
|
300
|
+
if (newlyConfigured.length > 0) {
|
|
301
|
+
console.log();
|
|
302
|
+
}
|
|
303
|
+
return newlyConfigured;
|
|
86
304
|
}
|
|
87
305
|
}
|
|
88
306
|
//# sourceMappingURL=update.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
interface Choice {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
configured?: boolean;
|
|
6
|
+
configuredLabel?: string;
|
|
7
|
+
preSelected?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface Config {
|
|
10
|
+
message: string;
|
|
11
|
+
choices: Choice[];
|
|
12
|
+
pageSize?: number;
|
|
13
|
+
validate?: (selected: string[]) => boolean | string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A searchable multi-select prompt with visible search box,
|
|
17
|
+
* selected items display, and intuitive keyboard navigation.
|
|
18
|
+
*
|
|
19
|
+
* - Type to filter choices
|
|
20
|
+
* - ↑↓ to navigate
|
|
21
|
+
* - Enter to add highlighted item
|
|
22
|
+
* - Backspace to remove last selected item (or delete search char)
|
|
23
|
+
* - Tab to confirm selections
|
|
24
|
+
*/
|
|
25
|
+
export declare function searchableMultiSelect(config: Config): Promise<string[]>;
|
|
26
|
+
export default searchableMultiSelect;
|
|
27
|
+
//# sourceMappingURL=searchable-multi-select.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
/**
|
|
3
|
+
* Create the searchable multi-select prompt.
|
|
4
|
+
* Uses dynamic import to prevent pre-commit hook hangs (see #367).
|
|
5
|
+
*/
|
|
6
|
+
async function createSearchableMultiSelect() {
|
|
7
|
+
const { createPrompt, useState, useKeypress, useMemo, usePrefix, isEnterKey, isBackspaceKey, isUpKey, isDownKey, } = await import('@inquirer/core');
|
|
8
|
+
return createPrompt((config, done) => {
|
|
9
|
+
const { message, choices, pageSize = 15, validate } = config;
|
|
10
|
+
const [searchText, setSearchText] = useState('');
|
|
11
|
+
const [selectedValues, setSelectedValues] = useState(() => choices.filter(c => c.preSelected).map(c => c.value));
|
|
12
|
+
const [cursor, setCursor] = useState(0);
|
|
13
|
+
const [status, setStatus] = useState('idle');
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
const prefix = usePrefix({ status });
|
|
16
|
+
// Filter choices by search
|
|
17
|
+
const filteredChoices = useMemo(() => {
|
|
18
|
+
if (!searchText.trim())
|
|
19
|
+
return choices;
|
|
20
|
+
const term = searchText.toLowerCase();
|
|
21
|
+
return choices.filter((c) => c.name.toLowerCase().includes(term) ||
|
|
22
|
+
c.value.toLowerCase().includes(term));
|
|
23
|
+
}, [searchText, choices]);
|
|
24
|
+
const selectedSet = useMemo(() => new Set(selectedValues), [selectedValues]);
|
|
25
|
+
const choiceMap = useMemo(() => new Map(choices.map((c) => [c.value, c])), [choices]);
|
|
26
|
+
useKeypress((key) => {
|
|
27
|
+
if (status === 'done')
|
|
28
|
+
return;
|
|
29
|
+
// Tab to confirm
|
|
30
|
+
if (key.name === 'tab') {
|
|
31
|
+
if (validate) {
|
|
32
|
+
const result = validate(selectedValues);
|
|
33
|
+
if (result !== true) {
|
|
34
|
+
setError(typeof result === 'string' ? result : 'Invalid');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
setStatus('done');
|
|
39
|
+
done(selectedValues);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Enter to add item
|
|
43
|
+
if (isEnterKey(key)) {
|
|
44
|
+
const choice = filteredChoices[cursor];
|
|
45
|
+
if (choice && !selectedSet.has(choice.value)) {
|
|
46
|
+
setSelectedValues([...selectedValues, choice.value]);
|
|
47
|
+
setSearchText('');
|
|
48
|
+
setCursor(0);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Backspace to remove or delete search char
|
|
53
|
+
if (isBackspaceKey(key)) {
|
|
54
|
+
if (searchText === '' && selectedValues.length > 0) {
|
|
55
|
+
setSelectedValues(selectedValues.slice(0, -1));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
setSearchText(searchText.slice(0, -1));
|
|
59
|
+
setCursor(0);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Navigation
|
|
64
|
+
if (isUpKey(key)) {
|
|
65
|
+
setCursor(Math.max(0, cursor - 1));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (isDownKey(key)) {
|
|
69
|
+
setCursor(Math.min(filteredChoices.length - 1, cursor + 1));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Character input - handle printable characters
|
|
73
|
+
if (key.name && key.name.length === 1 && !key.ctrl) {
|
|
74
|
+
setSearchText(searchText + key.name);
|
|
75
|
+
setCursor(0);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// Render done state
|
|
79
|
+
if (status === 'done') {
|
|
80
|
+
const names = selectedValues
|
|
81
|
+
.map((v) => choiceMap.get(v)?.name ?? v)
|
|
82
|
+
.join(', ');
|
|
83
|
+
return `${prefix} ${chalk.bold(message)} ${chalk.cyan(names || '(none)')}`;
|
|
84
|
+
}
|
|
85
|
+
// Render active state
|
|
86
|
+
const lines = [];
|
|
87
|
+
lines.push(`${prefix} ${chalk.bold(message)}`);
|
|
88
|
+
// Selected chips
|
|
89
|
+
const chips = selectedValues.length > 0
|
|
90
|
+
? selectedValues
|
|
91
|
+
.map((v) => chalk.bgCyan.black(` ${choiceMap.get(v)?.name} `))
|
|
92
|
+
.join(' ')
|
|
93
|
+
: chalk.dim('(none selected)');
|
|
94
|
+
lines.push(` Selected: ${chips}`);
|
|
95
|
+
// Search box
|
|
96
|
+
lines.push(` Search: ${chalk.yellow('[')}${searchText || chalk.dim('type to filter')}${chalk.yellow(']')}`);
|
|
97
|
+
// Instructions
|
|
98
|
+
lines.push(` ${chalk.cyan('↑↓')} navigate • ${chalk.cyan('Enter')} add • ${chalk.cyan('Backspace')} remove • ${chalk.cyan('Tab')} confirm`);
|
|
99
|
+
// List
|
|
100
|
+
if (filteredChoices.length === 0) {
|
|
101
|
+
lines.push(chalk.yellow(' No matches'));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Calculate pagination
|
|
105
|
+
const startIndex = Math.max(0, Math.min(cursor - Math.floor(pageSize / 2), filteredChoices.length - pageSize));
|
|
106
|
+
const endIndex = Math.min(startIndex + pageSize, filteredChoices.length);
|
|
107
|
+
const visibleChoices = filteredChoices.slice(startIndex, endIndex);
|
|
108
|
+
for (let i = 0; i < visibleChoices.length; i++) {
|
|
109
|
+
const item = visibleChoices[i];
|
|
110
|
+
const actualIndex = startIndex + i;
|
|
111
|
+
const isActive = actualIndex === cursor;
|
|
112
|
+
const selected = selectedSet.has(item.value);
|
|
113
|
+
const icon = selected ? chalk.green('◉') : chalk.dim('○');
|
|
114
|
+
const arrow = isActive ? chalk.cyan('›') : ' ';
|
|
115
|
+
const name = isActive ? chalk.cyan(item.name) : item.name;
|
|
116
|
+
const isRefresh = selected && item.configured;
|
|
117
|
+
const suffix = selected
|
|
118
|
+
? chalk.dim(isRefresh ? ' (refresh)' : ' (selected)')
|
|
119
|
+
: '';
|
|
120
|
+
lines.push(` ${arrow} ${icon} ${name}${suffix}`);
|
|
121
|
+
}
|
|
122
|
+
// Show pagination indicator if needed
|
|
123
|
+
if (filteredChoices.length > pageSize) {
|
|
124
|
+
const currentPage = Math.floor(cursor / pageSize) + 1;
|
|
125
|
+
const totalPages = Math.ceil(filteredChoices.length / pageSize);
|
|
126
|
+
lines.push(chalk.dim(` (${currentPage}/${totalPages})`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (error)
|
|
130
|
+
lines.push(chalk.red(` ${error}`));
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A searchable multi-select prompt with visible search box,
|
|
136
|
+
* selected items display, and intuitive keyboard navigation.
|
|
137
|
+
*
|
|
138
|
+
* - Type to filter choices
|
|
139
|
+
* - ↑↓ to navigate
|
|
140
|
+
* - Enter to add highlighted item
|
|
141
|
+
* - Backspace to remove last selected item (or delete search char)
|
|
142
|
+
* - Tab to confirm selections
|
|
143
|
+
*/
|
|
144
|
+
export async function searchableMultiSelect(config) {
|
|
145
|
+
const prompt = await createSearchableMultiSelect();
|
|
146
|
+
return prompt(config);
|
|
147
|
+
}
|
|
148
|
+
export default searchableMultiSelect;
|
|
149
|
+
//# sourceMappingURL=searchable-multi-select.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII art animation patterns for the welcome screen.
|
|
3
|
+
* OpenSpec logo animation - diamond/rhombus shape with hollow center "O".
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Welcome animation frames - OpenSpec logo building from center
|
|
7
|
+
* 7 rows × 6 columns diamond with hollow center "O"
|
|
8
|
+
* Center bar is 2 cols × 3 rows (rows 3,4,5 cols 3,4)
|
|
9
|
+
* Each frame is an array of strings (lines of ASCII art)
|
|
10
|
+
* Grid: 6 cols × 2 chars = 12 chars wide
|
|
11
|
+
*/
|
|
12
|
+
export declare const WELCOME_ANIMATION: {
|
|
13
|
+
interval: number;
|
|
14
|
+
frames: string[][];
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=ascii-patterns.d.ts.map
|