@fission-ai/openspec 1.6.0-beta.1 → 1.7.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 +28 -1
- package/dist/cli/index.js +73 -7
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +90 -50
- package/dist/commands/config.js +14 -8
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +74 -23
- package/dist/commands/schema.js +21 -11
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +20 -24
- package/dist/commands/validate.js +18 -3
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.js +89 -6
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +7 -2
- package/dist/core/archive.d.ts +0 -1
- package/dist/core/archive.js +96 -33
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- package/dist/core/artifact-graph/instruction-loader.d.ts +27 -2
- package/dist/core/artifact-graph/instruction-loader.js +55 -7
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +34 -6
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +6 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +7 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -1
- package/dist/core/command-generation/adapters/auggie.js +2 -1
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +6 -5
- package/dist/core/command-generation/adapters/claude.js +1 -8
- package/dist/core/command-generation/adapters/codebuddy.js +3 -2
- package/dist/core/command-generation/adapters/continue.js +3 -2
- package/dist/core/command-generation/adapters/costrict.js +2 -1
- package/dist/core/command-generation/adapters/crush.js +5 -5
- package/dist/core/command-generation/adapters/cursor.js +2 -2
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/factory.js +2 -1
- package/dist/core/command-generation/adapters/gemini.js +37 -2
- package/dist/core/command-generation/adapters/github-copilot.js +2 -1
- package/dist/core/command-generation/adapters/iflow.js +5 -4
- package/dist/core/command-generation/adapters/index.d.ts +2 -2
- package/dist/core/command-generation/adapters/index.js +2 -2
- package/dist/core/command-generation/adapters/junie.js +2 -1
- package/dist/core/command-generation/adapters/kiro.js +2 -1
- package/dist/core/command-generation/adapters/lingma.js +5 -5
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
- package/dist/core/command-generation/adapters/opencode.js +3 -5
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +3 -6
- package/dist/core/command-generation/adapters/qoder.js +5 -5
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +12 -7
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +4 -4
- package/dist/core/command-generation/adapters/trae.js +1 -21
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.js +4 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +8 -2
- package/dist/core/completions/generators/powershell-generator.js +15 -8
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +7 -1
- package/dist/core/config-schema.js +40 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +21 -3
- package/dist/core/global-config.d.ts +5 -0
- package/dist/core/init.d.ts +35 -0
- package/dist/core/init.js +225 -39
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +249 -22
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +71 -1
- package/dist/core/migration.js +294 -1
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +33 -20
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.js +2 -1
- package/dist/core/parsers/requirement-blocks.d.ts +8 -0
- package/dist/core/parsers/requirement-blocks.js +60 -35
- package/dist/core/parsers/requirement-text.d.ts +1 -8
- package/dist/core/parsers/requirement-text.js +4 -42
- package/dist/core/parsers/spec-structure.js +3 -29
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +9 -45
- package/dist/core/project-config.d.ts +25 -6
- package/dist/core/project-config.js +77 -7
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- package/dist/core/shared/tool-detection.d.ts +25 -5
- package/dist/core/shared/tool-detection.js +105 -6
- package/dist/core/specs-apply.d.ts +8 -33
- package/dist/core/specs-apply.js +252 -157
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/templates/workflows/apply-change.js +54 -4
- package/dist/core/templates/workflows/archive-change.js +166 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +240 -63
- package/dist/core/templates/workflows/continue-change.js +32 -40
- package/dist/core/templates/workflows/explore.js +12 -0
- package/dist/core/templates/workflows/ff-change.js +46 -30
- package/dist/core/templates/workflows/new-change.js +2 -2
- package/dist/core/templates/workflows/onboard.js +4 -4
- package/dist/core/templates/workflows/propose.js +50 -32
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +172 -22
- package/dist/core/templates/workflows/update-change.js +20 -12
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +39 -5
- package/dist/core/update.js +288 -71
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +1 -7
- package/dist/core/validation/validator.js +92 -36
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +736 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +55 -33
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +30 -0
- package/dist/utils/change-metadata.js +101 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/command-references.d.ts +80 -6
- package/dist/utils/command-references.js +138 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +102 -0
- package/package.json +29 -30
- package/schemas/spec-driven/schema.yaml +60 -5
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +5 -1
- package/schemas/spec-driven/templates/spec.md +3 -0
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
package/dist/core/init.js
CHANGED
|
@@ -12,18 +12,19 @@ import { createRequire } from 'module';
|
|
|
12
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
13
|
import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
|
|
14
14
|
import { findRepoPlanningRootSync } from './planning-home.js';
|
|
15
|
-
import {
|
|
16
|
-
import { AI_TOOLS, OPENSPEC_DIR_NAME, } from './config.js';
|
|
15
|
+
import { getSkillReferenceTransformer, getTransformerForTool } from '../utils/command-references.js';
|
|
16
|
+
import { AI_TOOLS, OPENSPEC_DIR_NAME, resolveToolIdAlias, } from './config.js';
|
|
17
17
|
import { PALETTE } from './styles/palette.js';
|
|
18
18
|
import { isInteractive } from '../utils/interactive.js';
|
|
19
19
|
import { serializeConfig } from './config-prompts.js';
|
|
20
20
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
21
|
-
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, } from './legacy-cleanup.js';
|
|
21
|
+
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDeferredGlobalPromptSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
22
22
|
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, } from './shared/index.js';
|
|
23
23
|
import { getGlobalConfig } from './global-config.js';
|
|
24
24
|
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
|
|
25
25
|
import { getAvailableTools } from './available-tools.js';
|
|
26
|
-
import { migrateIfNeeded } from './migration.js';
|
|
26
|
+
import { migrateIfNeeded, migrateLegacyToolDirs, describeLegacyMigration, keptInPlaceNotice, hasMovableContent, scanInstalledWorkflows as scanInstalledWorkflowsShared } from './migration.js';
|
|
27
|
+
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
27
28
|
const require = createRequire(import.meta.url);
|
|
28
29
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
29
30
|
// -----------------------------------------------------------------------------
|
|
@@ -56,11 +57,13 @@ export class InitCommand {
|
|
|
56
57
|
force;
|
|
57
58
|
interactiveOption;
|
|
58
59
|
profileOverride;
|
|
60
|
+
animation;
|
|
59
61
|
constructor(options = {}) {
|
|
60
62
|
this.toolsArg = options.tools;
|
|
61
63
|
this.force = options.force ?? false;
|
|
62
64
|
this.interactiveOption = options.interactive;
|
|
63
65
|
this.profileOverride = options.profile;
|
|
66
|
+
this.animation = options.animation ?? true;
|
|
64
67
|
}
|
|
65
68
|
async execute(targetPath) {
|
|
66
69
|
const projectPath = path.resolve(targetPath);
|
|
@@ -91,32 +94,53 @@ export class InitCommand {
|
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
// Check for legacy artifacts and handle cleanup
|
|
94
|
-
await this.handleLegacyCleanup(projectPath, extendMode);
|
|
97
|
+
const deferredLegacyCleanup = await this.handleLegacyCleanup(projectPath, extendMode);
|
|
98
|
+
// Migrate OpenSpec-managed skills left in renamed tool directories
|
|
99
|
+
// (e.g. .kimi -> .kimi-code) before detection so they stay recognized.
|
|
100
|
+
migrateLegacyToolDirs(projectPath);
|
|
95
101
|
// Detect available tools in the project (task 7.1)
|
|
96
102
|
const detectedTools = getAvailableTools(projectPath);
|
|
97
103
|
// Migration check: migrate existing projects to profile system (task 7.3)
|
|
98
104
|
if (extendMode) {
|
|
99
105
|
migrateIfNeeded(projectPath, detectedTools);
|
|
100
106
|
}
|
|
107
|
+
// Validate profile override early so invalid values fail before tool setup.
|
|
108
|
+
// The resolved value is consumed later when generation reads effective config.
|
|
109
|
+
// This runs ahead of the welcome screen so an invalid --profile does not make
|
|
110
|
+
// the user press Enter before seeing the error.
|
|
111
|
+
this.resolveProfileOverride();
|
|
101
112
|
// Show animated welcome screen (interactive mode only)
|
|
102
113
|
const canPrompt = this.canPromptInteractively();
|
|
103
114
|
if (canPrompt) {
|
|
104
115
|
const { showWelcomeScreen } = await import('../ui/welcome-screen.js');
|
|
105
|
-
await showWelcomeScreen();
|
|
116
|
+
await showWelcomeScreen(this.getActiveWorkflows(), { animate: this.animation });
|
|
106
117
|
}
|
|
107
|
-
// Validate profile override early so invalid values fail before tool setup.
|
|
108
|
-
// The resolved value is consumed later when generation reads effective config.
|
|
109
|
-
this.resolveProfileOverride();
|
|
110
118
|
// Get tool states before processing
|
|
111
119
|
const toolStates = getToolStates(projectPath);
|
|
112
120
|
// Get tool selection (pass detected tools for pre-selection)
|
|
113
121
|
const selectedToolIds = await this.getSelectedTools(toolStates, extendMode, detectedTools, projectPath);
|
|
114
122
|
// Validate selected tools
|
|
115
123
|
const validatedTools = this.validateTools(selectedToolIds, toolStates);
|
|
124
|
+
// Selecting a renamed tool is consent to leave its former directory:
|
|
125
|
+
// init is about to write the current one, and leaving OpenSpec content
|
|
126
|
+
// behind would give the user two installs of the same tool.
|
|
127
|
+
for (const migration of migrateLegacyToolDirs(projectPath, validatedTools.map((tool) => tool.value))) {
|
|
128
|
+
if (hasMovableContent(migration)) {
|
|
129
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
130
|
+
}
|
|
131
|
+
const kept = keptInPlaceNotice(migration);
|
|
132
|
+
if (kept)
|
|
133
|
+
console.log(chalk.dim(kept));
|
|
134
|
+
}
|
|
116
135
|
// Create directory structure and config
|
|
117
136
|
await this.createDirectoryStructure(openspecPath, extendMode);
|
|
118
137
|
// Generate skills and commands for each tool
|
|
119
138
|
const results = await this.generateSkillsAndCommands(projectPath, validatedTools);
|
|
139
|
+
// Legacy cleanup was deferred to avoid interfering with skill/command generation;
|
|
140
|
+
// now that outputs are written, finalize the cleanup (e.g. remove stale files).
|
|
141
|
+
if (deferredLegacyCleanup) {
|
|
142
|
+
await this.finalizeDeferredLegacyCleanup(projectPath, deferredLegacyCleanup);
|
|
143
|
+
}
|
|
120
144
|
// Create config.yaml if needed
|
|
121
145
|
const configStatus = await this.createConfig(openspecPath, extendMode);
|
|
122
146
|
// Display success message
|
|
@@ -149,26 +173,50 @@ export class InitCommand {
|
|
|
149
173
|
}
|
|
150
174
|
throw new Error(`Invalid profile "${this.profileOverride}". Available profiles: core, custom`);
|
|
151
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Resolves the workflows the effective profile installs, so onboarding output
|
|
178
|
+
* only mentions commands that will actually exist.
|
|
179
|
+
*/
|
|
180
|
+
getActiveWorkflows() {
|
|
181
|
+
const globalCfg = getGlobalConfig();
|
|
182
|
+
const activeProfile = this.resolveProfileOverride() ?? globalCfg.profile ?? 'core';
|
|
183
|
+
return [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
|
|
184
|
+
}
|
|
152
185
|
// ═══════════════════════════════════════════════════════════
|
|
153
186
|
// LEGACY CLEANUP
|
|
154
187
|
// ═══════════════════════════════════════════════════════════
|
|
188
|
+
/**
|
|
189
|
+
* Cleans repo-local legacy artifacts immediately and defers global Codex prompt
|
|
190
|
+
* cleanup until replacement skills have been installed.
|
|
191
|
+
*/
|
|
155
192
|
async handleLegacyCleanup(projectPath, extendMode) {
|
|
156
193
|
// Detect legacy artifacts
|
|
157
194
|
const detection = await detectLegacyArtifacts(projectPath);
|
|
158
195
|
if (!detection.hasLegacyArtifacts) {
|
|
159
|
-
return; // No legacy artifacts found
|
|
196
|
+
return null; // No legacy artifacts found
|
|
160
197
|
}
|
|
198
|
+
const immediateDetection = omitGlobalLegacyPromptFiles(detection);
|
|
161
199
|
// Show what was detected
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
200
|
+
const immediateSummary = formatDetectionSummary(immediateDetection);
|
|
201
|
+
if (immediateSummary) {
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(immediateSummary);
|
|
204
|
+
console.log();
|
|
205
|
+
}
|
|
206
|
+
// Show which global prompts are deferred — they'll only be removed once
|
|
207
|
+
// the corresponding replacement skills are installed during generation.
|
|
208
|
+
const deferredSummary = formatDeferredGlobalPromptSummary(detection);
|
|
209
|
+
if (deferredSummary) {
|
|
210
|
+
console.log(deferredSummary);
|
|
211
|
+
console.log();
|
|
212
|
+
}
|
|
165
213
|
const canPrompt = this.canPromptInteractively();
|
|
166
214
|
if (this.force || !canPrompt) {
|
|
167
215
|
// --force flag or non-interactive mode: proceed with cleanup automatically.
|
|
168
216
|
// Legacy slash commands are 100% OpenSpec-managed, and config file cleanup
|
|
169
217
|
// only removes markers (never deletes files), so auto-cleanup is safe.
|
|
170
|
-
await this.
|
|
171
|
-
return;
|
|
218
|
+
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
219
|
+
return detection.globalSlashCommandFiles.length > 0 ? { detection } : null;
|
|
172
220
|
}
|
|
173
221
|
// Interactive mode: prompt for confirmation
|
|
174
222
|
const { confirm } = await import('@inquirer/prompts');
|
|
@@ -181,7 +229,51 @@ export class InitCommand {
|
|
|
181
229
|
console.log(chalk.dim('Run with --force to skip this prompt, or manually remove legacy files.'));
|
|
182
230
|
process.exit(0);
|
|
183
231
|
}
|
|
184
|
-
await this.
|
|
232
|
+
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
233
|
+
return detection.globalSlashCommandFiles.length > 0 ? { detection } : null;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Applies the safe subset of legacy cleanup that does not depend on newly
|
|
237
|
+
* generated Codex skills.
|
|
238
|
+
*/
|
|
239
|
+
async performImmediateLegacyCleanup(projectPath, detection) {
|
|
240
|
+
const immediateDetection = omitGlobalLegacyPromptFiles(detection);
|
|
241
|
+
if (!immediateDetection.hasLegacyArtifacts) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
await this.performLegacyCleanup(projectPath, immediateDetection);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Removes only the legacy global Codex prompts whose workflows now have
|
|
248
|
+
* replacement skills in the project.
|
|
249
|
+
*/
|
|
250
|
+
async finalizeDeferredLegacyCleanup(projectPath, deferredCleanup) {
|
|
251
|
+
const availableCodexWorkflows = await this.getInstalledWorkflowsForTool(projectPath, 'codex');
|
|
252
|
+
const removableMatches = getLegacyGlobalPromptMatches(deferredCleanup.detection)
|
|
253
|
+
.filter((prompt) => prompt.workflowIds.every((workflowId) => availableCodexWorkflows.has(workflowId)));
|
|
254
|
+
if (removableMatches.length > 0) {
|
|
255
|
+
await this.performLegacyCleanup(projectPath, pickGlobalLegacyPromptFiles(deferredCleanup.detection, removableMatches.map((prompt) => prompt.path)));
|
|
256
|
+
}
|
|
257
|
+
const blockedMatches = getLegacyGlobalPromptMatches(deferredCleanup.detection)
|
|
258
|
+
.filter((prompt) => !removableMatches.some((match) => match.path === prompt.path));
|
|
259
|
+
if (blockedMatches.length > 0) {
|
|
260
|
+
console.log(chalk.yellow('Preserved deferred global prompts without replacement skills:'));
|
|
261
|
+
for (const prompt of blockedMatches) {
|
|
262
|
+
console.log(chalk.dim(` - ${prompt.toolId}: ${prompt.path}`));
|
|
263
|
+
}
|
|
264
|
+
console.log();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Reads the currently installed workflow IDs for a single tool from the
|
|
269
|
+
* generated skill layout on disk.
|
|
270
|
+
*/
|
|
271
|
+
async getInstalledWorkflowsForTool(projectPath, toolId) {
|
|
272
|
+
const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
|
|
273
|
+
if (!tool) {
|
|
274
|
+
return new Set();
|
|
275
|
+
}
|
|
276
|
+
return new Set(scanInstalledWorkflowsShared(projectPath, [tool]));
|
|
185
277
|
}
|
|
186
278
|
async performLegacyCleanup(projectPath, detection) {
|
|
187
279
|
const spinner = ora('Cleaning up legacy files...').start();
|
|
@@ -300,7 +392,9 @@ export class InitCommand {
|
|
|
300
392
|
if (tokens.length === 0) {
|
|
301
393
|
throw new Error('The --tools option requires at least one tool ID when not using "all" or "none".');
|
|
302
394
|
}
|
|
303
|
-
|
|
395
|
+
// Retired ids resolve to their current tool, so a rebrand does not break
|
|
396
|
+
// an existing `--tools windsurf` in someone's setup script.
|
|
397
|
+
const normalizedTokens = tokens.map((token) => resolveToolIdAlias(token.toLowerCase()));
|
|
304
398
|
if (normalizedTokens.some((token) => token === 'all' || token === 'none')) {
|
|
305
399
|
throw new Error('Cannot combine reserved values "all" or "none" with specific tool IDs.');
|
|
306
400
|
}
|
|
@@ -374,11 +468,20 @@ export class InitCommand {
|
|
|
374
468
|
// ═══════════════════════════════════════════════════════════
|
|
375
469
|
// SKILL & COMMAND GENERATION
|
|
376
470
|
// ═══════════════════════════════════════════════════════════
|
|
471
|
+
/**
|
|
472
|
+
* Generates skill files and slash commands for each selected tool,
|
|
473
|
+
* honoring the configured delivery mode (skills, commands, or both).
|
|
474
|
+
*
|
|
475
|
+
* @param projectPath - Absolute path to the project root
|
|
476
|
+
* @param tools - Selected tools with their skill directory metadata
|
|
477
|
+
* @returns Created, refreshed, and failed tools plus removed artifact counts
|
|
478
|
+
*/
|
|
377
479
|
async generateSkillsAndCommands(projectPath, tools) {
|
|
378
480
|
const createdTools = [];
|
|
379
481
|
const refreshedTools = [];
|
|
380
482
|
const failedTools = [];
|
|
381
483
|
const commandsSkipped = [];
|
|
484
|
+
const skillsInvocableCommandSkips = [];
|
|
382
485
|
let removedCommandCount = 0;
|
|
383
486
|
let removedSkillCount = 0;
|
|
384
487
|
// Read global config for profile and delivery settings (use --profile override if set)
|
|
@@ -387,15 +490,16 @@ export class InitCommand {
|
|
|
387
490
|
const delivery = globalConfig.delivery ?? 'both';
|
|
388
491
|
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
389
492
|
// Get skill and command templates filtered by profile workflows
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const commandContents = shouldGenerateCommands ? getCommandContents(workflows) : [];
|
|
493
|
+
const deliveryIncludesCommands = delivery !== 'skills';
|
|
494
|
+
const skillTemplates = getSkillTemplates(workflows);
|
|
495
|
+
const commandContents = getCommandContents(workflows);
|
|
394
496
|
// Process each tool
|
|
395
497
|
for (const tool of tools) {
|
|
396
498
|
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
397
499
|
try {
|
|
398
|
-
|
|
500
|
+
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
501
|
+
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
502
|
+
// Generate skill files if the selected delivery and tool capability allow skills
|
|
399
503
|
if (shouldGenerateSkills) {
|
|
400
504
|
// Use tool-specific skillsDir
|
|
401
505
|
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
@@ -404,14 +508,13 @@ export class InitCommand {
|
|
|
404
508
|
const skillDir = path.join(skillsDir, dirName);
|
|
405
509
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
406
510
|
// Generate SKILL.md content with YAML frontmatter including generatedBy
|
|
407
|
-
|
|
408
|
-
const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
|
|
511
|
+
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
409
512
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
410
513
|
// Write the skill file
|
|
411
514
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
412
515
|
}
|
|
413
516
|
}
|
|
414
|
-
if (
|
|
517
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery)) {
|
|
415
518
|
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
416
519
|
removedSkillCount += await this.removeSkillDirs(skillsDir);
|
|
417
520
|
}
|
|
@@ -425,11 +528,16 @@ export class InitCommand {
|
|
|
425
528
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
426
529
|
}
|
|
427
530
|
}
|
|
531
|
+
}
|
|
532
|
+
else if (deliveryIncludesCommands) {
|
|
533
|
+
if (resolveCommandSurfaceCapability(tool.value) === 'skills-invocable') {
|
|
534
|
+
skillsInvocableCommandSkips.push(tool.value);
|
|
535
|
+
}
|
|
428
536
|
else {
|
|
429
537
|
commandsSkipped.push(tool.value);
|
|
430
538
|
}
|
|
431
539
|
}
|
|
432
|
-
if (
|
|
540
|
+
if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
|
|
433
541
|
removedCommandCount += await this.removeCommandFiles(projectPath, tool.value);
|
|
434
542
|
}
|
|
435
543
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
@@ -450,6 +558,7 @@ export class InitCommand {
|
|
|
450
558
|
refreshedTools,
|
|
451
559
|
failedTools,
|
|
452
560
|
commandsSkipped,
|
|
561
|
+
skillsInvocableCommandSkips,
|
|
453
562
|
removedCommandCount,
|
|
454
563
|
removedSkillCount,
|
|
455
564
|
};
|
|
@@ -496,8 +605,12 @@ export class InitCommand {
|
|
|
496
605
|
const delivery = globalConfig.delivery ?? 'both';
|
|
497
606
|
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
498
607
|
const toolDirs = [...new Set(successfulTools.map((t) => t.skillsDir))].join(', ');
|
|
499
|
-
const skillCount =
|
|
500
|
-
|
|
608
|
+
const skillCount = successfulTools.some((tool) => shouldGenerateSkillsForTool(tool.value, delivery))
|
|
609
|
+
? getSkillTemplates(workflows).length
|
|
610
|
+
: 0;
|
|
611
|
+
const commandCount = successfulTools.some((tool) => shouldGenerateCommandsForTool(tool.value, delivery))
|
|
612
|
+
? getCommandContents(workflows).length
|
|
613
|
+
: 0;
|
|
501
614
|
if (skillCount > 0 && commandCount > 0) {
|
|
502
615
|
console.log(`${skillCount} skills and ${commandCount} commands in ${toolDirs}/`);
|
|
503
616
|
}
|
|
@@ -516,12 +629,22 @@ export class InitCommand {
|
|
|
516
629
|
if (results.commandsSkipped.length > 0) {
|
|
517
630
|
console.log(chalk.dim(`Commands skipped for: ${results.commandsSkipped.join(', ')} (no adapter)`));
|
|
518
631
|
}
|
|
632
|
+
if (results.skillsInvocableCommandSkips.length > 0) {
|
|
633
|
+
console.log(chalk.dim(`Commands skipped for: ${results.skillsInvocableCommandSkips.join(', ')} (uses skills)`));
|
|
634
|
+
}
|
|
519
635
|
if (results.removedCommandCount > 0) {
|
|
520
636
|
console.log(chalk.dim(`Removed: ${results.removedCommandCount} command files (delivery: skills)`));
|
|
521
637
|
}
|
|
522
638
|
if (results.removedSkillCount > 0) {
|
|
523
639
|
console.log(chalk.dim(`Removed: ${results.removedSkillCount} skill directories (delivery: commands)`));
|
|
524
640
|
}
|
|
641
|
+
// Show manual setup notes for tools that need extra configuration
|
|
642
|
+
for (const tool of successfulTools) {
|
|
643
|
+
const setupNote = AI_TOOLS.find((t) => t.value === tool.value)?.setupNote;
|
|
644
|
+
if (setupNote) {
|
|
645
|
+
console.log(chalk.yellow(`Setup required for ${tool.name}: ${setupNote}`));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
525
648
|
// Config status
|
|
526
649
|
if (configStatus === 'created') {
|
|
527
650
|
console.log(`Config: openspec/config.yaml (schema: ${DEFAULT_SCHEMA})`);
|
|
@@ -537,17 +660,74 @@ export class InitCommand {
|
|
|
537
660
|
console.log(chalk.dim(`Config: skipped (non-interactive mode)`));
|
|
538
661
|
}
|
|
539
662
|
// Getting started (task 7.6: show propose if in profile)
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
663
|
+
const activeWorkflows = this.getActiveWorkflows();
|
|
664
|
+
// When no tool got /opsx:* commands, point at the skill instead of a
|
|
665
|
+
// command that does not exist.
|
|
666
|
+
const activeDelivery = getGlobalConfig().delivery ?? 'both';
|
|
667
|
+
const commandsGenerated = successfulTools.some((tool) => shouldGenerateCommandsForTool(tool.value, activeDelivery));
|
|
668
|
+
const skillsGenerated = successfulTools.some((tool) => shouldGenerateSkillsForTool(tool.value, activeDelivery));
|
|
669
|
+
// Each hint line must be a usable instruction for the tool it serves.
|
|
670
|
+
// Tools that generated commands are told the command name their files
|
|
671
|
+
// answer to (/opsx:* when namespaced under opsx/, /opsx-* when the
|
|
672
|
+
// filename is the command); tools that only got skills are told their
|
|
673
|
+
// documented skill invocation (Kimi Code: /skill:openspec-*; Codex CLI:
|
|
674
|
+
// $openspec-*; others: /openspec-*). Tools that got no artifacts are
|
|
675
|
+
// covered by the configuration correction instead. When the selection
|
|
676
|
+
// disagrees, print one line per distinct instruction, labeled with the
|
|
677
|
+
// tools it applies to.
|
|
678
|
+
const startHintLines = (command) => {
|
|
679
|
+
const hintToTools = new Map();
|
|
680
|
+
for (const tool of successfulTools) {
|
|
681
|
+
let hint;
|
|
682
|
+
if (shouldGenerateCommandsForTool(tool.value, activeDelivery)) {
|
|
683
|
+
const transformer = getTransformerForTool(tool.value, activeDelivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
684
|
+
hint = `Start your first change: ${transformer ? transformer(command) : command} "your idea"`;
|
|
685
|
+
}
|
|
686
|
+
else if (shouldGenerateSkillsForTool(tool.value, activeDelivery)) {
|
|
687
|
+
hint = `Start your first change: ${getSkillReferenceTransformer(tool.value)(command)} "your idea"`;
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
hintToTools.set(hint, [...(hintToTools.get(hint) ?? []), tool.name]);
|
|
693
|
+
}
|
|
694
|
+
if (hintToTools.size === 0) {
|
|
695
|
+
// No successful tools: keep the generic command hint
|
|
696
|
+
return [`Start your first change: ${command} "your idea"`];
|
|
697
|
+
}
|
|
698
|
+
if (hintToTools.size === 1) {
|
|
699
|
+
return [[...hintToTools.keys()][0]];
|
|
700
|
+
}
|
|
701
|
+
return [...hintToTools.entries()].map(([hint, toolNames]) => `${hint} (${toolNames.join(', ')})`);
|
|
702
|
+
};
|
|
703
|
+
const printStartHints = (command) => {
|
|
545
704
|
console.log(chalk.bold('Getting started:'));
|
|
546
|
-
|
|
705
|
+
for (const line of startHintLines(command)) {
|
|
706
|
+
console.log(` ${line}`);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
console.log();
|
|
710
|
+
// delivery=commands with tools that only support skills: those tools get
|
|
711
|
+
// no artifacts at all, so print a per-tool configuration correction
|
|
712
|
+
// rather than leave them with a dead (or missing) instruction — even
|
|
713
|
+
// when other selected tools did get commands or skills.
|
|
714
|
+
const zeroArtifactTools = successfulTools.filter((tool) => !shouldGenerateSkillsForTool(tool.value, activeDelivery) &&
|
|
715
|
+
!shouldGenerateCommandsForTool(tool.value, activeDelivery));
|
|
716
|
+
if (zeroArtifactTools.length > 0) {
|
|
717
|
+
const names = zeroArtifactTools.map((tool) => tool.name).join(', ');
|
|
718
|
+
console.log(chalk.yellow(`No skills or commands were generated for ${names}: delivery is set to 'commands' but ` +
|
|
719
|
+
`${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
|
|
720
|
+
`Run 'openspec config set delivery both' to generate skills.`));
|
|
721
|
+
}
|
|
722
|
+
if (successfulTools.length > 0 && !commandsGenerated && !skillsGenerated) {
|
|
723
|
+
// Nothing was generated for any tool: the correction above is the
|
|
724
|
+
// whole story, so don't advertise an invocation that doesn't exist.
|
|
725
|
+
}
|
|
726
|
+
else if (activeWorkflows.includes('propose')) {
|
|
727
|
+
printStartHints('/opsx:propose');
|
|
547
728
|
}
|
|
548
729
|
else if (activeWorkflows.includes('new')) {
|
|
549
|
-
|
|
550
|
-
console.log(' Start your first change: /opsx:new "your idea"');
|
|
730
|
+
printStartHints('/opsx:new');
|
|
551
731
|
}
|
|
552
732
|
else {
|
|
553
733
|
console.log("Done. Run 'openspec config profile' to configure your workflows.");
|
|
@@ -556,10 +736,16 @@ export class InitCommand {
|
|
|
556
736
|
console.log();
|
|
557
737
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
558
738
|
console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`);
|
|
559
|
-
// Restart instruction if any tools were configured
|
|
560
|
-
|
|
739
|
+
// Restart instruction if any tools were configured and got a surface
|
|
740
|
+
// (when nothing was generated there is nothing a restart would pick up);
|
|
741
|
+
// only mention commands when commands were actually generated. Not "slash
|
|
742
|
+
// commands": Amazon Q's generated files are prompt-library entries invoked
|
|
743
|
+
// with @, so a restart line promising slash commands would be wrong for it.
|
|
744
|
+
if ((results.createdTools.length > 0 || results.refreshedTools.length > 0) && (commandsGenerated || skillsGenerated)) {
|
|
561
745
|
console.log();
|
|
562
|
-
console.log(chalk.white(
|
|
746
|
+
console.log(chalk.white(commandsGenerated
|
|
747
|
+
? 'Restart your IDE for the new commands to take effect.'
|
|
748
|
+
: 'Restart your IDE for the new skills to take effect.'));
|
|
563
749
|
}
|
|
564
750
|
console.log();
|
|
565
751
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Legacy cleanup module for detecting and removing OpenSpec artifacts
|
|
3
3
|
* from previous init versions during the migration to the skill-based workflow.
|
|
4
4
|
*/
|
|
5
|
+
import type { WorkflowId } from './profiles.js';
|
|
5
6
|
/**
|
|
6
7
|
* Legacy config file names from the old ToolRegistry.
|
|
7
8
|
* These were config files created at project root with OpenSpec markers.
|
|
@@ -13,6 +14,11 @@ export declare const LEGACY_CONFIG_FILES: readonly ["CLAUDE.md", "CLINE.md", "CO
|
|
|
13
14
|
* Some tools used a directory structure, others used individual files.
|
|
14
15
|
*/
|
|
15
16
|
export declare const LEGACY_SLASH_COMMAND_PATHS: Record<string, LegacySlashCommandPattern>;
|
|
17
|
+
/**
|
|
18
|
+
* Global legacy prompt locations that live outside the project tree and require
|
|
19
|
+
* allowlisted matching instead of broad glob-based cleanup.
|
|
20
|
+
*/
|
|
21
|
+
export declare const LEGACY_GLOBAL_SLASH_COMMAND_PATHS: Record<string, LegacyGlobalPromptPattern>;
|
|
16
22
|
/**
|
|
17
23
|
* Pattern types for legacy slash commands
|
|
18
24
|
*/
|
|
@@ -21,6 +27,28 @@ export interface LegacySlashCommandPattern {
|
|
|
21
27
|
path?: string;
|
|
22
28
|
pattern?: string | string[];
|
|
23
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Describes a managed global prompt home and the exact filenames OpenSpec is
|
|
32
|
+
* allowed to treat as legacy artifacts there.
|
|
33
|
+
*/
|
|
34
|
+
export interface LegacyGlobalPromptPattern {
|
|
35
|
+
managedFileNames: readonly string[];
|
|
36
|
+
workflowIdsByFileName?: Readonly<Record<string, readonly WorkflowId[]>>;
|
|
37
|
+
resolvePromptDir: () => string;
|
|
38
|
+
replacementLabel?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Workflow-aware metadata for a detected global legacy prompt that is safe for
|
|
42
|
+
* replacement-gated cleanup.
|
|
43
|
+
*/
|
|
44
|
+
export interface LegacyGlobalPromptMatch {
|
|
45
|
+
path: string;
|
|
46
|
+
toolId: string;
|
|
47
|
+
managedFileName: string;
|
|
48
|
+
workflowIds: readonly WorkflowId[];
|
|
49
|
+
replacementLabel?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function getCodexPromptDir(): string;
|
|
24
52
|
/**
|
|
25
53
|
* Result of legacy artifact detection
|
|
26
54
|
*/
|
|
@@ -33,6 +61,10 @@ export interface LegacyDetectionResult {
|
|
|
33
61
|
slashCommandDirs: string[];
|
|
34
62
|
/** Legacy slash command files found (for file-based tools) */
|
|
35
63
|
slashCommandFiles: string[];
|
|
64
|
+
/** Managed global command/prompt files found outside the project root */
|
|
65
|
+
globalSlashCommandFiles: string[];
|
|
66
|
+
/** Details for managed global command/prompt files */
|
|
67
|
+
globalSlashCommandDetails?: LegacyGlobalPromptMatch[];
|
|
36
68
|
/** Whether openspec/AGENTS.md exists */
|
|
37
69
|
hasOpenspecAgents: boolean;
|
|
38
70
|
/** Whether openspec/project.md exists (preserved, migration hint only) */
|
|
@@ -111,6 +143,8 @@ export declare function removeMarkerBlock(content: string): string;
|
|
|
111
143
|
export interface CleanupResult {
|
|
112
144
|
/** Files that were deleted entirely */
|
|
113
145
|
deletedFiles: string[];
|
|
146
|
+
/** Replacement labels for deleted files when cleanup knows the new surface */
|
|
147
|
+
deletedFileReplacementLabels?: Record<string, string>;
|
|
114
148
|
/** Files that had marker blocks removed */
|
|
115
149
|
modifiedFiles: string[];
|
|
116
150
|
/** Directories that were deleted */
|
|
@@ -144,6 +178,11 @@ export declare function formatCleanupSummary(result: CleanupResult): string;
|
|
|
144
178
|
* @returns Formatted summary string showing what was found
|
|
145
179
|
*/
|
|
146
180
|
export declare function formatDetectionSummary(detection: LegacyDetectionResult): string;
|
|
181
|
+
/**
|
|
182
|
+
* Generates a summary for managed global prompt files whose cleanup must wait
|
|
183
|
+
* until replacement skills are installed.
|
|
184
|
+
*/
|
|
185
|
+
export declare function formatDeferredGlobalPromptSummary(detection: LegacyDetectionResult): string;
|
|
147
186
|
/**
|
|
148
187
|
* Extract tool IDs from detected legacy artifacts.
|
|
149
188
|
* Uses LEGACY_SLASH_COMMAND_PATHS to map paths back to tool IDs.
|
|
@@ -152,6 +191,26 @@ export declare function formatDetectionSummary(detection: LegacyDetectionResult)
|
|
|
152
191
|
* @returns Array of tool IDs that had legacy artifacts
|
|
153
192
|
*/
|
|
154
193
|
export declare function getToolsFromLegacyArtifacts(detection: LegacyDetectionResult): string[];
|
|
194
|
+
/**
|
|
195
|
+
* Normalizes global Codex prompt matches so callers can rely on workflow-aware
|
|
196
|
+
* metadata even when older detection results only carry file paths.
|
|
197
|
+
*/
|
|
198
|
+
export declare function getLegacyGlobalPromptMatches(detection: LegacyDetectionResult): LegacyGlobalPromptMatch[];
|
|
199
|
+
/**
|
|
200
|
+
* Collects workflow IDs inferred from detected legacy global prompts for a
|
|
201
|
+
* specific tool.
|
|
202
|
+
*/
|
|
203
|
+
export declare function getLegacyWorkflowIdsForTool(detection: LegacyDetectionResult, toolId: string): WorkflowId[];
|
|
204
|
+
/**
|
|
205
|
+
* Returns a detection snapshot with global Codex prompt cleanup removed so
|
|
206
|
+
* callers can safely perform the immediate, non-deferred cleanup pass.
|
|
207
|
+
*/
|
|
208
|
+
export declare function omitGlobalLegacyPromptFiles(detection: LegacyDetectionResult): LegacyDetectionResult;
|
|
209
|
+
/**
|
|
210
|
+
* Builds a detection snapshot containing only the selected global Codex prompt
|
|
211
|
+
* matches for replacement-gated cleanup.
|
|
212
|
+
*/
|
|
213
|
+
export declare function pickGlobalLegacyPromptFiles(detection: LegacyDetectionResult, filePaths: readonly string[]): LegacyDetectionResult;
|
|
155
214
|
/**
|
|
156
215
|
* Generates a migration hint message for project.md.
|
|
157
216
|
* This is shown when project.md exists and needs manual migration to config.yaml.
|