openspec-cn 0.23.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/LICENSE +22 -0
- package/README.md +153 -0
- package/bin/openspec.js +3 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +480 -0
- package/dist/commands/change.d.ts +35 -0
- package/dist/commands/change.js +277 -0
- package/dist/commands/completion.d.ts +72 -0
- package/dist/commands/completion.js +257 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +198 -0
- package/dist/commands/feedback.d.ts +9 -0
- package/dist/commands/feedback.js +183 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.js +869 -0
- package/dist/commands/show.d.ts +14 -0
- package/dist/commands/show.js +132 -0
- package/dist/commands/spec.d.ts +15 -0
- package/dist/commands/spec.js +225 -0
- package/dist/commands/validate.d.ts +24 -0
- package/dist/commands/validate.js +294 -0
- package/dist/commands/workflow/index.d.ts +17 -0
- package/dist/commands/workflow/index.js +12 -0
- package/dist/commands/workflow/instructions.d.ts +29 -0
- package/dist/commands/workflow/instructions.js +381 -0
- package/dist/commands/workflow/new-change.d.ts +11 -0
- package/dist/commands/workflow/new-change.js +44 -0
- package/dist/commands/workflow/schemas.d.ts +10 -0
- package/dist/commands/workflow/schemas.js +34 -0
- package/dist/commands/workflow/shared.d.ts +52 -0
- package/dist/commands/workflow/shared.js +111 -0
- package/dist/commands/workflow/status.d.ts +14 -0
- package/dist/commands/workflow/status.js +58 -0
- package/dist/commands/workflow/templates.d.ts +16 -0
- package/dist/commands/workflow/templates.js +68 -0
- package/dist/core/archive.d.ts +11 -0
- package/dist/core/archive.js +280 -0
- package/dist/core/artifact-graph/graph.d.ts +56 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +7 -0
- package/dist/core/artifact-graph/index.js +13 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +143 -0
- package/dist/core/artifact-graph/instruction-loader.js +214 -0
- package/dist/core/artifact-graph/resolver.d.ts +81 -0
- package/dist/core/artifact-graph/resolver.js +257 -0
- package/dist/core/artifact-graph/schema.d.ts +13 -0
- package/dist/core/artifact-graph/schema.js +108 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +54 -0
- package/dist/core/artifact-graph/types.d.ts +45 -0
- package/dist/core/artifact-graph/types.js +43 -0
- package/dist/core/command-generation/adapters/amazon-q.d.ts +13 -0
- package/dist/core/command-generation/adapters/amazon-q.js +26 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +26 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +27 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +50 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +28 -0
- package/dist/core/command-generation/adapters/codex.d.ts +13 -0
- package/dist/core/command-generation/adapters/codex.js +27 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +28 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +27 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +44 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +27 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +26 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +26 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +29 -0
- package/dist/core/command-generation/adapters/index.d.ts +27 -0
- package/dist/core/command-generation/adapters/index.js +27 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +13 -0
- package/dist/core/command-generation/adapters/opencode.js +26 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +13 -0
- package/dist/core/command-generation/adapters/qwen.js +26 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- package/dist/core/command-generation/adapters/windsurf.d.ts +14 -0
- package/dist/core/command-generation/adapters/windsurf.js +51 -0
- package/dist/core/command-generation/generator.d.ts +21 -0
- package/dist/core/command-generation/generator.js +27 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- package/dist/core/command-generation/registry.d.ts +36 -0
- package/dist/core/command-generation/registry.js +88 -0
- package/dist/core/command-generation/types.d.ts +55 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/completions/command-registry.d.ts +7 -0
- package/dist/core/completions/command-registry.js +456 -0
- package/dist/core/completions/completion-provider.d.ts +60 -0
- package/dist/core/completions/completion-provider.js +102 -0
- package/dist/core/completions/factory.d.ts +64 -0
- package/dist/core/completions/factory.js +75 -0
- package/dist/core/completions/generators/bash-generator.d.ts +32 -0
- package/dist/core/completions/generators/bash-generator.js +174 -0
- package/dist/core/completions/generators/fish-generator.d.ts +32 -0
- package/dist/core/completions/generators/fish-generator.js +157 -0
- package/dist/core/completions/generators/powershell-generator.d.ts +33 -0
- package/dist/core/completions/generators/powershell-generator.js +207 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +44 -0
- package/dist/core/completions/generators/zsh-generator.js +250 -0
- package/dist/core/completions/installers/bash-installer.d.ts +87 -0
- package/dist/core/completions/installers/bash-installer.js +318 -0
- package/dist/core/completions/installers/fish-installer.d.ts +43 -0
- package/dist/core/completions/installers/fish-installer.js +143 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +88 -0
- package/dist/core/completions/installers/powershell-installer.js +327 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +125 -0
- package/dist/core/completions/installers/zsh-installer.js +449 -0
- package/dist/core/completions/templates/bash-templates.d.ts +6 -0
- package/dist/core/completions/templates/bash-templates.js +24 -0
- package/dist/core/completions/templates/fish-templates.d.ts +7 -0
- package/dist/core/completions/templates/fish-templates.js +39 -0
- package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
- package/dist/core/completions/templates/powershell-templates.js +25 -0
- package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
- package/dist/core/completions/templates/zsh-templates.js +36 -0
- package/dist/core/completions/types.d.ts +79 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +34 -0
- package/dist/core/config-schema.d.ts +76 -0
- package/dist/core/config-schema.js +200 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.js +30 -0
- package/dist/core/converters/json-converter.d.ts +6 -0
- package/dist/core/converters/json-converter.js +51 -0
- package/dist/core/global-config.d.ts +39 -0
- package/dist/core/global-config.js +115 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/init.d.ts +32 -0
- package/dist/core/init.js +433 -0
- package/dist/core/legacy-cleanup.d.ts +162 -0
- package/dist/core/legacy-cleanup.js +501 -0
- package/dist/core/list.d.ts +9 -0
- package/dist/core/list.js +171 -0
- package/dist/core/parsers/change-parser.d.ts +13 -0
- package/dist/core/parsers/change-parser.js +193 -0
- package/dist/core/parsers/markdown-parser.d.ts +22 -0
- package/dist/core/parsers/markdown-parser.js +187 -0
- package/dist/core/parsers/requirement-blocks.d.ts +37 -0
- package/dist/core/parsers/requirement-blocks.js +201 -0
- package/dist/core/project-config.d.ts +64 -0
- package/dist/core/project-config.js +223 -0
- package/dist/core/schemas/base.schema.d.ts +13 -0
- package/dist/core/schemas/base.schema.js +13 -0
- package/dist/core/schemas/change.schema.d.ts +73 -0
- package/dist/core/schemas/change.schema.js +31 -0
- package/dist/core/schemas/index.d.ts +4 -0
- package/dist/core/schemas/index.js +4 -0
- package/dist/core/schemas/spec.schema.d.ts +18 -0
- package/dist/core/schemas/spec.schema.js +15 -0
- package/dist/core/shared/index.d.ts +8 -0
- package/dist/core/shared/index.js +8 -0
- package/dist/core/shared/skill-generation.d.ts +41 -0
- package/dist/core/shared/skill-generation.js +74 -0
- package/dist/core/shared/tool-detection.d.ts +66 -0
- package/dist/core/shared/tool-detection.js +140 -0
- package/dist/core/specs-apply.d.ts +73 -0
- package/dist/core/specs-apply.js +384 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/index.d.ts +8 -0
- package/dist/core/templates/index.js +9 -0
- package/dist/core/templates/skill-templates.d.ts +112 -0
- package/dist/core/templates/skill-templates.js +2893 -0
- package/dist/core/update.d.ts +42 -0
- package/dist/core/update.js +306 -0
- package/dist/core/validation/constants.d.ts +34 -0
- package/dist/core/validation/constants.js +40 -0
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.js +2 -0
- package/dist/core/validation/validator.d.ts +33 -0
- package/dist/core/validation/validator.js +409 -0
- package/dist/core/view.d.ts +8 -0
- package/dist/core/view.js +168 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/prompts/searchable-multi-select.d.ts +27 -0
- package/dist/prompts/searchable-multi-select.js +149 -0
- package/dist/telemetry/config.d.ts +32 -0
- package/dist/telemetry/config.js +68 -0
- package/dist/telemetry/index.d.ts +31 -0
- package/dist/telemetry/index.js +145 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +10 -0
- package/dist/ui/welcome-screen.js +146 -0
- package/dist/utils/change-metadata.d.ts +51 -0
- package/dist/utils/change-metadata.js +147 -0
- package/dist/utils/change-utils.d.ts +62 -0
- package/dist/utils/change-utils.js +121 -0
- package/dist/utils/file-system.d.ts +36 -0
- package/dist/utils/file-system.js +281 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/interactive.d.ts +18 -0
- package/dist/utils/interactive.js +21 -0
- package/dist/utils/item-discovery.d.ts +4 -0
- package/dist/utils/item-discovery.js +72 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +41 -0
- package/dist/utils/task-progress.d.ts +8 -0
- package/dist/utils/task-progress.js +36 -0
- package/package.json +84 -0
- package/schemas/spec-driven/schema.yaml +148 -0
- package/schemas/spec-driven/templates/design.md +19 -0
- package/schemas/spec-driven/templates/proposal.md +23 -0
- package/schemas/spec-driven/templates/spec.md +8 -0
- package/schemas/spec-driven/templates/tasks.md +9 -0
- package/schemas/tdd/schema.yaml +213 -0
- package/schemas/tdd/templates/docs.md +15 -0
- package/schemas/tdd/templates/implementation.md +11 -0
- package/schemas/tdd/templates/spec.md +11 -0
- package/schemas/tdd/templates/test.md +11 -0
- package/scripts/postinstall.js +147 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy cleanup module for detecting and removing OpenSpec artifacts
|
|
3
|
+
* from previous init versions during the migration to the skill-based workflow.
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { FileSystemUtils, removeMarkerBlock as removeMarkerBlockUtil } from '../utils/file-system.js';
|
|
8
|
+
import { OPENSPEC_MARKERS } from './config.js';
|
|
9
|
+
/**
|
|
10
|
+
* Legacy config file names from the old ToolRegistry.
|
|
11
|
+
* These were config files created at project root with OpenSpec markers.
|
|
12
|
+
*/
|
|
13
|
+
export const LEGACY_CONFIG_FILES = [
|
|
14
|
+
'CLAUDE.md',
|
|
15
|
+
'CLINE.md',
|
|
16
|
+
'CODEBUDDY.md',
|
|
17
|
+
'COSTRICT.md',
|
|
18
|
+
'QODER.md',
|
|
19
|
+
'IFLOW.md',
|
|
20
|
+
'AGENTS.md', // root AGENTS.md (not openspec/AGENTS.md)
|
|
21
|
+
'QWEN.md',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Legacy slash command patterns from the old SlashCommandRegistry.
|
|
25
|
+
* These map toolId to the path pattern where legacy commands were created.
|
|
26
|
+
* Some tools used a directory structure, others used individual files.
|
|
27
|
+
*/
|
|
28
|
+
export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
29
|
+
// Directory-based: .tooldir/commands/openspec/ or .tooldir/commands/openspec/*.md
|
|
30
|
+
'claude': { type: 'directory', path: '.claude/commands/openspec' },
|
|
31
|
+
'codebuddy': { type: 'directory', path: '.codebuddy/commands/openspec' },
|
|
32
|
+
'qoder': { type: 'directory', path: '.qoder/commands/openspec' },
|
|
33
|
+
'crush': { type: 'directory', path: '.crush/commands/openspec' },
|
|
34
|
+
'gemini': { type: 'directory', path: '.gemini/commands/openspec' },
|
|
35
|
+
'costrict': { type: 'directory', path: '.cospec/openspec/commands' },
|
|
36
|
+
// File-based: individual openspec-*.md files in a commands/workflows/prompts folder
|
|
37
|
+
'cursor': { type: 'files', pattern: '.cursor/commands/openspec-*.md' },
|
|
38
|
+
'windsurf': { type: 'files', pattern: '.windsurf/workflows/openspec-*.md' },
|
|
39
|
+
'kilocode': { type: 'files', pattern: '.kilocode/workflows/openspec-*.md' },
|
|
40
|
+
'github-copilot': { type: 'files', pattern: '.github/prompts/openspec-*.prompt.md' },
|
|
41
|
+
'amazon-q': { type: 'files', pattern: '.amazonq/prompts/openspec-*.md' },
|
|
42
|
+
'cline': { type: 'files', pattern: '.clinerules/workflows/openspec-*.md' },
|
|
43
|
+
'roocode': { type: 'files', pattern: '.roo/commands/openspec-*.md' },
|
|
44
|
+
'auggie': { type: 'files', pattern: '.augment/commands/openspec-*.md' },
|
|
45
|
+
'factory': { type: 'files', pattern: '.factory/commands/openspec-*.md' },
|
|
46
|
+
'opencode': { type: 'files', pattern: '.opencode/command/openspec-*.md' },
|
|
47
|
+
'continue': { type: 'files', pattern: '.continue/prompts/openspec-*.prompt' },
|
|
48
|
+
'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
|
|
49
|
+
'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },
|
|
50
|
+
'qwen': { type: 'files', pattern: '.qwen/commands/openspec-*.toml' },
|
|
51
|
+
'codex': { type: 'files', pattern: '.codex/prompts/openspec-*.md' },
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Detects all legacy OpenSpec artifacts in a project.
|
|
55
|
+
*
|
|
56
|
+
* @param projectPath - The root path of the project
|
|
57
|
+
* @returns Detection result with all found legacy artifacts
|
|
58
|
+
*/
|
|
59
|
+
export async function detectLegacyArtifacts(projectPath) {
|
|
60
|
+
const result = {
|
|
61
|
+
configFiles: [],
|
|
62
|
+
configFilesToUpdate: [],
|
|
63
|
+
slashCommandDirs: [],
|
|
64
|
+
slashCommandFiles: [],
|
|
65
|
+
hasOpenspecAgents: false,
|
|
66
|
+
hasProjectMd: false,
|
|
67
|
+
hasRootAgentsWithMarkers: false,
|
|
68
|
+
hasLegacyArtifacts: false,
|
|
69
|
+
};
|
|
70
|
+
// Detect legacy config files
|
|
71
|
+
const configResult = await detectLegacyConfigFiles(projectPath);
|
|
72
|
+
result.configFiles = configResult.allFiles;
|
|
73
|
+
result.configFilesToUpdate = configResult.filesToUpdate;
|
|
74
|
+
// Detect legacy slash commands
|
|
75
|
+
const slashResult = await detectLegacySlashCommands(projectPath);
|
|
76
|
+
result.slashCommandDirs = slashResult.directories;
|
|
77
|
+
result.slashCommandFiles = slashResult.files;
|
|
78
|
+
// Detect legacy structure files
|
|
79
|
+
const structureResult = await detectLegacyStructureFiles(projectPath);
|
|
80
|
+
result.hasOpenspecAgents = structureResult.hasOpenspecAgents;
|
|
81
|
+
result.hasProjectMd = structureResult.hasProjectMd;
|
|
82
|
+
result.hasRootAgentsWithMarkers = structureResult.hasRootAgentsWithMarkers;
|
|
83
|
+
// Determine if any legacy artifacts exist
|
|
84
|
+
result.hasLegacyArtifacts =
|
|
85
|
+
result.configFiles.length > 0 ||
|
|
86
|
+
result.slashCommandDirs.length > 0 ||
|
|
87
|
+
result.slashCommandFiles.length > 0 ||
|
|
88
|
+
result.hasOpenspecAgents ||
|
|
89
|
+
result.hasRootAgentsWithMarkers ||
|
|
90
|
+
result.hasProjectMd;
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Detects legacy config files with OpenSpec markers.
|
|
95
|
+
* All config files with markers are candidates for update (marker removal only).
|
|
96
|
+
* Config files are NEVER deleted - they belong to the user's project root.
|
|
97
|
+
*
|
|
98
|
+
* @param projectPath - The root path of the project
|
|
99
|
+
* @returns Object with all files found and files to update
|
|
100
|
+
*/
|
|
101
|
+
export async function detectLegacyConfigFiles(projectPath) {
|
|
102
|
+
const allFiles = [];
|
|
103
|
+
const filesToUpdate = [];
|
|
104
|
+
for (const fileName of LEGACY_CONFIG_FILES) {
|
|
105
|
+
const filePath = FileSystemUtils.joinPath(projectPath, fileName);
|
|
106
|
+
if (await FileSystemUtils.fileExists(filePath)) {
|
|
107
|
+
const content = await FileSystemUtils.readFile(filePath);
|
|
108
|
+
if (hasOpenSpecMarkers(content)) {
|
|
109
|
+
allFiles.push(fileName);
|
|
110
|
+
filesToUpdate.push(fileName); // Always update, never delete config files
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { allFiles, filesToUpdate };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Detects legacy slash command directories and files.
|
|
118
|
+
*
|
|
119
|
+
* @param projectPath - The root path of the project
|
|
120
|
+
* @returns Object with directories and individual files found
|
|
121
|
+
*/
|
|
122
|
+
export async function detectLegacySlashCommands(projectPath) {
|
|
123
|
+
const directories = [];
|
|
124
|
+
const files = [];
|
|
125
|
+
for (const [toolId, pattern] of Object.entries(LEGACY_SLASH_COMMAND_PATHS)) {
|
|
126
|
+
if (pattern.type === 'directory' && pattern.path) {
|
|
127
|
+
const dirPath = FileSystemUtils.joinPath(projectPath, pattern.path);
|
|
128
|
+
if (await FileSystemUtils.directoryExists(dirPath)) {
|
|
129
|
+
directories.push(pattern.path);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (pattern.type === 'files' && pattern.pattern) {
|
|
133
|
+
// For file-based patterns, check for individual files
|
|
134
|
+
const foundFiles = await findLegacySlashCommandFiles(projectPath, pattern.pattern);
|
|
135
|
+
files.push(...foundFiles);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { directories, files };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Finds legacy slash command files matching a glob pattern.
|
|
142
|
+
*
|
|
143
|
+
* @param projectPath - The root path of the project
|
|
144
|
+
* @param pattern - Glob pattern like '.cursor/commands/openspec-*.md'
|
|
145
|
+
* @returns Array of matching file paths relative to projectPath
|
|
146
|
+
*/
|
|
147
|
+
async function findLegacySlashCommandFiles(projectPath, pattern) {
|
|
148
|
+
const foundFiles = [];
|
|
149
|
+
// Extract directory and file pattern from glob
|
|
150
|
+
// Handle both forward and backward slashes for Windows compatibility
|
|
151
|
+
const lastForwardSlash = pattern.lastIndexOf('/');
|
|
152
|
+
const lastBackSlash = pattern.lastIndexOf('\\');
|
|
153
|
+
const lastSeparator = Math.max(lastForwardSlash, lastBackSlash);
|
|
154
|
+
const dirPart = pattern.substring(0, lastSeparator);
|
|
155
|
+
const filePart = pattern.substring(lastSeparator + 1);
|
|
156
|
+
const dirPath = FileSystemUtils.joinPath(projectPath, dirPart);
|
|
157
|
+
if (!(await FileSystemUtils.directoryExists(dirPath))) {
|
|
158
|
+
return foundFiles;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const entries = await fs.readdir(dirPath);
|
|
162
|
+
// Convert glob pattern to regex
|
|
163
|
+
// openspec-*.md -> /^openspec-.*\.md$/
|
|
164
|
+
// openspec-*.prompt.md -> /^openspec-.*\.prompt\.md$/
|
|
165
|
+
// openspec-*.toml -> /^openspec-.*\.toml$/
|
|
166
|
+
const regexPattern = filePart
|
|
167
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
|
|
168
|
+
.replace(/\*/g, '.*'); // Replace * with .*
|
|
169
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
if (regex.test(entry)) {
|
|
172
|
+
// Use forward slashes for consistency in relative paths (cross-platform)
|
|
173
|
+
const normalizedDir = dirPart.replace(/\\/g, '/');
|
|
174
|
+
foundFiles.push(`${normalizedDir}/${entry}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Directory doesn't exist or can't be read
|
|
180
|
+
}
|
|
181
|
+
return foundFiles;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Detects legacy OpenSpec structure files (AGENTS.md and project.md).
|
|
185
|
+
*
|
|
186
|
+
* @param projectPath - The root path of the project
|
|
187
|
+
* @returns Object with detection results for structure files
|
|
188
|
+
*/
|
|
189
|
+
export async function detectLegacyStructureFiles(projectPath) {
|
|
190
|
+
let hasOpenspecAgents = false;
|
|
191
|
+
let hasProjectMd = false;
|
|
192
|
+
let hasRootAgentsWithMarkers = false;
|
|
193
|
+
// Check for openspec/AGENTS.md
|
|
194
|
+
const openspecAgentsPath = FileSystemUtils.joinPath(projectPath, 'openspec', 'AGENTS.md');
|
|
195
|
+
hasOpenspecAgents = await FileSystemUtils.fileExists(openspecAgentsPath);
|
|
196
|
+
// Check for openspec/project.md (for migration messaging, not deleted)
|
|
197
|
+
const projectMdPath = FileSystemUtils.joinPath(projectPath, 'openspec', 'project.md');
|
|
198
|
+
hasProjectMd = await FileSystemUtils.fileExists(projectMdPath);
|
|
199
|
+
// Check for root AGENTS.md with OpenSpec markers
|
|
200
|
+
const rootAgentsPath = FileSystemUtils.joinPath(projectPath, 'AGENTS.md');
|
|
201
|
+
if (await FileSystemUtils.fileExists(rootAgentsPath)) {
|
|
202
|
+
const content = await FileSystemUtils.readFile(rootAgentsPath);
|
|
203
|
+
hasRootAgentsWithMarkers = hasOpenSpecMarkers(content);
|
|
204
|
+
}
|
|
205
|
+
return { hasOpenspecAgents, hasProjectMd, hasRootAgentsWithMarkers };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Checks if content contains OpenSpec markers.
|
|
209
|
+
*
|
|
210
|
+
* @param content - File content to check
|
|
211
|
+
* @returns True if both start and end markers are present
|
|
212
|
+
*/
|
|
213
|
+
export function hasOpenSpecMarkers(content) {
|
|
214
|
+
return (content.includes(OPENSPEC_MARKERS.start) && content.includes(OPENSPEC_MARKERS.end));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Checks if file content is 100% OpenSpec content (only markers and whitespace outside).
|
|
218
|
+
*
|
|
219
|
+
* @param content - File content to check
|
|
220
|
+
* @returns True if content outside markers is only whitespace
|
|
221
|
+
*/
|
|
222
|
+
export function isOnlyOpenSpecContent(content) {
|
|
223
|
+
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
|
|
224
|
+
const endIndex = content.indexOf(OPENSPEC_MARKERS.end);
|
|
225
|
+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const before = content.substring(0, startIndex);
|
|
229
|
+
const after = content.substring(endIndex + OPENSPEC_MARKERS.end.length);
|
|
230
|
+
return before.trim() === '' && after.trim() === '';
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Removes the OpenSpec marker block from file content.
|
|
234
|
+
* Only removes markers that are on their own lines (ignores inline mentions).
|
|
235
|
+
* Cleans up double blank lines that may result from removal.
|
|
236
|
+
*
|
|
237
|
+
* @param content - File content with OpenSpec markers
|
|
238
|
+
* @returns Content with marker block removed
|
|
239
|
+
*/
|
|
240
|
+
export function removeMarkerBlock(content) {
|
|
241
|
+
return removeMarkerBlockUtil(content, OPENSPEC_MARKERS.start, OPENSPEC_MARKERS.end);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Cleans up legacy OpenSpec artifacts from a project.
|
|
245
|
+
* Preserves openspec/project.md (shows migration hint instead of deleting).
|
|
246
|
+
*
|
|
247
|
+
* @param projectPath - The root path of the project
|
|
248
|
+
* @param detection - Detection result from detectLegacyArtifacts
|
|
249
|
+
* @returns Cleanup result with summary of actions taken
|
|
250
|
+
*/
|
|
251
|
+
export async function cleanupLegacyArtifacts(projectPath, detection) {
|
|
252
|
+
const result = {
|
|
253
|
+
deletedFiles: [],
|
|
254
|
+
modifiedFiles: [],
|
|
255
|
+
deletedDirs: [],
|
|
256
|
+
projectMdNeedsMigration: detection.hasProjectMd,
|
|
257
|
+
errors: [],
|
|
258
|
+
};
|
|
259
|
+
// Remove marker blocks from config files (NEVER delete config files)
|
|
260
|
+
// Config files like CLAUDE.md, AGENTS.md belong to the user's project root
|
|
261
|
+
for (const fileName of detection.configFilesToUpdate) {
|
|
262
|
+
const filePath = FileSystemUtils.joinPath(projectPath, fileName);
|
|
263
|
+
try {
|
|
264
|
+
const content = await FileSystemUtils.readFile(filePath);
|
|
265
|
+
const newContent = removeMarkerBlock(content);
|
|
266
|
+
// Always write the file, even if empty - never delete user config files
|
|
267
|
+
await FileSystemUtils.writeFile(filePath, newContent);
|
|
268
|
+
result.modifiedFiles.push(fileName);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
result.errors.push(`Failed to modify ${fileName}: ${error.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Delete legacy slash command directories (these are 100% OpenSpec-managed)
|
|
275
|
+
for (const dirPath of detection.slashCommandDirs) {
|
|
276
|
+
const fullPath = FileSystemUtils.joinPath(projectPath, dirPath);
|
|
277
|
+
try {
|
|
278
|
+
await fs.rm(fullPath, { recursive: true, force: true });
|
|
279
|
+
result.deletedDirs.push(dirPath);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
result.errors.push(`Failed to delete directory ${dirPath}: ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Delete legacy slash command files (these are 100% OpenSpec-managed)
|
|
286
|
+
for (const filePath of detection.slashCommandFiles) {
|
|
287
|
+
const fullPath = FileSystemUtils.joinPath(projectPath, filePath);
|
|
288
|
+
try {
|
|
289
|
+
await fs.unlink(fullPath);
|
|
290
|
+
result.deletedFiles.push(filePath);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
result.errors.push(`Failed to delete ${filePath}: ${error.message}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Delete openspec/AGENTS.md (this is inside openspec/, it's OpenSpec-managed)
|
|
297
|
+
if (detection.hasOpenspecAgents) {
|
|
298
|
+
const agentsPath = FileSystemUtils.joinPath(projectPath, 'openspec', 'AGENTS.md');
|
|
299
|
+
if (await FileSystemUtils.fileExists(agentsPath)) {
|
|
300
|
+
try {
|
|
301
|
+
await fs.unlink(agentsPath);
|
|
302
|
+
result.deletedFiles.push('openspec/AGENTS.md');
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
result.errors.push(`Failed to delete openspec/AGENTS.md: ${error.message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Handle root AGENTS.md with OpenSpec markers - remove markers only, NEVER delete
|
|
310
|
+
// Note: Root AGENTS.md is handled via configFilesToUpdate above (it's in LEGACY_CONFIG_FILES)
|
|
311
|
+
// This hasRootAgentsWithMarkers flag is just for detection, cleanup happens via configFilesToUpdate
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Generates a cleanup summary message for display.
|
|
316
|
+
*
|
|
317
|
+
* @param result - Cleanup result from cleanupLegacyArtifacts
|
|
318
|
+
* @returns Formatted summary string for console output
|
|
319
|
+
*/
|
|
320
|
+
export function formatCleanupSummary(result) {
|
|
321
|
+
const lines = [];
|
|
322
|
+
if (result.deletedFiles.length > 0 || result.deletedDirs.length > 0 || result.modifiedFiles.length > 0) {
|
|
323
|
+
lines.push('Cleaned up legacy files:');
|
|
324
|
+
for (const file of result.deletedFiles) {
|
|
325
|
+
lines.push(` ✓ Removed ${file}`);
|
|
326
|
+
}
|
|
327
|
+
for (const dir of result.deletedDirs) {
|
|
328
|
+
lines.push(` ✓ Removed ${dir}/ (replaced by /opsx:*)`);
|
|
329
|
+
}
|
|
330
|
+
for (const file of result.modifiedFiles) {
|
|
331
|
+
lines.push(` ✓ Removed OpenSpec markers from ${file}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (result.projectMdNeedsMigration) {
|
|
335
|
+
if (lines.length > 0) {
|
|
336
|
+
lines.push('');
|
|
337
|
+
}
|
|
338
|
+
lines.push(formatProjectMdMigrationHint());
|
|
339
|
+
}
|
|
340
|
+
if (result.errors.length > 0) {
|
|
341
|
+
if (lines.length > 0) {
|
|
342
|
+
lines.push('');
|
|
343
|
+
}
|
|
344
|
+
lines.push('Errors during cleanup:');
|
|
345
|
+
for (const error of result.errors) {
|
|
346
|
+
lines.push(` ⚠ ${error}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return lines.join('\n');
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Build list of files to be removed with explanations.
|
|
353
|
+
* Only includes OpenSpec-managed files (slash commands, openspec/AGENTS.md).
|
|
354
|
+
* Config files like CLAUDE.md, AGENTS.md are NEVER deleted.
|
|
355
|
+
*
|
|
356
|
+
* @param detection - Detection result from detectLegacyArtifacts
|
|
357
|
+
* @returns Array of objects with path and explanation
|
|
358
|
+
*/
|
|
359
|
+
function buildRemovalsList(detection) {
|
|
360
|
+
const removals = [];
|
|
361
|
+
// Slash command directories (these are 100% OpenSpec-managed)
|
|
362
|
+
for (const dir of detection.slashCommandDirs) {
|
|
363
|
+
// Split on both forward and backward slashes for Windows compatibility
|
|
364
|
+
const toolDir = dir.split(/[\/\\]/)[0];
|
|
365
|
+
removals.push({ path: dir + '/', explanation: `replaced by ${toolDir}/skills/` });
|
|
366
|
+
}
|
|
367
|
+
// Slash command files (these are 100% OpenSpec-managed)
|
|
368
|
+
for (const file of detection.slashCommandFiles) {
|
|
369
|
+
removals.push({ path: file, explanation: 'replaced by skills/' });
|
|
370
|
+
}
|
|
371
|
+
// openspec/AGENTS.md (inside openspec/, it's OpenSpec-managed)
|
|
372
|
+
if (detection.hasOpenspecAgents) {
|
|
373
|
+
removals.push({ path: 'openspec/AGENTS.md', explanation: 'obsolete workflow file' });
|
|
374
|
+
}
|
|
375
|
+
// Note: Config files (CLAUDE.md, AGENTS.md, etc.) are NEVER in the removals list
|
|
376
|
+
// They always go to the updates list where only markers are removed
|
|
377
|
+
return removals;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Build list of files to be updated with explanations.
|
|
381
|
+
* Includes ALL config files with markers - markers are removed, file is never deleted.
|
|
382
|
+
*
|
|
383
|
+
* @param detection - Detection result from detectLegacyArtifacts
|
|
384
|
+
* @returns Array of objects with path and explanation
|
|
385
|
+
*/
|
|
386
|
+
function buildUpdatesList(detection) {
|
|
387
|
+
const updates = [];
|
|
388
|
+
// All config files with markers get updated (markers removed, file preserved)
|
|
389
|
+
for (const file of detection.configFilesToUpdate) {
|
|
390
|
+
updates.push({ path: file, explanation: 'removing OpenSpec markers' });
|
|
391
|
+
}
|
|
392
|
+
return updates;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Generates a detection summary message for display before cleanup.
|
|
396
|
+
* Groups files by action type: removals, updates, and manual migration.
|
|
397
|
+
*
|
|
398
|
+
* @param detection - Detection result from detectLegacyArtifacts
|
|
399
|
+
* @returns Formatted summary string showing what was found
|
|
400
|
+
*/
|
|
401
|
+
export function formatDetectionSummary(detection) {
|
|
402
|
+
const lines = [];
|
|
403
|
+
const removals = buildRemovalsList(detection);
|
|
404
|
+
const updates = buildUpdatesList(detection);
|
|
405
|
+
// If nothing to show, return empty
|
|
406
|
+
if (removals.length === 0 && updates.length === 0 && !detection.hasProjectMd) {
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
// Header - welcoming upgrade message
|
|
410
|
+
lines.push(chalk.bold('Upgrading to the new OpenSpec'));
|
|
411
|
+
lines.push('');
|
|
412
|
+
lines.push('OpenSpec now uses agent skills, the emerging standard across coding');
|
|
413
|
+
lines.push('agents. This simplifies your setup while keeping everything working');
|
|
414
|
+
lines.push('as before.');
|
|
415
|
+
lines.push('');
|
|
416
|
+
// Section 1: Files to remove (no user content to preserve)
|
|
417
|
+
if (removals.length > 0) {
|
|
418
|
+
lines.push(chalk.bold('Files to remove'));
|
|
419
|
+
lines.push(chalk.dim('No user content to preserve:'));
|
|
420
|
+
for (const { path } of removals) {
|
|
421
|
+
lines.push(` • ${path}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Section 2: Files to update (markers removed, content preserved)
|
|
425
|
+
if (updates.length > 0) {
|
|
426
|
+
if (removals.length > 0)
|
|
427
|
+
lines.push('');
|
|
428
|
+
lines.push(chalk.bold('Files to update'));
|
|
429
|
+
lines.push(chalk.dim('OpenSpec markers will be removed, your content preserved:'));
|
|
430
|
+
for (const { path } of updates) {
|
|
431
|
+
lines.push(` • ${path}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Section 3: Manual migration (project.md)
|
|
435
|
+
if (detection.hasProjectMd) {
|
|
436
|
+
if (removals.length > 0 || updates.length > 0)
|
|
437
|
+
lines.push('');
|
|
438
|
+
lines.push(formatProjectMdMigrationHint());
|
|
439
|
+
}
|
|
440
|
+
return lines.join('\n');
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Extract tool IDs from detected legacy artifacts.
|
|
444
|
+
* Uses LEGACY_SLASH_COMMAND_PATHS to map paths back to tool IDs.
|
|
445
|
+
*
|
|
446
|
+
* @param detection - Detection result from detectLegacyArtifacts
|
|
447
|
+
* @returns Array of tool IDs that had legacy artifacts
|
|
448
|
+
*/
|
|
449
|
+
export function getToolsFromLegacyArtifacts(detection) {
|
|
450
|
+
const tools = new Set();
|
|
451
|
+
// Match directories to tool IDs
|
|
452
|
+
for (const dir of detection.slashCommandDirs) {
|
|
453
|
+
for (const [toolId, pattern] of Object.entries(LEGACY_SLASH_COMMAND_PATHS)) {
|
|
454
|
+
if (pattern.type === 'directory' && pattern.path === dir) {
|
|
455
|
+
tools.add(toolId);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Match files to tool IDs using glob patterns
|
|
461
|
+
for (const file of detection.slashCommandFiles) {
|
|
462
|
+
// Normalize file path to use forward slashes for consistent matching (Windows compatibility)
|
|
463
|
+
const normalizedFile = file.replace(/\\/g, '/');
|
|
464
|
+
for (const [toolId, pattern] of Object.entries(LEGACY_SLASH_COMMAND_PATHS)) {
|
|
465
|
+
if (pattern.type === 'files' && pattern.pattern) {
|
|
466
|
+
// Convert glob pattern to regex for matching
|
|
467
|
+
// e.g., '.cursor/commands/openspec-*.md' -> /^\.cursor\/commands\/openspec-.*\.md$/
|
|
468
|
+
const regexPattern = pattern.pattern
|
|
469
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
|
|
470
|
+
.replace(/\*/g, '.*'); // Replace * with .*
|
|
471
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
472
|
+
if (regex.test(normalizedFile)) {
|
|
473
|
+
tools.add(toolId);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return Array.from(tools);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Generates a migration hint message for project.md.
|
|
483
|
+
* This is shown when project.md exists and needs manual migration to config.yaml.
|
|
484
|
+
*
|
|
485
|
+
* @returns Formatted migration hint string for console output
|
|
486
|
+
*/
|
|
487
|
+
export function formatProjectMdMigrationHint() {
|
|
488
|
+
const lines = [];
|
|
489
|
+
lines.push(chalk.yellow.bold('Needs your attention'));
|
|
490
|
+
lines.push(' • openspec/project.md');
|
|
491
|
+
lines.push(chalk.dim(' We won\'t delete this file. It may contain useful project context.'));
|
|
492
|
+
lines.push('');
|
|
493
|
+
lines.push(chalk.dim(' The new openspec/config.yaml has a "context:" section for planning'));
|
|
494
|
+
lines.push(chalk.dim(' context. This is included in every OpenSpec request and works more'));
|
|
495
|
+
lines.push(chalk.dim(' reliably than the old project.md approach.'));
|
|
496
|
+
lines.push('');
|
|
497
|
+
lines.push(chalk.dim(' Review project.md, move any useful content to config.yaml\'s context'));
|
|
498
|
+
lines.push(chalk.dim(' section, then delete the file when ready.'));
|
|
499
|
+
return lines.join('\n');
|
|
500
|
+
}
|
|
501
|
+
//# sourceMappingURL=legacy-cleanup.js.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { MarkdownParser } from './parsers/markdown-parser.js';
|
|
7
|
+
/**
|
|
8
|
+
* Get the most recent modification time of any file in a directory (recursive).
|
|
9
|
+
* Falls back to the directory's own mtime if no files are found.
|
|
10
|
+
*/
|
|
11
|
+
async function getLastModified(dirPath) {
|
|
12
|
+
let latest = null;
|
|
13
|
+
async function walk(dir) {
|
|
14
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const fullPath = path.join(dir, entry.name);
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
await walk(fullPath);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const stat = await fs.stat(fullPath);
|
|
22
|
+
if (latest === null || stat.mtime > latest) {
|
|
23
|
+
latest = stat.mtime;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
await walk(dirPath);
|
|
29
|
+
// If no files found, use the directory's own modification time
|
|
30
|
+
if (latest === null) {
|
|
31
|
+
const dirStat = await fs.stat(dirPath);
|
|
32
|
+
return dirStat.mtime;
|
|
33
|
+
}
|
|
34
|
+
return latest;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Format a date as relative time (e.g., "2 hours ago", "3 days ago")
|
|
38
|
+
*/
|
|
39
|
+
function formatRelativeTime(date) {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const diffMs = now.getTime() - date.getTime();
|
|
42
|
+
const diffSecs = Math.floor(diffMs / 1000);
|
|
43
|
+
const diffMins = Math.floor(diffSecs / 60);
|
|
44
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
45
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
46
|
+
if (diffDays > 30) {
|
|
47
|
+
return date.toLocaleDateString();
|
|
48
|
+
}
|
|
49
|
+
else if (diffDays > 0) {
|
|
50
|
+
return `${diffDays}d ago`;
|
|
51
|
+
}
|
|
52
|
+
else if (diffHours > 0) {
|
|
53
|
+
return `${diffHours}h ago`;
|
|
54
|
+
}
|
|
55
|
+
else if (diffMins > 0) {
|
|
56
|
+
return `${diffMins}m ago`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
return 'just now';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class ListCommand {
|
|
63
|
+
async execute(targetPath = '.', mode = 'changes', options = {}) {
|
|
64
|
+
const { sort = 'recent', json = false } = options;
|
|
65
|
+
if (mode === 'changes') {
|
|
66
|
+
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
|
67
|
+
// Check if changes directory exists
|
|
68
|
+
try {
|
|
69
|
+
await fs.access(changesDir);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
|
|
73
|
+
}
|
|
74
|
+
// Get all directories in changes (excluding archive)
|
|
75
|
+
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
76
|
+
const changeDirs = entries
|
|
77
|
+
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
|
78
|
+
.map(entry => entry.name);
|
|
79
|
+
if (changeDirs.length === 0) {
|
|
80
|
+
if (json) {
|
|
81
|
+
console.log(JSON.stringify({ changes: [] }));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log('No active changes found.');
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Collect information about each change
|
|
89
|
+
const changes = [];
|
|
90
|
+
for (const changeDir of changeDirs) {
|
|
91
|
+
const progress = await getTaskProgressForChange(changesDir, changeDir);
|
|
92
|
+
const changePath = path.join(changesDir, changeDir);
|
|
93
|
+
const lastModified = await getLastModified(changePath);
|
|
94
|
+
changes.push({
|
|
95
|
+
name: changeDir,
|
|
96
|
+
completedTasks: progress.completed,
|
|
97
|
+
totalTasks: progress.total,
|
|
98
|
+
lastModified
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Sort by preference (default: recent first)
|
|
102
|
+
if (sort === 'recent') {
|
|
103
|
+
changes.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
changes.sort((a, b) => a.name.localeCompare(b.name));
|
|
107
|
+
}
|
|
108
|
+
// JSON output for programmatic use
|
|
109
|
+
if (json) {
|
|
110
|
+
const jsonOutput = changes.map(c => ({
|
|
111
|
+
name: c.name,
|
|
112
|
+
completedTasks: c.completedTasks,
|
|
113
|
+
totalTasks: c.totalTasks,
|
|
114
|
+
lastModified: c.lastModified.toISOString(),
|
|
115
|
+
status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress'
|
|
116
|
+
}));
|
|
117
|
+
console.log(JSON.stringify({ changes: jsonOutput }, null, 2));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Display results
|
|
121
|
+
console.log('Changes:');
|
|
122
|
+
const padding = ' ';
|
|
123
|
+
const nameWidth = Math.max(...changes.map(c => c.name.length));
|
|
124
|
+
for (const change of changes) {
|
|
125
|
+
const paddedName = change.name.padEnd(nameWidth);
|
|
126
|
+
const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
|
|
127
|
+
const timeAgo = formatRelativeTime(change.lastModified);
|
|
128
|
+
console.log(`${padding}${paddedName} ${status.padEnd(12)} ${timeAgo}`);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// specs mode
|
|
133
|
+
const specsDir = path.join(targetPath, 'openspec', 'specs');
|
|
134
|
+
try {
|
|
135
|
+
await fs.access(specsDir);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
console.log('No specs found.');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const entries = await fs.readdir(specsDir, { withFileTypes: true });
|
|
142
|
+
const specDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
143
|
+
if (specDirs.length === 0) {
|
|
144
|
+
console.log('No specs found.');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const specs = [];
|
|
148
|
+
for (const id of specDirs) {
|
|
149
|
+
const specPath = join(specsDir, id, 'spec.md');
|
|
150
|
+
try {
|
|
151
|
+
const content = readFileSync(specPath, 'utf-8');
|
|
152
|
+
const parser = new MarkdownParser(content);
|
|
153
|
+
const spec = parser.parseSpec(id);
|
|
154
|
+
specs.push({ id, requirementCount: spec.requirements.length });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// If spec cannot be read or parsed, include with 0 count
|
|
158
|
+
specs.push({ id, requirementCount: 0 });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
specs.sort((a, b) => a.id.localeCompare(b.id));
|
|
162
|
+
console.log('Specs:');
|
|
163
|
+
const padding = ' ';
|
|
164
|
+
const nameWidth = Math.max(...specs.map(s => s.id.length));
|
|
165
|
+
for (const spec of specs) {
|
|
166
|
+
const padded = spec.id.padEnd(nameWidth);
|
|
167
|
+
console.log(`${padding}${padded} requirements ${spec.requirementCount}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=list.js.map
|