@fission-ai/openspec 1.6.0 → 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 +14 -12
- 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
|
@@ -2,6 +2,8 @@
|
|
|
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 path from 'path';
|
|
6
|
+
import os from 'os';
|
|
5
7
|
import { promises as fs } from 'fs';
|
|
6
8
|
import chalk from 'chalk';
|
|
7
9
|
import { FileSystemUtils, removeMarkerBlock as removeMarkerBlockUtil } from '../utils/file-system.js';
|
|
@@ -36,7 +38,10 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
36
38
|
'costrict': { type: 'directory', path: '.cospec/openspec/commands' },
|
|
37
39
|
// File-based: individual openspec-*.md files in a commands/workflows/prompts folder
|
|
38
40
|
'cursor': { type: 'files', pattern: '.cursor/commands/openspec-*.md' },
|
|
39
|
-
|
|
41
|
+
// Keyed by the tool id these map back to, so the pre-opsx Windsurf files
|
|
42
|
+
// belong to `devin` — the id Windsurf became. Only `.windsurf/` is listed:
|
|
43
|
+
// `.devin/` postdates the opsx rename and never held `openspec-*` files.
|
|
44
|
+
'devin': { type: 'files', pattern: '.windsurf/workflows/openspec-*.md' },
|
|
40
45
|
'kilocode': { type: 'files', pattern: '.kilocode/workflows/openspec-*.md' },
|
|
41
46
|
'kiro': { type: 'files', pattern: '.kiro/prompts/openspec-*.prompt.md' },
|
|
42
47
|
'github-copilot': { type: 'files', pattern: '.github/prompts/openspec-*.prompt.md' },
|
|
@@ -50,9 +55,83 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
50
55
|
'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
|
|
51
56
|
'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },
|
|
52
57
|
'junie': { type: 'files', pattern: ['.junie/commands/opsx-*.md', '.junie/commands/openspec-*.md'] },
|
|
53
|
-
'qwen': { type: 'files', pattern: '.qwen/commands/openspec-*.toml' },
|
|
58
|
+
'qwen': { type: 'files', pattern: ['.qwen/commands/opsx-*.toml', '.qwen/commands/openspec-*.toml'] },
|
|
54
59
|
'codex': { type: 'files', pattern: '.codex/prompts/openspec-*.md' },
|
|
55
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Final OpenSpec-managed global Codex prompt filenames mapped to the workflows
|
|
63
|
+
* they represented before Codex moved to skills-only delivery.
|
|
64
|
+
*/
|
|
65
|
+
const LEGACY_GLOBAL_CODEX_WORKFLOWS = {
|
|
66
|
+
'opsx-propose.md': ['propose'],
|
|
67
|
+
'opsx-explore.md': ['explore'],
|
|
68
|
+
'opsx-new.md': ['new'],
|
|
69
|
+
'opsx-continue.md': ['continue'],
|
|
70
|
+
'opsx-apply.md': ['apply'],
|
|
71
|
+
'opsx-update.md': ['update'],
|
|
72
|
+
'opsx-ff.md': ['ff'],
|
|
73
|
+
'opsx-sync.md': ['sync'],
|
|
74
|
+
'opsx-archive.md': ['archive'],
|
|
75
|
+
'opsx-bulk-archive.md': ['bulk-archive'],
|
|
76
|
+
'opsx-verify.md': ['verify'],
|
|
77
|
+
'opsx-onboard.md': ['onboard'],
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Global legacy prompt locations that live outside the project tree and require
|
|
81
|
+
* allowlisted matching instead of broad glob-based cleanup.
|
|
82
|
+
*/
|
|
83
|
+
export const LEGACY_GLOBAL_SLASH_COMMAND_PATHS = {
|
|
84
|
+
'codex': {
|
|
85
|
+
managedFileNames: Object.keys(LEGACY_GLOBAL_CODEX_WORKFLOWS),
|
|
86
|
+
workflowIdsByFileName: LEGACY_GLOBAL_CODEX_WORKFLOWS,
|
|
87
|
+
resolvePromptDir: getCodexPromptDir,
|
|
88
|
+
replacementLabel: 'Codex skills',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
// Resolve the Codex global prompts directory, respecting CODEX_HOME if set.
|
|
92
|
+
export function getCodexPromptDir() {
|
|
93
|
+
const envHome = process.env.CODEX_HOME?.trim();
|
|
94
|
+
const codexHome = envHome ? envHome : path.join(os.homedir(), '.codex');
|
|
95
|
+
return path.join(path.resolve(codexHome), 'prompts');
|
|
96
|
+
}
|
|
97
|
+
// Convert a simple glob pattern (only * wildcards) into an anchored RegExp.
|
|
98
|
+
function globToRegex(pattern) {
|
|
99
|
+
const regexPattern = pattern
|
|
100
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
101
|
+
.replace(/\*/g, '.*');
|
|
102
|
+
return new RegExp(`^${regexPattern}$`);
|
|
103
|
+
}
|
|
104
|
+
// Normalize Windows backslashes to forward slashes for cross-platform path matching.
|
|
105
|
+
function normalizePathForMatch(filePath) {
|
|
106
|
+
return filePath.replace(/\\/g, '/');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Classifies a global Codex prompt path as OpenSpec-managed only when it matches
|
|
110
|
+
* the explicit legacy allowlist for the resolved prompt home.
|
|
111
|
+
*/
|
|
112
|
+
function getManagedGlobalLegacyPromptMetadata(filePath) {
|
|
113
|
+
if (!path.isAbsolute(filePath)) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const resolvedPath = path.resolve(filePath);
|
|
117
|
+
for (const [toolId, pattern] of Object.entries(LEGACY_GLOBAL_SLASH_COMMAND_PATHS)) {
|
|
118
|
+
const promptDir = path.resolve(pattern.resolvePromptDir());
|
|
119
|
+
if (path.dirname(resolvedPath) !== promptDir) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const managedFileName = path.basename(resolvedPath);
|
|
123
|
+
if (pattern.managedFileNames.includes(managedFileName)) {
|
|
124
|
+
return {
|
|
125
|
+
path: resolvedPath,
|
|
126
|
+
toolId,
|
|
127
|
+
managedFileName,
|
|
128
|
+
workflowIds: pattern.workflowIdsByFileName?.[managedFileName] ?? [],
|
|
129
|
+
replacementLabel: pattern.replacementLabel,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
56
135
|
/**
|
|
57
136
|
* Detects all legacy OpenSpec artifacts in a project.
|
|
58
137
|
*
|
|
@@ -65,6 +144,8 @@ export async function detectLegacyArtifacts(projectPath) {
|
|
|
65
144
|
configFilesToUpdate: [],
|
|
66
145
|
slashCommandDirs: [],
|
|
67
146
|
slashCommandFiles: [],
|
|
147
|
+
globalSlashCommandFiles: [],
|
|
148
|
+
globalSlashCommandDetails: [],
|
|
68
149
|
hasOpenspecAgents: false,
|
|
69
150
|
hasProjectMd: false,
|
|
70
151
|
hasRootAgentsWithMarkers: false,
|
|
@@ -77,7 +158,10 @@ export async function detectLegacyArtifacts(projectPath) {
|
|
|
77
158
|
// Detect legacy slash commands
|
|
78
159
|
const slashResult = await detectLegacySlashCommands(projectPath);
|
|
79
160
|
result.slashCommandDirs = slashResult.directories;
|
|
80
|
-
result.slashCommandFiles = slashResult.files;
|
|
161
|
+
result.slashCommandFiles = [...new Set(slashResult.files)];
|
|
162
|
+
// Detect legacy global slash commands
|
|
163
|
+
result.globalSlashCommandDetails = await detectLegacyGlobalPromptFiles();
|
|
164
|
+
result.globalSlashCommandFiles = result.globalSlashCommandDetails.map((detail) => detail.path);
|
|
81
165
|
// Detect legacy structure files
|
|
82
166
|
const structureResult = await detectLegacyStructureFiles(projectPath);
|
|
83
167
|
result.hasOpenspecAgents = structureResult.hasOpenspecAgents;
|
|
@@ -88,6 +172,7 @@ export async function detectLegacyArtifacts(projectPath) {
|
|
|
88
172
|
result.configFiles.length > 0 ||
|
|
89
173
|
result.slashCommandDirs.length > 0 ||
|
|
90
174
|
result.slashCommandFiles.length > 0 ||
|
|
175
|
+
result.globalSlashCommandFiles.length > 0 ||
|
|
91
176
|
result.hasOpenspecAgents ||
|
|
92
177
|
result.hasRootAgentsWithMarkers ||
|
|
93
178
|
result.hasProjectMd;
|
|
@@ -125,7 +210,7 @@ export async function detectLegacyConfigFiles(projectPath) {
|
|
|
125
210
|
export async function detectLegacySlashCommands(projectPath) {
|
|
126
211
|
const directories = [];
|
|
127
212
|
const files = [];
|
|
128
|
-
for (const
|
|
213
|
+
for (const pattern of Object.values(LEGACY_SLASH_COMMAND_PATHS)) {
|
|
129
214
|
if (pattern.type === 'directory' && pattern.path) {
|
|
130
215
|
const dirPath = FileSystemUtils.joinPath(projectPath, pattern.path);
|
|
131
216
|
if (await FileSystemUtils.directoryExists(dirPath)) {
|
|
@@ -133,7 +218,6 @@ export async function detectLegacySlashCommands(projectPath) {
|
|
|
133
218
|
}
|
|
134
219
|
}
|
|
135
220
|
else if (pattern.type === 'files' && pattern.pattern) {
|
|
136
|
-
// For file-based patterns, check for individual files
|
|
137
221
|
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
|
|
138
222
|
for (const p of patterns) {
|
|
139
223
|
const foundFiles = await findLegacySlashCommandFiles(projectPath, p);
|
|
@@ -143,6 +227,37 @@ export async function detectLegacySlashCommands(projectPath) {
|
|
|
143
227
|
}
|
|
144
228
|
return { directories, files };
|
|
145
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Detects legacy global slash command files.
|
|
232
|
+
*
|
|
233
|
+
* @returns Object with individual files found
|
|
234
|
+
*/
|
|
235
|
+
/**
|
|
236
|
+
* Scans the resolved global Codex prompt directories and returns only the
|
|
237
|
+
* allowlisted OpenSpec-managed legacy prompt files.
|
|
238
|
+
*/
|
|
239
|
+
async function detectLegacyGlobalPromptFiles() {
|
|
240
|
+
const foundFiles = [];
|
|
241
|
+
for (const pattern of Object.values(LEGACY_GLOBAL_SLASH_COMMAND_PATHS)) {
|
|
242
|
+
const promptDir = pattern.resolvePromptDir();
|
|
243
|
+
try {
|
|
244
|
+
const entries = await fs.readdir(promptDir, { withFileTypes: true });
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
if (entry.isFile() && pattern.managedFileNames.includes(entry.name)) {
|
|
247
|
+
const fullPath = path.join(promptDir, entry.name);
|
|
248
|
+
const match = getManagedGlobalLegacyPromptMetadata(fullPath);
|
|
249
|
+
if (match) {
|
|
250
|
+
foundFiles.push(match);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Directory does not exist or cannot be read.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return foundFiles;
|
|
260
|
+
}
|
|
146
261
|
/**
|
|
147
262
|
* Finds legacy slash command files matching a glob pattern.
|
|
148
263
|
*
|
|
@@ -165,14 +280,7 @@ async function findLegacySlashCommandFiles(projectPath, pattern) {
|
|
|
165
280
|
}
|
|
166
281
|
try {
|
|
167
282
|
const entries = await fs.readdir(dirPath);
|
|
168
|
-
|
|
169
|
-
// openspec-*.md -> /^openspec-.*\.md$/
|
|
170
|
-
// openspec-*.prompt.md -> /^openspec-.*\.prompt\.md$/
|
|
171
|
-
// openspec-*.toml -> /^openspec-.*\.toml$/
|
|
172
|
-
const regexPattern = filePart
|
|
173
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
|
|
174
|
-
.replace(/\*/g, '.*'); // Replace * with .*
|
|
175
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
283
|
+
const regex = globToRegex(filePart);
|
|
176
284
|
for (const entry of entries) {
|
|
177
285
|
if (regex.test(entry)) {
|
|
178
286
|
// Use forward slashes for consistency in relative paths (cross-platform)
|
|
@@ -257,6 +365,7 @@ export function removeMarkerBlock(content) {
|
|
|
257
365
|
export async function cleanupLegacyArtifacts(projectPath, detection) {
|
|
258
366
|
const result = {
|
|
259
367
|
deletedFiles: [],
|
|
368
|
+
deletedFileReplacementLabels: {},
|
|
260
369
|
modifiedFiles: [],
|
|
261
370
|
deletedDirs: [],
|
|
262
371
|
projectMdNeedsMigration: detection.hasProjectMd,
|
|
@@ -299,6 +408,25 @@ export async function cleanupLegacyArtifacts(projectPath, detection) {
|
|
|
299
408
|
result.errors.push(`Failed to delete ${filePath}: ${error.message}`);
|
|
300
409
|
}
|
|
301
410
|
}
|
|
411
|
+
// Delete managed global slash command files (these are 100% OpenSpec-managed)
|
|
412
|
+
const globalPromptMatchesByPath = new Map(getLegacyGlobalPromptMatches(detection).map((prompt) => [prompt.path, prompt]));
|
|
413
|
+
for (const filePath of detection.globalSlashCommandFiles) {
|
|
414
|
+
if (!getManagedGlobalLegacyPromptMetadata(filePath)) {
|
|
415
|
+
result.errors.push(`Skipped unmanaged global prompt ${filePath}`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
await fs.unlink(filePath);
|
|
420
|
+
result.deletedFiles.push(filePath);
|
|
421
|
+
const promptMatch = globalPromptMatchesByPath.get(filePath);
|
|
422
|
+
if (promptMatch?.replacementLabel) {
|
|
423
|
+
result.deletedFileReplacementLabels[filePath] = promptMatch.replacementLabel;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
result.errors.push(`Failed to delete ${filePath}: ${error.message}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
302
430
|
// Delete openspec/AGENTS.md (this is inside openspec/, it's OpenSpec-managed)
|
|
303
431
|
if (detection.hasOpenspecAgents) {
|
|
304
432
|
const agentsPath = FileSystemUtils.joinPath(projectPath, 'openspec', 'AGENTS.md');
|
|
@@ -328,10 +456,15 @@ export function formatCleanupSummary(result) {
|
|
|
328
456
|
if (result.deletedFiles.length > 0 || result.deletedDirs.length > 0 || result.modifiedFiles.length > 0) {
|
|
329
457
|
lines.push('Cleaned up legacy files:');
|
|
330
458
|
for (const file of result.deletedFiles) {
|
|
331
|
-
|
|
459
|
+
const replacementLabel = result.deletedFileReplacementLabels?.[file]
|
|
460
|
+
?? getManagedGlobalLegacyPromptMetadata(file)?.replacementLabel;
|
|
461
|
+
const replacement = replacementLabel
|
|
462
|
+
? ` (replaced by ${replacementLabel})`
|
|
463
|
+
: '';
|
|
464
|
+
lines.push(` ✓ Removed ${file}${replacement}`);
|
|
332
465
|
}
|
|
333
466
|
for (const dir of result.deletedDirs) {
|
|
334
|
-
lines.push(` ✓ Removed ${dir}/ (replaced by
|
|
467
|
+
lines.push(` ✓ Removed ${dir}/ (replaced by OpenSpec skills and commands)`);
|
|
335
468
|
}
|
|
336
469
|
for (const file of result.modifiedFiles) {
|
|
337
470
|
lines.push(` ✓ Removed OpenSpec markers from ${file}`);
|
|
@@ -374,6 +507,13 @@ function buildRemovalsList(detection) {
|
|
|
374
507
|
for (const file of detection.slashCommandFiles) {
|
|
375
508
|
removals.push({ path: file, explanation: 'replaced by skills/' });
|
|
376
509
|
}
|
|
510
|
+
// Managed global slash command files
|
|
511
|
+
for (const prompt of getLegacyGlobalPromptMatches(detection)) {
|
|
512
|
+
const explanation = prompt.toolId
|
|
513
|
+
? `replaced by .${prompt.toolId}/skills/`
|
|
514
|
+
: 'replaced by skills/';
|
|
515
|
+
removals.push({ path: prompt.path, explanation });
|
|
516
|
+
}
|
|
377
517
|
// openspec/AGENTS.md (inside openspec/, it's OpenSpec-managed)
|
|
378
518
|
if (detection.hasOpenspecAgents) {
|
|
379
519
|
removals.push({ path: 'openspec/AGENTS.md', explanation: 'obsolete workflow file' });
|
|
@@ -445,6 +585,24 @@ export function formatDetectionSummary(detection) {
|
|
|
445
585
|
}
|
|
446
586
|
return lines.join('\n');
|
|
447
587
|
}
|
|
588
|
+
/**
|
|
589
|
+
* Generates a summary for managed global prompt files whose cleanup must wait
|
|
590
|
+
* until replacement skills are installed.
|
|
591
|
+
*/
|
|
592
|
+
export function formatDeferredGlobalPromptSummary(detection) {
|
|
593
|
+
const deferredPrompts = getLegacyGlobalPromptMatches(detection);
|
|
594
|
+
if (deferredPrompts.length === 0) {
|
|
595
|
+
return '';
|
|
596
|
+
}
|
|
597
|
+
const lines = [];
|
|
598
|
+
lines.push(chalk.bold('Deferred global prompts cleanup'));
|
|
599
|
+
lines.push(chalk.dim('These global prompts will only be removed after matching replacement skills are installed.'));
|
|
600
|
+
for (const prompt of deferredPrompts) {
|
|
601
|
+
const toolLabel = prompt.toolId ? `${prompt.toolId}: ` : '';
|
|
602
|
+
lines.push(` • ${toolLabel}${prompt.path}`);
|
|
603
|
+
}
|
|
604
|
+
return lines.join('\n');
|
|
605
|
+
}
|
|
448
606
|
/**
|
|
449
607
|
* Extract tool IDs from detected legacy artifacts.
|
|
450
608
|
* Uses LEGACY_SLASH_COMMAND_PATHS to map paths back to tool IDs.
|
|
@@ -466,18 +624,13 @@ export function getToolsFromLegacyArtifacts(detection) {
|
|
|
466
624
|
// Match files to tool IDs using glob patterns
|
|
467
625
|
for (const file of detection.slashCommandFiles) {
|
|
468
626
|
// Normalize file path to use forward slashes for consistent matching (Windows compatibility)
|
|
469
|
-
const normalizedFile = file
|
|
627
|
+
const normalizedFile = normalizePathForMatch(file);
|
|
470
628
|
for (const [toolId, pattern] of Object.entries(LEGACY_SLASH_COMMAND_PATHS)) {
|
|
471
629
|
if (pattern.type === 'files' && pattern.pattern) {
|
|
472
|
-
// Convert glob pattern to regex for matching
|
|
473
|
-
// e.g., '.cursor/commands/openspec-*.md' -> /^\.cursor\/commands\/openspec-.*\.md$/
|
|
474
630
|
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
|
|
475
631
|
let matched = false;
|
|
476
632
|
for (const p of patterns) {
|
|
477
|
-
const
|
|
478
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
|
|
479
|
-
.replace(/\*/g, '.*'); // Replace * with .*
|
|
480
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
633
|
+
const regex = globToRegex(p);
|
|
481
634
|
if (regex.test(normalizedFile)) {
|
|
482
635
|
tools.add(toolId);
|
|
483
636
|
matched = true;
|
|
@@ -489,8 +642,82 @@ export function getToolsFromLegacyArtifacts(detection) {
|
|
|
489
642
|
}
|
|
490
643
|
}
|
|
491
644
|
}
|
|
645
|
+
for (const prompt of getLegacyGlobalPromptMatches(detection)) {
|
|
646
|
+
tools.add(prompt.toolId);
|
|
647
|
+
}
|
|
492
648
|
return Array.from(tools);
|
|
493
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Normalizes global Codex prompt matches so callers can rely on workflow-aware
|
|
652
|
+
* metadata even when older detection results only carry file paths.
|
|
653
|
+
*/
|
|
654
|
+
export function getLegacyGlobalPromptMatches(detection) {
|
|
655
|
+
if (detection.globalSlashCommandDetails && detection.globalSlashCommandDetails.length > 0) {
|
|
656
|
+
return detection.globalSlashCommandDetails;
|
|
657
|
+
}
|
|
658
|
+
return detection.globalSlashCommandFiles
|
|
659
|
+
.map((filePath) => getManagedGlobalLegacyPromptMetadata(filePath))
|
|
660
|
+
.filter((match) => match !== undefined);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Collects workflow IDs inferred from detected legacy global prompts for a
|
|
664
|
+
* specific tool.
|
|
665
|
+
*/
|
|
666
|
+
export function getLegacyWorkflowIdsForTool(detection, toolId) {
|
|
667
|
+
const workflows = new Set();
|
|
668
|
+
for (const prompt of getLegacyGlobalPromptMatches(detection)) {
|
|
669
|
+
if (prompt.toolId !== toolId) {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
for (const workflowId of prompt.workflowIds) {
|
|
673
|
+
workflows.add(workflowId);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return Array.from(workflows);
|
|
677
|
+
}
|
|
678
|
+
function hasLegacyArtifacts(detection) {
|
|
679
|
+
return (detection.configFiles.length > 0 ||
|
|
680
|
+
detection.slashCommandDirs.length > 0 ||
|
|
681
|
+
detection.slashCommandFiles.length > 0 ||
|
|
682
|
+
detection.globalSlashCommandFiles.length > 0 ||
|
|
683
|
+
detection.hasOpenspecAgents ||
|
|
684
|
+
detection.hasRootAgentsWithMarkers ||
|
|
685
|
+
detection.hasProjectMd);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Returns a detection snapshot with global Codex prompt cleanup removed so
|
|
689
|
+
* callers can safely perform the immediate, non-deferred cleanup pass.
|
|
690
|
+
*/
|
|
691
|
+
export function omitGlobalLegacyPromptFiles(detection) {
|
|
692
|
+
const nextDetection = {
|
|
693
|
+
...detection,
|
|
694
|
+
globalSlashCommandFiles: [],
|
|
695
|
+
globalSlashCommandDetails: [],
|
|
696
|
+
};
|
|
697
|
+
nextDetection.hasLegacyArtifacts = hasLegacyArtifacts(nextDetection);
|
|
698
|
+
return nextDetection;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Builds a detection snapshot containing only the selected global Codex prompt
|
|
702
|
+
* matches for replacement-gated cleanup.
|
|
703
|
+
*/
|
|
704
|
+
export function pickGlobalLegacyPromptFiles(detection, filePaths) {
|
|
705
|
+
const selectedPaths = new Set(filePaths.map((filePath) => path.resolve(filePath)));
|
|
706
|
+
const details = getLegacyGlobalPromptMatches(detection)
|
|
707
|
+
.filter((detail) => selectedPaths.has(path.resolve(detail.path)));
|
|
708
|
+
return {
|
|
709
|
+
configFiles: [],
|
|
710
|
+
configFilesToUpdate: [],
|
|
711
|
+
slashCommandDirs: [],
|
|
712
|
+
slashCommandFiles: [],
|
|
713
|
+
globalSlashCommandFiles: details.map((detail) => detail.path),
|
|
714
|
+
globalSlashCommandDetails: details,
|
|
715
|
+
hasOpenspecAgents: false,
|
|
716
|
+
hasProjectMd: false,
|
|
717
|
+
hasRootAgentsWithMarkers: false,
|
|
718
|
+
hasLegacyArtifacts: details.length > 0,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
494
721
|
/**
|
|
495
722
|
* Generates a migration hint message for project.md.
|
|
496
723
|
* This is shown when project.md exists and needs manual migration to config.yaml.
|
package/dist/core/list.js
CHANGED
|
@@ -2,8 +2,8 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
4
4
|
import { readFileSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
6
5
|
import { MarkdownParser } from './parsers/markdown-parser.js';
|
|
6
|
+
import { discoverSpecFiles } from '../utils/spec-discovery.js';
|
|
7
7
|
function isMissingPathError(error) {
|
|
8
8
|
return (typeof error === 'object' &&
|
|
9
9
|
error !== null &&
|
|
@@ -152,9 +152,8 @@ export class ListCommand {
|
|
|
152
152
|
}
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
if (specDirs.length === 0) {
|
|
155
|
+
const discovered = await discoverSpecFiles(specsDir);
|
|
156
|
+
if (discovered.length === 0) {
|
|
158
157
|
if (json) {
|
|
159
158
|
console.log(JSON.stringify({ specs: [], ...(root ? { root } : {}) }, null, 2));
|
|
160
159
|
}
|
|
@@ -164,10 +163,9 @@ export class ListCommand {
|
|
|
164
163
|
return;
|
|
165
164
|
}
|
|
166
165
|
const specs = [];
|
|
167
|
-
for (const id of
|
|
168
|
-
const specPath = join(specsDir, id, 'spec.md');
|
|
166
|
+
for (const { id, specFile } of discovered) {
|
|
169
167
|
try {
|
|
170
|
-
const content = readFileSync(
|
|
168
|
+
const content = readFileSync(specFile, 'utf-8');
|
|
171
169
|
const parser = new MarkdownParser(content);
|
|
172
170
|
const spec = parser.parseSpec(id);
|
|
173
171
|
specs.push({ id, requirementCount: spec.requirements.length });
|
package/dist/core/migration.d.ts
CHANGED
|
@@ -4,7 +4,77 @@
|
|
|
4
4
|
* One-time migration logic for existing projects when profile system is introduced.
|
|
5
5
|
* Called by both init and update commands before profile resolution.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type AIToolOption } from './config.js';
|
|
8
|
+
export interface LegacyToolRoot {
|
|
9
|
+
/** Former tool root, e.g. '.kimi' */
|
|
10
|
+
root: string;
|
|
11
|
+
/**
|
|
12
|
+
* Whether leaving this root requires the user's say-so. False when the old
|
|
13
|
+
* product is gone and its directory is certainly dead. True when the old
|
|
14
|
+
* location may still be the live one for somebody.
|
|
15
|
+
*/
|
|
16
|
+
needsConsent: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Former tool roots whose OpenSpec-managed content belongs under the tool's
|
|
20
|
+
* current skillsDir. User files are never touched.
|
|
21
|
+
*/
|
|
22
|
+
export declare const LEGACY_TOOL_ROOTS: Record<string, LegacyToolRoot[]>;
|
|
23
|
+
export interface LegacyToolMigration {
|
|
24
|
+
toolId: string;
|
|
25
|
+
/** Legacy tool root, e.g. '.windsurf' */
|
|
26
|
+
from: string;
|
|
27
|
+
/** Current tool root, e.g. '.devin' */
|
|
28
|
+
to: string;
|
|
29
|
+
/** Skill directories that moved, or would move */
|
|
30
|
+
skillDirs: number;
|
|
31
|
+
/** Command files that moved, or would move */
|
|
32
|
+
commandFiles: number;
|
|
33
|
+
/**
|
|
34
|
+
* OpenSpec-managed files left under the legacy root because the copy there
|
|
35
|
+
* differs from the one that survives — the user edited it, so it is reported
|
|
36
|
+
* rather than dropped.
|
|
37
|
+
*/
|
|
38
|
+
keptInPlace: number;
|
|
39
|
+
/** Whether this move needs the user's consent first */
|
|
40
|
+
needsConsent: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Reports the OpenSpec content sitting under each tool's legacy root, without
|
|
44
|
+
* moving anything. Callers use this to ask before a move that needs consent.
|
|
45
|
+
*/
|
|
46
|
+
export declare function findLegacyToolMigrations(projectPath: string): LegacyToolMigration[];
|
|
47
|
+
/**
|
|
48
|
+
* Moves OpenSpec-managed skill directories (openspec-*) and command files
|
|
49
|
+
* (opsx-*) from a tool's legacy root to its current one. When the destination
|
|
50
|
+
* already exists the legacy copy is removed instead. Legacy directories are
|
|
51
|
+
* deleted only when left empty, so user files under the old location — a
|
|
52
|
+
* hand-written Cascade workflow next to the generated ones — are preserved.
|
|
53
|
+
*
|
|
54
|
+
* @param projectPath - Project root
|
|
55
|
+
* @param toolIds - Restrict the move to these tools; omit to move every tool
|
|
56
|
+
* whose legacy root needs no consent
|
|
57
|
+
*/
|
|
58
|
+
export declare function migrateLegacyToolDirs(projectPath: string, toolIds?: string[]): LegacyToolMigration[];
|
|
59
|
+
/**
|
|
60
|
+
* Summarizes what a migration moved, e.g. "6 skills and 6 commands".
|
|
61
|
+
*/
|
|
62
|
+
export declare function describeLegacyMigration(migration: LegacyToolMigration): string;
|
|
63
|
+
/**
|
|
64
|
+
* Names OpenSpec-managed files the move deliberately left behind, so a user
|
|
65
|
+
* who customized one knows there are now two copies to reconcile.
|
|
66
|
+
*/
|
|
67
|
+
export declare function keptInPlaceNotice(migration: LegacyToolMigration): string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Whether a migration has anything to move, as opposed to only files left in
|
|
70
|
+
* place. Callers use this to avoid offering a move of nothing.
|
|
71
|
+
*/
|
|
72
|
+
export declare function hasMovableContent(migration: LegacyToolMigration): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Explains why a consent-gated move is being offered, in the user's terms.
|
|
75
|
+
* Keyed by tool so the reason is specific rather than a generic "files moved".
|
|
76
|
+
*/
|
|
77
|
+
export declare function legacyMigrationNotice(migration: LegacyToolMigration): string;
|
|
8
78
|
/**
|
|
9
79
|
* Scans installed workflow files across all detected tools and returns
|
|
10
80
|
* the union of installed workflow IDs.
|