@fission-ai/openspec 1.7.0 → 1.9.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 +3 -1
- package/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.js +39 -5
- package/dist/commands/change.js +16 -1
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +5 -1
- package/dist/commands/schema.js +281 -47
- package/dist/commands/spec.js +30 -6
- package/dist/commands/validate.d.ts +23 -0
- package/dist/commands/validate.js +148 -4
- package/dist/commands/workflow/instructions.js +32 -27
- package/dist/commands/workflow/schemas.d.ts +2 -0
- package/dist/commands/workflow/schemas.js +9 -2
- package/dist/commands/workflow/status.js +2 -2
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -0
- package/dist/core/archive.js +1272 -151
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +3 -1
- package/dist/core/artifact-graph/instruction-loader.js +15 -5
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +0 -12
- package/dist/core/artifact-graph/resolver.js +49 -9
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +7 -0
- package/dist/core/change-status-policy.js +2 -2
- package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
- package/dist/core/command-generation/adapters/command-code.js +34 -0
- package/dist/core/command-generation/adapters/index.d.ts +1 -0
- package/dist/core/command-generation/adapters/index.js +1 -0
- package/dist/core/command-generation/registry.js +2 -0
- package/dist/core/completions/command-registry.js +13 -0
- package/dist/core/config-schema.d.ts +3 -0
- package/dist/core/config-schema.js +28 -1
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +40 -17
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +11 -0
- package/dist/core/init.d.ts +23 -0
- package/dist/core/init.js +253 -46
- package/dist/core/legacy-cleanup.d.ts +10 -0
- package/dist/core/legacy-cleanup.js +52 -28
- package/dist/core/migration.d.ts +6 -4
- package/dist/core/migration.js +78 -22
- package/dist/core/parsers/requirement-blocks.d.ts +10 -0
- package/dist/core/parsers/requirement-blocks.js +93 -1
- package/dist/core/parsers/requirement-text.d.ts +8 -0
- package/dist/core/parsers/requirement-text.js +4 -3
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +17 -1
- package/dist/core/profile-sync-drift.js +40 -5
- package/dist/core/project-config.d.ts +3 -0
- package/dist/core/project-config.js +31 -1
- package/dist/core/root-selection.d.ts +1 -1
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.js +66 -35
- package/dist/core/shared-skill-target.d.ts +31 -0
- package/dist/core/shared-skill-target.js +186 -0
- package/dist/core/specs-apply.d.ts +72 -0
- package/dist/core/specs-apply.js +459 -59
- package/dist/core/templates/skill-templates.d.ts +1 -1
- package/dist/core/templates/skill-templates.js +1 -1
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +25 -182
- package/dist/core/templates/workflows/archive-change.js +8 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +30 -26
- package/dist/core/templates/workflows/continue-change.js +6 -6
- package/dist/core/templates/workflows/explore.js +42 -18
- package/dist/core/templates/workflows/onboard.js +9 -3
- package/dist/core/templates/workflows/propose.js +72 -20
- 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 +96 -18
- package/dist/core/templates/workflows/update-change.js +10 -8
- package/dist/core/update.d.ts +1 -0
- package/dist/core/update.js +136 -30
- package/dist/core/validation/task-numbering.d.ts +16 -0
- package/dist/core/validation/task-numbering.js +57 -0
- package/dist/core/validation/validator.d.ts +26 -3
- package/dist/core/validation/validator.js +189 -19
- package/dist/core/version-check.js +6 -11
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +12 -5
- package/dist/telemetry/index.js +27 -9
- package/dist/utils/change-metadata.d.ts +17 -4
- package/dist/utils/change-metadata.js +26 -4
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +14 -2
- package/dist/utils/command-references.js +45 -3
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/interactive.d.ts +52 -0
- package/dist/utils/interactive.js +135 -0
- package/dist/utils/spec-discovery.d.ts +4 -4
- package/dist/utils/spec-discovery.js +19 -6
- package/dist/utils/task-progress.d.ts +51 -1
- package/dist/utils/task-progress.js +93 -41
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +8 -6
- package/schemas/spec-driven/templates/proposal.md +6 -4
package/dist/core/init.js
CHANGED
|
@@ -12,19 +12,21 @@ 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 { getSkillReferenceTransformer, getTransformerForTool } from '../utils/command-references.js';
|
|
15
|
+
import { getSkillReferenceTransformer, getTransformerForTool, usesNaturalLanguageSkillReferences } from '../utils/command-references.js';
|
|
16
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
21
|
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDeferredGlobalPromptSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
22
|
-
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, } from './shared/index.js';
|
|
22
|
+
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } 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 { writeSharedSkillTarget } from './shared-skill-target.js';
|
|
26
27
|
import { migrateIfNeeded, migrateLegacyToolDirs, describeLegacyMigration, keptInPlaceNotice, hasMovableContent, scanInstalledWorkflows as scanInstalledWorkflowsShared } from './migration.js';
|
|
27
28
|
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
29
|
+
import { writeCopilotCloudFiles, readCopilotCloudOptIn, hasExistingManagedCloudFiles, persistCopilotCloudOptIn, removeCopilotCloudFiles, findUnmanagedCloudFiles, listManagedCloudFiles, } from './github-copilot/cloud-agent.js';
|
|
28
30
|
const require = createRequire(import.meta.url);
|
|
29
31
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
30
32
|
// -----------------------------------------------------------------------------
|
|
@@ -58,12 +60,14 @@ export class InitCommand {
|
|
|
58
60
|
interactiveOption;
|
|
59
61
|
profileOverride;
|
|
60
62
|
animation;
|
|
63
|
+
copilotCloudOption;
|
|
61
64
|
constructor(options = {}) {
|
|
62
65
|
this.toolsArg = options.tools;
|
|
63
66
|
this.force = options.force ?? false;
|
|
64
67
|
this.interactiveOption = options.interactive;
|
|
65
68
|
this.profileOverride = options.profile;
|
|
66
69
|
this.animation = options.animation ?? true;
|
|
70
|
+
this.copilotCloudOption = options.copilotCloud;
|
|
67
71
|
}
|
|
68
72
|
async execute(targetPath) {
|
|
69
73
|
const projectPath = path.resolve(targetPath);
|
|
@@ -120,7 +124,7 @@ export class InitCommand {
|
|
|
120
124
|
// Get tool selection (pass detected tools for pre-selection)
|
|
121
125
|
const selectedToolIds = await this.getSelectedTools(toolStates, extendMode, detectedTools, projectPath);
|
|
122
126
|
// Validate selected tools
|
|
123
|
-
const validatedTools = this.validateTools(selectedToolIds, toolStates);
|
|
127
|
+
const validatedTools = this.validateTools(selectedToolIds, toolStates, projectPath);
|
|
124
128
|
// Selecting a renamed tool is consent to leave its former directory:
|
|
125
129
|
// init is about to write the current one, and leaving OpenSpec content
|
|
126
130
|
// behind would give the user two installs of the same tool.
|
|
@@ -132,10 +136,16 @@ export class InitCommand {
|
|
|
132
136
|
if (kept)
|
|
133
137
|
console.log(chalk.dim(kept));
|
|
134
138
|
}
|
|
139
|
+
// Decide whether to generate GitHub Copilot cloud files. This is opt-in
|
|
140
|
+
// (see cloud-agent.ts): selecting the Copilot tool no longer silently
|
|
141
|
+
// writes a GitHub Actions workflow into the user's .github/. The decision
|
|
142
|
+
// is made before generation so the write can be gated, and persisted after
|
|
143
|
+
// config.yaml exists so future non-interactive updates honor it.
|
|
144
|
+
const copilotDecision = await this.resolveCopilotCloudDecision(projectPath, validatedTools);
|
|
135
145
|
// Create directory structure and config
|
|
136
146
|
await this.createDirectoryStructure(openspecPath, extendMode);
|
|
137
147
|
// Generate skills and commands for each tool
|
|
138
|
-
const results = await this.generateSkillsAndCommands(projectPath, validatedTools);
|
|
148
|
+
const results = await this.generateSkillsAndCommands(projectPath, validatedTools, copilotDecision.write);
|
|
139
149
|
// Legacy cleanup was deferred to avoid interfering with skill/command generation;
|
|
140
150
|
// now that outputs are written, finalize the cleanup (e.g. remove stale files).
|
|
141
151
|
if (deferredLegacyCleanup) {
|
|
@@ -143,8 +153,49 @@ export class InitCommand {
|
|
|
143
153
|
}
|
|
144
154
|
// Create config.yaml if needed
|
|
145
155
|
const configStatus = await this.createConfig(openspecPath, extendMode);
|
|
156
|
+
// Persist an explicit Copilot cloud decision so `openspec update` (which
|
|
157
|
+
// never prompts) honors it. Best-effort: a config-write failure must not
|
|
158
|
+
// fail an otherwise-successful init.
|
|
159
|
+
if (copilotDecision.persist !== undefined) {
|
|
160
|
+
try {
|
|
161
|
+
await persistCopilotCloudOptIn(projectPath, copilotDecision.persist);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Non-fatal: the files (if any) were still written correctly.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// An explicit opt-out means "no cloud files here": clean up any that a
|
|
168
|
+
// previous run (or an older OpenSpec) generated. Only OpenSpec-managed
|
|
169
|
+
// files are removed — a user-customized file is preserved.
|
|
170
|
+
let copilotRemoved = 0;
|
|
171
|
+
if (copilotDecision.optedOut) {
|
|
172
|
+
try {
|
|
173
|
+
copilotRemoved = await removeCopilotCloudFiles(projectPath);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Non-fatal: removal targets files from a prior run; a failure here
|
|
177
|
+
// just leaves them for the next `openspec update` to clean up.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Report the cloud outcome from what is actually on disk after the write,
|
|
181
|
+
// not from the decision alone: writing over a user-owned file is a no-op,
|
|
182
|
+
// and the alternate-agent path can remove a managed file — so list only
|
|
183
|
+
// managed files that exist, and separately flag any left-untouched ones.
|
|
184
|
+
const copilotSucceeded = [...results.createdTools, ...results.refreshedTools].some((tool) => tool.value === 'github-copilot');
|
|
185
|
+
const wroteCloud = copilotDecision.write && copilotSucceeded;
|
|
186
|
+
const copilotPresent = wroteCloud ? await listManagedCloudFiles(projectPath) : [];
|
|
187
|
+
const copilotCollisions = wroteCloud ? await findUnmanagedCloudFiles(projectPath) : [];
|
|
146
188
|
// Display success message
|
|
147
|
-
this.displaySuccessMessage(projectPath, validatedTools, results, configStatus
|
|
189
|
+
this.displaySuccessMessage(projectPath, validatedTools, results, configStatus, {
|
|
190
|
+
write: copilotDecision.write,
|
|
191
|
+
skippedUndecided: copilotDecision.skippedUndecided,
|
|
192
|
+
present: copilotPresent,
|
|
193
|
+
collisions: copilotCollisions,
|
|
194
|
+
removed: copilotRemoved,
|
|
195
|
+
});
|
|
196
|
+
if (results.failedTools.length > 0) {
|
|
197
|
+
throw new Error(`OpenSpec setup failed for: ${results.failedTools.map((tool) => tool.name).join(', ')}`);
|
|
198
|
+
}
|
|
148
199
|
}
|
|
149
200
|
// ═══════════════════════════════════════════════════════════
|
|
150
201
|
// VALIDATION & SETUP
|
|
@@ -164,6 +215,59 @@ export class InitCommand {
|
|
|
164
215
|
return false;
|
|
165
216
|
return isInteractive({ interactive: this.interactiveOption });
|
|
166
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Decide whether to generate GitHub Copilot cloud files, and whether to
|
|
220
|
+
* persist that decision. Precedence:
|
|
221
|
+
* 1. `--copilot-cloud` / `--no-copilot-cloud` flag (explicit this run)
|
|
222
|
+
* 2. persisted opt-in in config.yaml
|
|
223
|
+
* 3. managed files already present (migration for pre-opt-in projects)
|
|
224
|
+
* 4. interactive confirm (default No)
|
|
225
|
+
* 5. non-interactive with no signal: skip, and don't persist a default
|
|
226
|
+
*
|
|
227
|
+
* @returns `write` — generate the files this run; `persist` — value to write
|
|
228
|
+
* back to config (undefined = leave config untouched); `optedOut` — the user
|
|
229
|
+
* explicitly declined, so any already-generated managed files should be
|
|
230
|
+
* removed; `skippedUndecided` — selected but no signal and couldn't ask, so
|
|
231
|
+
* the caller can hint that the opt-in exists.
|
|
232
|
+
*/
|
|
233
|
+
async resolveCopilotCloudDecision(projectPath, tools) {
|
|
234
|
+
const copilotSelected = tools.some((tool) => tool.value === 'github-copilot');
|
|
235
|
+
if (!copilotSelected) {
|
|
236
|
+
// A flag that can't apply is a likely mistake — say so rather than no-op.
|
|
237
|
+
if (this.copilotCloudOption !== undefined) {
|
|
238
|
+
console.log(chalk.yellow('--copilot-cloud/--no-copilot-cloud was ignored because the github-copilot tool was not selected.'));
|
|
239
|
+
}
|
|
240
|
+
return { write: false, optedOut: false, skippedUndecided: false };
|
|
241
|
+
}
|
|
242
|
+
if (this.copilotCloudOption !== undefined) {
|
|
243
|
+
return {
|
|
244
|
+
write: this.copilotCloudOption,
|
|
245
|
+
persist: this.copilotCloudOption,
|
|
246
|
+
optedOut: !this.copilotCloudOption,
|
|
247
|
+
skippedUndecided: false,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const persistedOptIn = readCopilotCloudOptIn(projectPath);
|
|
251
|
+
if (typeof persistedOptIn === 'boolean') {
|
|
252
|
+
return { write: persistedOptIn, optedOut: !persistedOptIn, skippedUndecided: false };
|
|
253
|
+
}
|
|
254
|
+
if (await hasExistingManagedCloudFiles(projectPath)) {
|
|
255
|
+
return { write: true, optedOut: false, skippedUndecided: false };
|
|
256
|
+
}
|
|
257
|
+
if (this.canPromptInteractively()) {
|
|
258
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
259
|
+
const answer = await confirm({
|
|
260
|
+
message: 'Set up GitHub Copilot cloud coding-agent files? This is for the GitHub-hosted ' +
|
|
261
|
+
'Copilot coding agent (github.com), not Copilot in your editor. It writes two files: ' +
|
|
262
|
+
'.github/workflows/copilot-setup-steps.yml and .github/agents/openspec.agent.md.',
|
|
263
|
+
default: false,
|
|
264
|
+
});
|
|
265
|
+
return { write: answer, persist: answer, optedOut: !answer, skippedUndecided: false };
|
|
266
|
+
}
|
|
267
|
+
// Non-interactive with no explicit signal: don't write, and leave the
|
|
268
|
+
// decision unpersisted so a later interactive run can still prompt.
|
|
269
|
+
return { write: false, optedOut: false, skippedUndecided: true };
|
|
270
|
+
}
|
|
167
271
|
resolveProfileOverride() {
|
|
168
272
|
if (this.profileOverride === undefined) {
|
|
169
273
|
return undefined;
|
|
@@ -411,24 +515,36 @@ export class InitCommand {
|
|
|
411
515
|
}
|
|
412
516
|
return deduped;
|
|
413
517
|
}
|
|
414
|
-
validateTools(toolIds, toolStates) {
|
|
518
|
+
validateTools(toolIds, toolStates, projectPath) {
|
|
415
519
|
const validatedTools = [];
|
|
416
|
-
|
|
520
|
+
const reconciledToolIds = toolIds.includes('codex') && toolIds.includes('agents')
|
|
521
|
+
? toolIds.filter((toolId) => toolId !== 'agents')
|
|
522
|
+
: toolIds;
|
|
523
|
+
if (reconciledToolIds.length !== toolIds.length) {
|
|
524
|
+
console.log(chalk.dim('Codex and agents share .agents/skills; writing one tree with Codex and generic skill references.'));
|
|
525
|
+
}
|
|
526
|
+
for (const toolId of reconciledToolIds) {
|
|
417
527
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
418
528
|
if (!tool) {
|
|
419
529
|
const validToolIds = getToolsWithSkillsDir();
|
|
420
530
|
throw new Error(`Unknown tool '${toolId}'. Valid tools:\n ${validToolIds.join('\n ')}`);
|
|
421
531
|
}
|
|
422
|
-
if (!tool
|
|
532
|
+
if (!toolSupportsSkills(tool)) {
|
|
423
533
|
const validToolsWithSkills = getToolsWithSkillsDir();
|
|
424
534
|
throw new Error(`Tool '${toolId}' does not support skill generation.\nTools with skill generation support:\n ${validToolsWithSkills.join('\n ')}`);
|
|
425
535
|
}
|
|
426
536
|
const preState = toolStates.get(tool.value);
|
|
537
|
+
const skillsPath = resolveToolSkillsDir(projectPath, tool);
|
|
538
|
+
const isGlobalSkillTarget = hasGlobalSkillTarget(tool);
|
|
427
539
|
validatedTools.push({
|
|
428
540
|
value: tool.value,
|
|
429
541
|
name: tool.name,
|
|
430
542
|
skillsDir: tool.skillsDir,
|
|
543
|
+
skillsPath,
|
|
544
|
+
skillsRoot: isGlobalSkillTarget ? skillsPath : projectPath,
|
|
545
|
+
isGlobalSkillTarget,
|
|
431
546
|
wasConfigured: preState?.configured ?? false,
|
|
547
|
+
requiresIdeRestart: tool.requiresIdeRestart,
|
|
432
548
|
});
|
|
433
549
|
}
|
|
434
550
|
return validatedTools;
|
|
@@ -446,6 +562,7 @@ export class InitCommand {
|
|
|
446
562
|
path.join(openspecPath, 'changes', 'archive'),
|
|
447
563
|
];
|
|
448
564
|
for (const dir of directories) {
|
|
565
|
+
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
|
|
449
566
|
await FileSystemUtils.createDirectory(dir);
|
|
450
567
|
}
|
|
451
568
|
return;
|
|
@@ -458,6 +575,7 @@ export class InitCommand {
|
|
|
458
575
|
path.join(openspecPath, 'changes', 'archive'),
|
|
459
576
|
];
|
|
460
577
|
for (const dir of directories) {
|
|
578
|
+
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
|
|
461
579
|
await FileSystemUtils.createDirectory(dir);
|
|
462
580
|
}
|
|
463
581
|
spinner.stopAndPersist({
|
|
@@ -476,7 +594,7 @@ export class InitCommand {
|
|
|
476
594
|
* @param tools - Selected tools with their skill directory metadata
|
|
477
595
|
* @returns Created, refreshed, and failed tools plus removed artifact counts
|
|
478
596
|
*/
|
|
479
|
-
async generateSkillsAndCommands(projectPath, tools) {
|
|
597
|
+
async generateSkillsAndCommands(projectPath, tools, writeCopilotCloud) {
|
|
480
598
|
const createdTools = [];
|
|
481
599
|
const refreshedTools = [];
|
|
482
600
|
const failedTools = [];
|
|
@@ -501,22 +619,24 @@ export class InitCommand {
|
|
|
501
619
|
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
502
620
|
// Generate skill files if the selected delivery and tool capability allow skills
|
|
503
621
|
if (shouldGenerateSkills) {
|
|
504
|
-
// Use tool-specific skillsDir
|
|
505
|
-
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
506
622
|
// Create skill directories and SKILL.md files
|
|
507
623
|
for (const { template, dirName } of skillTemplates) {
|
|
508
|
-
const skillDir = path.join(
|
|
624
|
+
const skillDir = path.join(tool.skillsPath, dirName);
|
|
509
625
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
510
626
|
// Generate SKILL.md content with YAML frontmatter including generatedBy
|
|
511
627
|
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
512
628
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
513
629
|
// Write the skill file
|
|
630
|
+
FileSystemUtils.assertPathWithin(tool.skillsRoot, skillFile);
|
|
514
631
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
515
632
|
}
|
|
633
|
+
writeSharedSkillTarget(projectPath, tool.value);
|
|
516
634
|
}
|
|
517
|
-
if (shouldRemoveSkillsForTool(tool.value, delivery)) {
|
|
518
|
-
|
|
519
|
-
|
|
635
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery) && !tool.isGlobalSkillTarget) {
|
|
636
|
+
removedSkillCount += await this.removeSkillDirs(tool.skillsRoot, tool.skillsPath);
|
|
637
|
+
// Retain an explicit selection even when this delivery mode produces
|
|
638
|
+
// no skills, so a divergent legacy sibling cannot reclaim ownership.
|
|
639
|
+
writeSharedSkillTarget(projectPath, tool.value);
|
|
520
640
|
}
|
|
521
641
|
// Generate commands if delivery includes commands
|
|
522
642
|
if (shouldGenerateCommands) {
|
|
@@ -524,7 +644,7 @@ export class InitCommand {
|
|
|
524
644
|
if (adapter) {
|
|
525
645
|
const generatedCommands = generateCommands(commandContents, adapter);
|
|
526
646
|
for (const cmd of generatedCommands) {
|
|
527
|
-
const commandFile =
|
|
647
|
+
const commandFile = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmd.path);
|
|
528
648
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
529
649
|
}
|
|
530
650
|
}
|
|
@@ -540,6 +660,9 @@ export class InitCommand {
|
|
|
540
660
|
if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
|
|
541
661
|
removedCommandCount += await this.removeCommandFiles(projectPath, tool.value);
|
|
542
662
|
}
|
|
663
|
+
if (tool.value === 'github-copilot' && writeCopilotCloud) {
|
|
664
|
+
await writeCopilotCloudFiles(projectPath);
|
|
665
|
+
}
|
|
543
666
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
544
667
|
if (tool.wasConfigured) {
|
|
545
668
|
refreshedTools.push(tool);
|
|
@@ -553,6 +676,16 @@ export class InitCommand {
|
|
|
553
676
|
failedTools.push({ name: tool.name, error: error });
|
|
554
677
|
}
|
|
555
678
|
}
|
|
679
|
+
for (const tool of [...createdTools, ...refreshedTools]) {
|
|
680
|
+
for (const migration of migrateLegacyToolDirs(projectPath, [tool.value], 'after-generation')) {
|
|
681
|
+
if (hasMovableContent(migration)) {
|
|
682
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
683
|
+
}
|
|
684
|
+
const kept = keptInPlaceNotice(migration);
|
|
685
|
+
if (kept)
|
|
686
|
+
console.log(chalk.dim(kept));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
556
689
|
return {
|
|
557
690
|
createdTools,
|
|
558
691
|
refreshedTools,
|
|
@@ -576,6 +709,7 @@ export class InitCommand {
|
|
|
576
709
|
}
|
|
577
710
|
try {
|
|
578
711
|
const yamlContent = serializeConfig({ schema: DEFAULT_SCHEMA });
|
|
712
|
+
FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), configPath);
|
|
579
713
|
await FileSystemUtils.writeFile(configPath, yamlContent);
|
|
580
714
|
return 'created';
|
|
581
715
|
}
|
|
@@ -586,9 +720,9 @@ export class InitCommand {
|
|
|
586
720
|
// ═══════════════════════════════════════════════════════════
|
|
587
721
|
// UI & OUTPUT
|
|
588
722
|
// ═══════════════════════════════════════════════════════════
|
|
589
|
-
displaySuccessMessage(projectPath, tools, results, configStatus) {
|
|
723
|
+
displaySuccessMessage(projectPath, tools, results, configStatus, copilot) {
|
|
590
724
|
console.log();
|
|
591
|
-
console.log(chalk.bold('OpenSpec Setup Complete'));
|
|
725
|
+
console.log(chalk.bold(results.failedTools.length > 0 ? 'OpenSpec Setup Incomplete' : 'OpenSpec Setup Complete'));
|
|
592
726
|
console.log();
|
|
593
727
|
// Show created vs refreshed tools
|
|
594
728
|
if (results.createdTools.length > 0) {
|
|
@@ -604,21 +738,56 @@ export class InitCommand {
|
|
|
604
738
|
const profile = this.profileOverride ?? globalConfig.profile ?? 'core';
|
|
605
739
|
const delivery = globalConfig.delivery ?? 'both';
|
|
606
740
|
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
741
|
+
const usesGlobalSkillTarget = successfulTools.some((tool) => tool.isGlobalSkillTarget);
|
|
742
|
+
if (!usesGlobalSkillTarget) {
|
|
743
|
+
const toolDirs = [
|
|
744
|
+
...new Set(successfulTools
|
|
745
|
+
.map((tool) => tool.skillsDir)
|
|
746
|
+
.filter((skillsDir) => Boolean(skillsDir))),
|
|
747
|
+
].join(', ');
|
|
748
|
+
const skillCount = successfulTools.some((tool) => shouldGenerateSkillsForTool(tool.value, delivery))
|
|
749
|
+
? getSkillTemplates(workflows).length
|
|
750
|
+
: 0;
|
|
751
|
+
const commandCount = successfulTools.some((tool) => shouldGenerateCommandsForTool(tool.value, delivery))
|
|
752
|
+
? getCommandContents(workflows).length
|
|
753
|
+
: 0;
|
|
754
|
+
if (skillCount > 0 && commandCount > 0) {
|
|
755
|
+
console.log(`${skillCount} skills and ${commandCount} commands in ${toolDirs}/`);
|
|
756
|
+
}
|
|
757
|
+
else if (skillCount > 0) {
|
|
758
|
+
console.log(`${skillCount} skills in ${toolDirs}/`);
|
|
759
|
+
}
|
|
760
|
+
else if (commandCount > 0) {
|
|
761
|
+
console.log(`${commandCount} commands in ${toolDirs}/`);
|
|
762
|
+
}
|
|
619
763
|
}
|
|
620
|
-
else
|
|
621
|
-
|
|
764
|
+
else {
|
|
765
|
+
const skillTools = successfulTools.filter((tool) => shouldGenerateSkillsForTool(tool.value, delivery));
|
|
766
|
+
const skillCount = skillTools.length * getSkillTemplates(workflows).length;
|
|
767
|
+
if (skillCount > 0) {
|
|
768
|
+
const skillDirs = [...new Set(skillTools.map((tool) => tool.skillsPath))];
|
|
769
|
+
console.log(`${skillCount} skills in ${skillDirs.join(', ')}`);
|
|
770
|
+
}
|
|
771
|
+
const commandContents = getCommandContents(workflows);
|
|
772
|
+
const commandTools = successfulTools.filter((tool) => shouldGenerateCommandsForTool(tool.value, delivery));
|
|
773
|
+
const commandCount = commandTools.length * commandContents.length;
|
|
774
|
+
if (commandCount > 0) {
|
|
775
|
+
const commandDirs = [
|
|
776
|
+
...new Set(commandTools.flatMap((tool) => {
|
|
777
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
778
|
+
if (!adapter)
|
|
779
|
+
return [];
|
|
780
|
+
return commandContents.map((command) => {
|
|
781
|
+
const commandPath = adapter.getFilePath(command.id);
|
|
782
|
+
const absolutePath = path.isAbsolute(commandPath)
|
|
783
|
+
? commandPath
|
|
784
|
+
: path.join(projectPath, commandPath);
|
|
785
|
+
return path.dirname(absolutePath);
|
|
786
|
+
});
|
|
787
|
+
})),
|
|
788
|
+
];
|
|
789
|
+
console.log(`${commandCount} commands in ${commandDirs.join(', ')}`);
|
|
790
|
+
}
|
|
622
791
|
}
|
|
623
792
|
}
|
|
624
793
|
// Show failures
|
|
@@ -638,6 +807,26 @@ export class InitCommand {
|
|
|
638
807
|
if (results.removedSkillCount > 0) {
|
|
639
808
|
console.log(chalk.dim(`Removed: ${results.removedSkillCount} skill directories (delivery: commands)`));
|
|
640
809
|
}
|
|
810
|
+
// GitHub Copilot cloud files are opt-in — report what is actually on disk:
|
|
811
|
+
// list the managed files that now exist (never files we didn't write), flag
|
|
812
|
+
// any user-owned file we left untouched, note an opt-out cleanup, or (when
|
|
813
|
+
// skipped for want of a signal) say how to turn them on.
|
|
814
|
+
const copilotSucceeded = successfulTools.some((tool) => tool.value === 'github-copilot');
|
|
815
|
+
if (copilotSucceeded && copilot.write) {
|
|
816
|
+
if (copilot.present.length > 0) {
|
|
817
|
+
console.log(`GitHub Copilot cloud files: ${copilot.present.join(', ')}`);
|
|
818
|
+
}
|
|
819
|
+
if (copilot.collisions.length > 0) {
|
|
820
|
+
console.log(chalk.dim(`Left your existing ${copilot.collisions.join(' and ')} untouched — add the OpenSpec ` +
|
|
821
|
+
`install step by hand so the Copilot cloud agent can run openspec.`));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else if (copilotSucceeded && copilot.removed > 0) {
|
|
825
|
+
console.log(chalk.dim(`Removed: ${copilot.removed} Copilot cloud agent file(s) (opted out of cloud files)`));
|
|
826
|
+
}
|
|
827
|
+
else if (copilotSucceeded && copilot.skippedUndecided) {
|
|
828
|
+
console.log(chalk.dim("Skipped GitHub Copilot cloud files (opt-in). Enable with 'openspec init --copilot-cloud'."));
|
|
829
|
+
}
|
|
641
830
|
// Show manual setup notes for tools that need extra configuration
|
|
642
831
|
for (const tool of successfulTools) {
|
|
643
832
|
const setupNote = AI_TOOLS.find((t) => t.value === tool.value)?.setupNote;
|
|
@@ -684,7 +873,13 @@ export class InitCommand {
|
|
|
684
873
|
hint = `Start your first change: ${transformer ? transformer(command) : command} "your idea"`;
|
|
685
874
|
}
|
|
686
875
|
else if (shouldGenerateSkillsForTool(tool.value, activeDelivery)) {
|
|
687
|
-
|
|
876
|
+
const skillReference = getSkillReferenceTransformer(tool.value)(command);
|
|
877
|
+
// Tools with no slash surface (e.g. Rovo Dev) reference skills as
|
|
878
|
+
// prose ("the openspec-propose skill"); phrase the hint so it reads
|
|
879
|
+
// as an instruction rather than a dead command with an argument.
|
|
880
|
+
hint = usesNaturalLanguageSkillReferences(tool.value)
|
|
881
|
+
? `Start your first change: ask ${tool.name} to use ${skillReference} with "your idea"`
|
|
882
|
+
: `Start your first change: ${skillReference} "your idea"`;
|
|
688
883
|
}
|
|
689
884
|
else {
|
|
690
885
|
continue;
|
|
@@ -736,14 +931,25 @@ export class InitCommand {
|
|
|
736
931
|
console.log();
|
|
737
932
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
738
933
|
console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`);
|
|
739
|
-
// Restart instruction
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
|
|
934
|
+
// Restart instruction only when at least one IDE/editor-resident tool
|
|
935
|
+
// actually received a generated surface. Two conditions, coupled to the SAME
|
|
936
|
+
// tool: (1) its commands/skills are loaded by a long-running editor process
|
|
937
|
+
// (CLI tools pick the files up immediately, so a restart line would be wrong
|
|
938
|
+
// for them — see #1067), and (2) a surface was actually generated for it
|
|
939
|
+
// under the active delivery (an IDE tool that generated nothing has nothing a
|
|
940
|
+
// restart would pick up, even if a co-configured CLI tool did generate).
|
|
941
|
+
// Wording follows what the IDE tool itself generated, not the global
|
|
942
|
+
// aggregate: it must not say "commands" when the IDE tool only got skills
|
|
943
|
+
// while a co-configured CLI tool got commands. Not "slash commands" either:
|
|
944
|
+
// Amazon Q's generated files are prompt-library entries invoked with @, so a
|
|
945
|
+
// restart line promising slash commands would be wrong for it.
|
|
946
|
+
const restartCommandsGenerated = successfulTools.some((tool) => tool.requiresIdeRestart &&
|
|
947
|
+
shouldGenerateCommandsForTool(tool.value, activeDelivery));
|
|
948
|
+
const restartSkillsGenerated = successfulTools.some((tool) => tool.requiresIdeRestart &&
|
|
949
|
+
shouldGenerateSkillsForTool(tool.value, activeDelivery));
|
|
950
|
+
if (restartCommandsGenerated || restartSkillsGenerated) {
|
|
745
951
|
console.log();
|
|
746
|
-
console.log(chalk.white(
|
|
952
|
+
console.log(chalk.white(restartCommandsGenerated
|
|
747
953
|
? 'Restart your IDE for the new commands to take effect.'
|
|
748
954
|
: 'Restart your IDE for the new skills to take effect.'));
|
|
749
955
|
}
|
|
@@ -757,18 +963,19 @@ export class InitCommand {
|
|
|
757
963
|
spinner: PROGRESS_SPINNER,
|
|
758
964
|
}).start();
|
|
759
965
|
}
|
|
760
|
-
async removeSkillDirs(skillsDir) {
|
|
966
|
+
async removeSkillDirs(skillsRoot, skillsDir) {
|
|
761
967
|
let removed = 0;
|
|
762
968
|
for (const workflow of ALL_WORKFLOWS) {
|
|
763
969
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
764
970
|
if (!dirName)
|
|
765
971
|
continue;
|
|
766
972
|
const skillDir = path.join(skillsDir, dirName);
|
|
973
|
+
if (!fs.existsSync(skillDir))
|
|
974
|
+
continue;
|
|
975
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
767
976
|
try {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
removed++;
|
|
771
|
-
}
|
|
977
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
978
|
+
removed++;
|
|
772
979
|
}
|
|
773
980
|
catch {
|
|
774
981
|
// Ignore errors
|
|
@@ -783,7 +990,7 @@ export class InitCommand {
|
|
|
783
990
|
return 0;
|
|
784
991
|
for (const workflow of ALL_WORKFLOWS) {
|
|
785
992
|
const cmdPath = adapter.getFilePath(workflow);
|
|
786
|
-
const fullPath =
|
|
993
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
787
994
|
try {
|
|
788
995
|
if (fs.existsSync(fullPath)) {
|
|
789
996
|
await fs.promises.unlink(fullPath);
|
|
@@ -206,6 +206,16 @@ export declare function getLegacyWorkflowIdsForTool(detection: LegacyDetectionRe
|
|
|
206
206
|
* callers can safely perform the immediate, non-deferred cleanup pass.
|
|
207
207
|
*/
|
|
208
208
|
export declare function omitGlobalLegacyPromptFiles(detection: LegacyDetectionResult): LegacyDetectionResult;
|
|
209
|
+
/**
|
|
210
|
+
* Returns a detection snapshot with the repo-local slash-command artifacts of
|
|
211
|
+
* the given tools removed. The legacy-upgrade path uses this to skip cleaning a
|
|
212
|
+
* tool's legacy files when its replacement was deliberately NOT written — e.g. a
|
|
213
|
+
* Codex upgrade suppressed because the shared `.agents` root is already owned by
|
|
214
|
+
* another tool. Deleting the legacy prompt without writing its replacement would
|
|
215
|
+
* violate the cleanup contract ("remove X because replacement Y now exists") and
|
|
216
|
+
* strip the tool's only OpenSpec integration.
|
|
217
|
+
*/
|
|
218
|
+
export declare function omitToolLegacyArtifacts(detection: LegacyDetectionResult, toolIds: readonly string[]): LegacyDetectionResult;
|
|
209
219
|
/**
|
|
210
220
|
* Builds a detection snapshot containing only the selected global Codex prompt
|
|
211
221
|
* matches for replacement-gated cleanup.
|
|
@@ -35,7 +35,6 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
35
35
|
'lingma': { type: 'directory', path: '.lingma/commands/openspec' },
|
|
36
36
|
'crush': { type: 'directory', path: '.crush/commands/openspec' },
|
|
37
37
|
'gemini': { type: 'directory', path: '.gemini/commands/openspec' },
|
|
38
|
-
'costrict': { type: 'directory', path: '.cospec/openspec/commands' },
|
|
39
38
|
// File-based: individual openspec-*.md files in a commands/workflows/prompts folder
|
|
40
39
|
'cursor': { type: 'files', pattern: '.cursor/commands/openspec-*.md' },
|
|
41
40
|
// Keyed by the tool id these map back to, so the pre-opsx Windsurf files
|
|
@@ -54,9 +53,12 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
54
53
|
'continue': { type: 'files', pattern: '.continue/prompts/openspec-*.prompt' },
|
|
55
54
|
'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
|
|
56
55
|
'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },
|
|
57
|
-
'junie': { type: 'files', pattern: ['.junie/commands/opsx-*.md', '.junie/commands/openspec-*.md'] },
|
|
58
56
|
'qwen': { type: 'files', pattern: ['.qwen/commands/opsx-*.toml', '.qwen/commands/openspec-*.toml'] },
|
|
59
57
|
'codex': { type: 'files', pattern: '.codex/prompts/openspec-*.md' },
|
|
58
|
+
// Keep this file-scoped: the CoStrict adapter writes `opsx-*.md` into the
|
|
59
|
+
// same folder, so a directory entry removes the live command files — and
|
|
60
|
+
// anything else the user keeps there — on every run.
|
|
61
|
+
'costrict': { type: 'files', pattern: '.cospec/openspec/commands/openspec-*.md' },
|
|
60
62
|
};
|
|
61
63
|
/**
|
|
62
64
|
* Final OpenSpec-managed global Codex prompt filenames mapped to the workflows
|
|
@@ -612,41 +614,42 @@ export function formatDeferredGlobalPromptSummary(detection) {
|
|
|
612
614
|
*/
|
|
613
615
|
export function getToolsFromLegacyArtifacts(detection) {
|
|
614
616
|
const tools = new Set();
|
|
615
|
-
// Match directories to tool IDs
|
|
616
617
|
for (const dir of detection.slashCommandDirs) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
break;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
618
|
+
const toolId = legacyToolIdForDir(dir);
|
|
619
|
+
if (toolId)
|
|
620
|
+
tools.add(toolId);
|
|
623
621
|
}
|
|
624
|
-
// Match files to tool IDs using glob patterns
|
|
625
622
|
for (const file of detection.slashCommandFiles) {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (pattern.type === 'files' && pattern.pattern) {
|
|
630
|
-
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
|
|
631
|
-
let matched = false;
|
|
632
|
-
for (const p of patterns) {
|
|
633
|
-
const regex = globToRegex(p);
|
|
634
|
-
if (regex.test(normalizedFile)) {
|
|
635
|
-
tools.add(toolId);
|
|
636
|
-
matched = true;
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
if (matched)
|
|
641
|
-
break;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
623
|
+
const toolId = legacyToolIdForFile(file);
|
|
624
|
+
if (toolId)
|
|
625
|
+
tools.add(toolId);
|
|
644
626
|
}
|
|
645
627
|
for (const prompt of getLegacyGlobalPromptMatches(detection)) {
|
|
646
628
|
tools.add(prompt.toolId);
|
|
647
629
|
}
|
|
648
630
|
return Array.from(tools);
|
|
649
631
|
}
|
|
632
|
+
/** The tool that owns a repo-local legacy slash-command directory, if any. */
|
|
633
|
+
function legacyToolIdForDir(dir) {
|
|
634
|
+
for (const [toolId, pattern] of Object.entries(LEGACY_SLASH_COMMAND_PATHS)) {
|
|
635
|
+
if (pattern.type === 'directory' && pattern.path === dir)
|
|
636
|
+
return toolId;
|
|
637
|
+
}
|
|
638
|
+
return undefined;
|
|
639
|
+
}
|
|
640
|
+
/** The tool that owns a repo-local legacy slash-command file, if any. */
|
|
641
|
+
function legacyToolIdForFile(file) {
|
|
642
|
+
// Normalize to forward slashes so the glob patterns match on Windows too.
|
|
643
|
+
const normalizedFile = normalizePathForMatch(file);
|
|
644
|
+
for (const [toolId, pattern] of Object.entries(LEGACY_SLASH_COMMAND_PATHS)) {
|
|
645
|
+
if (pattern.type !== 'files' || !pattern.pattern)
|
|
646
|
+
continue;
|
|
647
|
+
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
|
|
648
|
+
if (patterns.some((p) => globToRegex(p).test(normalizedFile)))
|
|
649
|
+
return toolId;
|
|
650
|
+
}
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
650
653
|
/**
|
|
651
654
|
* Normalizes global Codex prompt matches so callers can rely on workflow-aware
|
|
652
655
|
* metadata even when older detection results only carry file paths.
|
|
@@ -697,6 +700,27 @@ export function omitGlobalLegacyPromptFiles(detection) {
|
|
|
697
700
|
nextDetection.hasLegacyArtifacts = hasLegacyArtifacts(nextDetection);
|
|
698
701
|
return nextDetection;
|
|
699
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* Returns a detection snapshot with the repo-local slash-command artifacts of
|
|
705
|
+
* the given tools removed. The legacy-upgrade path uses this to skip cleaning a
|
|
706
|
+
* tool's legacy files when its replacement was deliberately NOT written — e.g. a
|
|
707
|
+
* Codex upgrade suppressed because the shared `.agents` root is already owned by
|
|
708
|
+
* another tool. Deleting the legacy prompt without writing its replacement would
|
|
709
|
+
* violate the cleanup contract ("remove X because replacement Y now exists") and
|
|
710
|
+
* strip the tool's only OpenSpec integration.
|
|
711
|
+
*/
|
|
712
|
+
export function omitToolLegacyArtifacts(detection, toolIds) {
|
|
713
|
+
if (toolIds.length === 0)
|
|
714
|
+
return detection;
|
|
715
|
+
const skip = new Set(toolIds);
|
|
716
|
+
const nextDetection = {
|
|
717
|
+
...detection,
|
|
718
|
+
slashCommandDirs: detection.slashCommandDirs.filter((dir) => !skip.has(legacyToolIdForDir(dir) ?? '')),
|
|
719
|
+
slashCommandFiles: detection.slashCommandFiles.filter((file) => !skip.has(legacyToolIdForFile(file) ?? '')),
|
|
720
|
+
};
|
|
721
|
+
nextDetection.hasLegacyArtifacts = hasLegacyArtifacts(nextDetection);
|
|
722
|
+
return nextDetection;
|
|
723
|
+
}
|
|
700
724
|
/**
|
|
701
725
|
* Builds a detection snapshot containing only the selected global Codex prompt
|
|
702
726
|
* matches for replacement-gated cleanup.
|