@zhuan-ai/zhuanspec 1.3.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 +461 -0
- package/README.zh.md +434 -0
- package/bin/zhuanspec.js +3 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +356 -0
- package/dist/commands/artifact-workflow.d.ts +13 -0
- package/dist/commands/artifact-workflow.js +916 -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 +221 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +198 -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/core/archive.d.ts +30 -0
- package/dist/core/archive.js +438 -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 +134 -0
- package/dist/core/artifact-graph/instruction-loader.js +180 -0
- package/dist/core/artifact-graph/resolver.d.ts +61 -0
- package/dist/core/artifact-graph/resolver.js +187 -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/completions/command-registry.d.ts +7 -0
- package/dist/core/completions/command-registry.js +362 -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 +51 -0
- package/dist/core/completions/factory.js +57 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +58 -0
- package/dist/core/completions/generators/zsh-generator.js +319 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +136 -0
- package/dist/core/completions/installers/zsh-installer.js +449 -0
- package/dist/core/completions/types.d.ts +78 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-schema.d.ts +76 -0
- package/dist/core/config-schema.js +200 -0
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +29 -0
- package/dist/core/configurators/agents.d.ts +8 -0
- package/dist/core/configurators/agents.js +15 -0
- package/dist/core/configurators/base.d.ts +7 -0
- package/dist/core/configurators/base.js +2 -0
- package/dist/core/configurators/claude.d.ts +8 -0
- package/dist/core/configurators/claude.js +15 -0
- package/dist/core/configurators/cline.d.ts +8 -0
- package/dist/core/configurators/cline.js +15 -0
- package/dist/core/configurators/codebuddy.d.ts +8 -0
- package/dist/core/configurators/codebuddy.js +15 -0
- package/dist/core/configurators/costrict.d.ts +8 -0
- package/dist/core/configurators/costrict.js +15 -0
- package/dist/core/configurators/iflow.d.ts +8 -0
- package/dist/core/configurators/iflow.js +15 -0
- package/dist/core/configurators/qoder.d.ts +30 -0
- package/dist/core/configurators/qoder.js +42 -0
- package/dist/core/configurators/qwen.d.ts +24 -0
- package/dist/core/configurators/qwen.js +37 -0
- package/dist/core/configurators/registry.d.ts +9 -0
- package/dist/core/configurators/registry.js +43 -0
- package/dist/core/configurators/slash/amazon-q.d.ts +9 -0
- package/dist/core/configurators/slash/amazon-q.js +46 -0
- package/dist/core/configurators/slash/antigravity.d.ts +9 -0
- package/dist/core/configurators/slash/antigravity.js +23 -0
- package/dist/core/configurators/slash/auggie.d.ts +9 -0
- package/dist/core/configurators/slash/auggie.js +31 -0
- package/dist/core/configurators/slash/base.d.ts +19 -0
- package/dist/core/configurators/slash/base.js +69 -0
- package/dist/core/configurators/slash/claude.d.ts +9 -0
- package/dist/core/configurators/slash/claude.js +37 -0
- package/dist/core/configurators/slash/cline.d.ts +9 -0
- package/dist/core/configurators/slash/cline.js +23 -0
- package/dist/core/configurators/slash/codebuddy.d.ts +9 -0
- package/dist/core/configurators/slash/codebuddy.js +37 -0
- package/dist/core/configurators/slash/codex.d.ts +14 -0
- package/dist/core/configurators/slash/codex.js +109 -0
- package/dist/core/configurators/slash/costrict.d.ts +9 -0
- package/dist/core/configurators/slash/costrict.js +31 -0
- package/dist/core/configurators/slash/crush.d.ts +9 -0
- package/dist/core/configurators/slash/crush.js +37 -0
- package/dist/core/configurators/slash/cursor.d.ts +9 -0
- package/dist/core/configurators/slash/cursor.js +37 -0
- package/dist/core/configurators/slash/factory.d.ts +10 -0
- package/dist/core/configurators/slash/factory.js +35 -0
- package/dist/core/configurators/slash/gemini.d.ts +9 -0
- package/dist/core/configurators/slash/gemini.js +22 -0
- package/dist/core/configurators/slash/github-copilot.d.ts +9 -0
- package/dist/core/configurators/slash/github-copilot.js +34 -0
- package/dist/core/configurators/slash/iflow.d.ts +9 -0
- package/dist/core/configurators/slash/iflow.js +37 -0
- package/dist/core/configurators/slash/kilocode.d.ts +9 -0
- package/dist/core/configurators/slash/kilocode.js +17 -0
- package/dist/core/configurators/slash/opencode.d.ts +12 -0
- package/dist/core/configurators/slash/opencode.js +72 -0
- package/dist/core/configurators/slash/qoder.d.ts +35 -0
- package/dist/core/configurators/slash/qoder.js +76 -0
- package/dist/core/configurators/slash/qwen.d.ts +32 -0
- package/dist/core/configurators/slash/qwen.js +49 -0
- package/dist/core/configurators/slash/registry.d.ts +8 -0
- package/dist/core/configurators/slash/registry.js +75 -0
- package/dist/core/configurators/slash/roocode.d.ts +9 -0
- package/dist/core/configurators/slash/roocode.js +23 -0
- package/dist/core/configurators/slash/toml-base.d.ts +10 -0
- package/dist/core/configurators/slash/toml-base.js +53 -0
- package/dist/core/configurators/slash/windsurf.d.ts +9 -0
- package/dist/core/configurators/slash/windsurf.js +23 -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 +60 -0
- package/dist/core/init.js +861 -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 +34 -0
- package/dist/core/project-config.js +79 -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/skill-discovery.d.ts +24 -0
- package/dist/core/skill-discovery.js +153 -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/agents-root-stub.d.ts +2 -0
- package/dist/core/templates/agents-root-stub.js +17 -0
- package/dist/core/templates/agents-template.d.ts +2 -0
- package/dist/core/templates/agents-template.js +706 -0
- package/dist/core/templates/claude-template.d.ts +2 -0
- package/dist/core/templates/claude-template.js +2 -0
- package/dist/core/templates/cline-template.d.ts +2 -0
- package/dist/core/templates/cline-template.js +2 -0
- package/dist/core/templates/costrict-template.d.ts +2 -0
- package/dist/core/templates/costrict-template.js +2 -0
- package/dist/core/templates/index.d.ts +17 -0
- package/dist/core/templates/index.js +37 -0
- package/dist/core/templates/project-template.d.ts +8 -0
- package/dist/core/templates/project-template.js +32 -0
- package/dist/core/templates/skill-templates.d.ts +103 -0
- package/dist/core/templates/skill-templates.js +2131 -0
- package/dist/core/templates/slash-command-templates.d.ts +4 -0
- package/dist/core/templates/slash-command-templates.js +81 -0
- package/dist/core/update.d.ts +4 -0
- package/dist/core/update.js +88 -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/utils/change-metadata.d.ts +47 -0
- package/dist/utils/change-metadata.js +130 -0
- package/dist/utils/change-utils.d.ts +51 -0
- package/dist/utils/change-utils.js +100 -0
- package/dist/utils/file-system.d.ts +19 -0
- package/dist/utils/file-system.js +177 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +5 -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 +81 -0
- package/schemas/spec-driven/schema.yaml +205 -0
- package/schemas/spec-driven/templates/design.md +19 -0
- package/schemas/spec-driven/templates/proposal.md +43 -0
- package/schemas/spec-driven/templates/spec.md +8 -0
- package/schemas/spec-driven/templates/tasks.md +25 -0
- package/schemas/tdd/schema.yaml +213 -0
- package/schemas/tdd/templates/docs.md +0 -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,438 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
4
|
+
import { Validator } from './validation/validator.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
|
|
7
|
+
export class ArchiveCommand {
|
|
8
|
+
async execute(changeName, options = {}) {
|
|
9
|
+
const targetPath = '.';
|
|
10
|
+
const changesDir = path.join(targetPath, 'zhuanspec', 'changes');
|
|
11
|
+
const archiveDir = path.join(changesDir, 'archive');
|
|
12
|
+
const mainSpecsDir = path.join(targetPath, 'zhuanspec', 'specs');
|
|
13
|
+
// Check if changes directory exists
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(changesDir);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error("未找到 ZhuanSpec 变更目录。请先运行 'zhuanspec init'。");
|
|
19
|
+
}
|
|
20
|
+
// Get change name interactively if not provided
|
|
21
|
+
if (!changeName) {
|
|
22
|
+
const selectedChange = await this.selectChange(changesDir);
|
|
23
|
+
if (!selectedChange) {
|
|
24
|
+
console.log('未选择变更。已中止。');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
changeName = selectedChange;
|
|
28
|
+
}
|
|
29
|
+
const changeDir = path.join(changesDir, changeName);
|
|
30
|
+
// Verify change exists
|
|
31
|
+
try {
|
|
32
|
+
const stat = await fs.stat(changeDir);
|
|
33
|
+
if (!stat.isDirectory()) {
|
|
34
|
+
throw new Error(`Change '${changeName}' not found.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`Change '${changeName}' not found.`);
|
|
39
|
+
}
|
|
40
|
+
const skipValidation = options.validate === false || options.noValidate === true;
|
|
41
|
+
// Validate specs and change before archiving
|
|
42
|
+
if (!skipValidation) {
|
|
43
|
+
const validator = new Validator();
|
|
44
|
+
let hasValidationErrors = false;
|
|
45
|
+
// Validate proposal.md (non-blocking unless strict mode desired in future)
|
|
46
|
+
const changeFile = path.join(changeDir, 'proposal.md');
|
|
47
|
+
try {
|
|
48
|
+
await fs.access(changeFile);
|
|
49
|
+
const changeReport = await validator.validateChange(changeFile);
|
|
50
|
+
// Proposal validation is informative only (do not block archive)
|
|
51
|
+
if (!changeReport.valid) {
|
|
52
|
+
console.log(chalk.yellow(`\nproposal.md 中的提案警告(非阻塞):`));
|
|
53
|
+
for (const issue of changeReport.issues) {
|
|
54
|
+
const symbol = issue.level === 'ERROR' ? '⚠' : (issue.level === 'WARNING' ? '⚠' : 'ℹ');
|
|
55
|
+
console.log(chalk.yellow(` ${symbol} ${issue.message}`));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Change file doesn't exist, skip validation
|
|
61
|
+
}
|
|
62
|
+
// Validate delta-formatted spec files under the change directory if present
|
|
63
|
+
const changeSpecsDir = path.join(changeDir, 'specs');
|
|
64
|
+
let hasDeltaSpecs = false;
|
|
65
|
+
try {
|
|
66
|
+
const candidates = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
|
67
|
+
for (const c of candidates) {
|
|
68
|
+
if (c.isDirectory()) {
|
|
69
|
+
try {
|
|
70
|
+
const candidatePath = path.join(changeSpecsDir, c.name, 'spec.md');
|
|
71
|
+
await fs.access(candidatePath);
|
|
72
|
+
const content = await fs.readFile(candidatePath, 'utf-8');
|
|
73
|
+
if (/^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements/m.test(content)) {
|
|
74
|
+
hasDeltaSpecs = true;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
if (hasDeltaSpecs) {
|
|
84
|
+
const deltaReport = await validator.validateChangeDeltaSpecs(changeDir);
|
|
85
|
+
if (!deltaReport.valid) {
|
|
86
|
+
hasValidationErrors = true;
|
|
87
|
+
console.log(chalk.red(`\n变更增量规范中的验证错误:`));
|
|
88
|
+
for (const issue of deltaReport.issues) {
|
|
89
|
+
if (issue.level === 'ERROR') {
|
|
90
|
+
console.log(chalk.red(` ✗ ${issue.message}`));
|
|
91
|
+
}
|
|
92
|
+
else if (issue.level === 'WARNING') {
|
|
93
|
+
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (hasValidationErrors) {
|
|
99
|
+
console.log(chalk.red('\n验证失败。请在归档之前修复错误。'));
|
|
100
|
+
console.log(chalk.yellow('要跳过验证(不推荐),请使用 --no-validate 标志。'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Log warning when validation is skipped
|
|
106
|
+
const timestamp = new Date().toISOString();
|
|
107
|
+
if (!options.yes) {
|
|
108
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
109
|
+
const proceed = await confirm({
|
|
110
|
+
message: chalk.yellow('⚠️ 警告:跳过验证可能会归档无效的规范。继续?(y/N)'),
|
|
111
|
+
default: false
|
|
112
|
+
});
|
|
113
|
+
if (!proceed) {
|
|
114
|
+
console.log('归档已取消。');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(chalk.yellow(`\n⚠️ 警告:跳过验证可能会归档无效的规范。`));
|
|
120
|
+
}
|
|
121
|
+
console.log(chalk.yellow(`[${timestamp}] 已跳过变更的验证:${changeName}`));
|
|
122
|
+
console.log(chalk.yellow(`受影响的文件:${changeDir}`));
|
|
123
|
+
}
|
|
124
|
+
// Show progress and check for incomplete tasks
|
|
125
|
+
const progress = await getTaskProgressForChange(changesDir, changeName);
|
|
126
|
+
const status = formatTaskStatus(progress);
|
|
127
|
+
console.log(`任务状态:${status}`);
|
|
128
|
+
const incompleteTasks = Math.max(progress.total - progress.completed, 0);
|
|
129
|
+
if (incompleteTasks > 0) {
|
|
130
|
+
if (!options.yes) {
|
|
131
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
132
|
+
const proceed = await confirm({
|
|
133
|
+
message: `警告:发现 ${incompleteTasks} 个未完成的任务。继续?`,
|
|
134
|
+
default: false
|
|
135
|
+
});
|
|
136
|
+
if (!proceed) {
|
|
137
|
+
console.log('归档已取消。');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log(`警告:发现 ${incompleteTasks} 个未完成的任务。由于 --yes 标志,继续执行。`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Handle spec updates unless skipSpecs flag is set
|
|
146
|
+
if (options.skipSpecs) {
|
|
147
|
+
console.log('跳过规范更新(已提供 --skip-specs 标志)。');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Find specs to update
|
|
151
|
+
const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
|
|
152
|
+
if (specUpdates.length > 0) {
|
|
153
|
+
console.log('\n要更新的规范:');
|
|
154
|
+
for (const update of specUpdates) {
|
|
155
|
+
const status = update.exists ? 'update' : 'create';
|
|
156
|
+
const capability = path.basename(path.dirname(update.target));
|
|
157
|
+
console.log(` ${capability}: ${status}`);
|
|
158
|
+
}
|
|
159
|
+
let shouldUpdateSpecs = true;
|
|
160
|
+
if (!options.yes) {
|
|
161
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
162
|
+
shouldUpdateSpecs = await confirm({
|
|
163
|
+
message: '继续更新规范?',
|
|
164
|
+
default: true
|
|
165
|
+
});
|
|
166
|
+
if (!shouldUpdateSpecs) {
|
|
167
|
+
console.log('跳过规范更新。继续归档。');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (shouldUpdateSpecs) {
|
|
171
|
+
// Prepare all updates first (validation pass, no writes)
|
|
172
|
+
const prepared = [];
|
|
173
|
+
try {
|
|
174
|
+
for (const update of specUpdates) {
|
|
175
|
+
const built = await buildUpdatedSpec(update, changeName);
|
|
176
|
+
prepared.push({ update, rebuilt: built.rebuilt, counts: built.counts });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
console.log(String(err.message || err));
|
|
181
|
+
console.log('已中止。未更改任何文件。');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// All validations passed; pre-validate rebuilt full spec and then write files and display counts
|
|
185
|
+
let totals = { added: 0, modified: 0, removed: 0, renamed: 0 };
|
|
186
|
+
for (const p of prepared) {
|
|
187
|
+
const specName = path.basename(path.dirname(p.update.target));
|
|
188
|
+
if (!skipValidation) {
|
|
189
|
+
const report = await new Validator().validateSpecContent(specName, p.rebuilt);
|
|
190
|
+
if (!report.valid) {
|
|
191
|
+
console.log(chalk.red(`\n重建的规范 ${specName} 中的验证错误(不会写入更改):`));
|
|
192
|
+
for (const issue of report.issues) {
|
|
193
|
+
if (issue.level === 'ERROR')
|
|
194
|
+
console.log(chalk.red(` ✗ ${issue.message}`));
|
|
195
|
+
else if (issue.level === 'WARNING')
|
|
196
|
+
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
|
197
|
+
}
|
|
198
|
+
console.log('已中止。未更改任何文件。');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
await writeUpdatedSpec(p.update, p.rebuilt, p.counts);
|
|
203
|
+
totals.added += p.counts.added;
|
|
204
|
+
totals.modified += p.counts.modified;
|
|
205
|
+
totals.removed += p.counts.removed;
|
|
206
|
+
totals.renamed += p.counts.renamed;
|
|
207
|
+
}
|
|
208
|
+
console.log(`总计:+ ${totals.added},~ ${totals.modified},- ${totals.removed},→ ${totals.renamed}`);
|
|
209
|
+
console.log('规范更新成功。');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Create archive directory with date prefix
|
|
214
|
+
const archiveName = `${this.getArchiveDate()}-${changeName}`;
|
|
215
|
+
const archivePath = path.join(archiveDir, archiveName);
|
|
216
|
+
// Check if archive already exists
|
|
217
|
+
try {
|
|
218
|
+
await fs.access(archivePath);
|
|
219
|
+
throw new Error(`归档 '${archiveName}' 已存在。`);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error.code !== 'ENOENT') {
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Create archive directory if needed
|
|
227
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
228
|
+
// Move change to archive (with cross-device fallback)
|
|
229
|
+
await this.moveDirectory(changeDir, archivePath);
|
|
230
|
+
console.log(`变更 '${changeName}' 已归档为 '${archiveName}'。`);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Move a directory, falling back to copy+remove when rename fails
|
|
234
|
+
* (e.g., across devices, network drives, or permission issues).
|
|
235
|
+
*/
|
|
236
|
+
async moveDirectory(src, dest) {
|
|
237
|
+
try {
|
|
238
|
+
await fs.rename(src, dest);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
// EXDEV: cross-device link, EPERM: operation not permitted
|
|
242
|
+
if (error.code === 'EXDEV' || error.code === 'EPERM') {
|
|
243
|
+
console.log(chalk.yellow('rename 失败,回退到 copy+remove...'));
|
|
244
|
+
await this.copyDirectoryRecursive(src, dest);
|
|
245
|
+
await fs.rm(src, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Recursively copy a directory.
|
|
254
|
+
*/
|
|
255
|
+
async copyDirectoryRecursive(src, dest) {
|
|
256
|
+
await fs.mkdir(dest, { recursive: true });
|
|
257
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
const srcPath = path.join(src, entry.name);
|
|
260
|
+
const destPath = path.join(dest, entry.name);
|
|
261
|
+
if (entry.isDirectory()) {
|
|
262
|
+
await this.copyDirectoryRecursive(srcPath, destPath);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
await fs.copyFile(srcPath, destPath);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async selectChange(changesDir) {
|
|
270
|
+
const { select } = await import('@inquirer/prompts');
|
|
271
|
+
// Get all directories in changes (excluding archive)
|
|
272
|
+
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
273
|
+
const changeDirs = entries
|
|
274
|
+
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
|
275
|
+
.map(entry => entry.name)
|
|
276
|
+
.sort();
|
|
277
|
+
if (changeDirs.length === 0) {
|
|
278
|
+
console.log('未找到活动变更。');
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
// Build choices with progress inline to avoid duplicate lists
|
|
282
|
+
let choices = changeDirs.map(name => ({ name, value: name }));
|
|
283
|
+
try {
|
|
284
|
+
const progressList = [];
|
|
285
|
+
for (const id of changeDirs) {
|
|
286
|
+
const progress = await getTaskProgressForChange(changesDir, id);
|
|
287
|
+
const status = formatTaskStatus(progress);
|
|
288
|
+
progressList.push({ id, status });
|
|
289
|
+
}
|
|
290
|
+
const nameWidth = Math.max(...progressList.map(p => p.id.length));
|
|
291
|
+
choices = progressList.map(p => ({
|
|
292
|
+
name: `${p.id.padEnd(nameWidth)} ${p.status}`,
|
|
293
|
+
value: p.id
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// If anything fails, fall back to simple names
|
|
298
|
+
choices = changeDirs.map(name => ({ name, value: name }));
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const answer = await select({
|
|
302
|
+
message: '选择要归档的变更',
|
|
303
|
+
choices
|
|
304
|
+
});
|
|
305
|
+
return answer;
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
// User cancelled (Ctrl+C)
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
getArchiveDate() {
|
|
313
|
+
// Returns date in YYYY-MM-DD format
|
|
314
|
+
return new Date().toISOString().split('T')[0];
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Bulk archive multiple completed changes at once.
|
|
318
|
+
* Lists all active changes, lets user select multiple, then archives them sequentially.
|
|
319
|
+
*/
|
|
320
|
+
async bulkArchive(options = {}) {
|
|
321
|
+
const targetPath = '.';
|
|
322
|
+
const changesDir = path.join(targetPath, 'zhuanspec', 'changes');
|
|
323
|
+
// Check if changes directory exists
|
|
324
|
+
try {
|
|
325
|
+
await fs.access(changesDir);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
throw new Error("未找到 ZhuanSpec 变更目录。请先运行 'zhuanspec init'。");
|
|
329
|
+
}
|
|
330
|
+
// Get all active changes
|
|
331
|
+
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
332
|
+
const changeDirs = entries
|
|
333
|
+
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
|
334
|
+
.map(entry => entry.name)
|
|
335
|
+
.sort();
|
|
336
|
+
if (changeDirs.length === 0) {
|
|
337
|
+
console.log('未找到活动变更。');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// Build choices with progress
|
|
341
|
+
let choices = changeDirs.map(name => ({ name, value: name }));
|
|
342
|
+
try {
|
|
343
|
+
const progressList = [];
|
|
344
|
+
for (const id of changeDirs) {
|
|
345
|
+
const progress = await getTaskProgressForChange(changesDir, id);
|
|
346
|
+
const status = formatTaskStatus(progress);
|
|
347
|
+
progressList.push({ id, status });
|
|
348
|
+
}
|
|
349
|
+
const nameWidth = Math.max(...progressList.map(p => p.id.length));
|
|
350
|
+
choices = progressList.map(p => ({
|
|
351
|
+
name: `${p.id.padEnd(nameWidth)} ${p.status}`,
|
|
352
|
+
value: p.id
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
choices = changeDirs.map(name => ({ name, value: name }));
|
|
357
|
+
}
|
|
358
|
+
// Let user select multiple changes
|
|
359
|
+
let selectedChanges;
|
|
360
|
+
if (options.yes) {
|
|
361
|
+
// With --yes, archive all active changes
|
|
362
|
+
selectedChanges = changeDirs;
|
|
363
|
+
console.log(`将归档所有 ${changeDirs.length} 个活动变更:${changeDirs.join(', ')}`);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
367
|
+
selectedChanges = await checkbox({
|
|
368
|
+
message: '选择要归档的变更(空格选择,回车确认)',
|
|
369
|
+
choices: choices.map(c => ({ name: c.name, value: c.value })),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (selectedChanges.length === 0) {
|
|
373
|
+
console.log('未选择任何变更。已中止。');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
// Check for overlapping specs (conflict detection)
|
|
377
|
+
const specCapabilities = new Map();
|
|
378
|
+
for (const changeName of selectedChanges) {
|
|
379
|
+
const changeDir = path.join(changesDir, changeName);
|
|
380
|
+
const changeSpecsDir = path.join(changeDir, 'specs');
|
|
381
|
+
try {
|
|
382
|
+
const specEntries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
|
383
|
+
const capabilities = specEntries
|
|
384
|
+
.filter(e => e.isDirectory())
|
|
385
|
+
.map(e => e.name);
|
|
386
|
+
for (const cap of capabilities) {
|
|
387
|
+
if (!specCapabilities.has(cap)) {
|
|
388
|
+
specCapabilities.set(cap, []);
|
|
389
|
+
}
|
|
390
|
+
specCapabilities.get(cap).push(changeName);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// No specs dir, skip
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Warn about conflicts
|
|
398
|
+
const conflicts = Array.from(specCapabilities.entries())
|
|
399
|
+
.filter(([, changes]) => changes.length > 1);
|
|
400
|
+
if (conflicts.length > 0) {
|
|
401
|
+
console.log(chalk.yellow('\n⚠️ 检测到规范冲突 - 以下功能被多个变更修改:'));
|
|
402
|
+
for (const [cap, changes] of conflicts) {
|
|
403
|
+
console.log(chalk.yellow(` ${cap}: ${changes.join(', ')}`));
|
|
404
|
+
}
|
|
405
|
+
if (!options.yes) {
|
|
406
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
407
|
+
const proceed = await confirm({
|
|
408
|
+
message: '存在规范冲突。继续按选择顺序归档?(后归档的会覆盖先归档的规范)',
|
|
409
|
+
default: false
|
|
410
|
+
});
|
|
411
|
+
if (!proceed) {
|
|
412
|
+
console.log('批量归档已取消。');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
console.log(chalk.yellow('由于 --yes 标志,按顺序继续归档。'));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Archive each change sequentially
|
|
421
|
+
let successCount = 0;
|
|
422
|
+
let failCount = 0;
|
|
423
|
+
for (const changeName of selectedChanges) {
|
|
424
|
+
console.log(chalk.cyan(`\n━━━ 归档: ${changeName} ━━━`));
|
|
425
|
+
try {
|
|
426
|
+
await this.execute(changeName, options);
|
|
427
|
+
successCount++;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
failCount++;
|
|
431
|
+
console.log(chalk.red(`归档 '${changeName}' 失败: ${error.message}`));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
console.log(chalk.cyan(`\n━━━ 批量归档完成 ━━━`));
|
|
435
|
+
console.log(`成功: ${successCount}, 失败: ${failCount}, 总计: ${selectedChanges.length}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Artifact, SchemaYaml, CompletedSet, BlockedArtifacts } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Represents an artifact dependency graph.
|
|
4
|
+
* Provides methods for querying build order, ready artifacts, and completion status.
|
|
5
|
+
*/
|
|
6
|
+
export declare class ArtifactGraph {
|
|
7
|
+
private artifacts;
|
|
8
|
+
private schema;
|
|
9
|
+
private constructor();
|
|
10
|
+
/**
|
|
11
|
+
* Creates an ArtifactGraph from a YAML file path.
|
|
12
|
+
*/
|
|
13
|
+
static fromYaml(filePath: string): ArtifactGraph;
|
|
14
|
+
/**
|
|
15
|
+
* Creates an ArtifactGraph from YAML content string.
|
|
16
|
+
*/
|
|
17
|
+
static fromYamlContent(yamlContent: string): ArtifactGraph;
|
|
18
|
+
/**
|
|
19
|
+
* Creates an ArtifactGraph from a pre-validated schema object.
|
|
20
|
+
*/
|
|
21
|
+
static fromSchema(schema: SchemaYaml): ArtifactGraph;
|
|
22
|
+
/**
|
|
23
|
+
* Gets a single artifact by ID.
|
|
24
|
+
*/
|
|
25
|
+
getArtifact(id: string): Artifact | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Gets all artifacts in the graph.
|
|
28
|
+
*/
|
|
29
|
+
getAllArtifacts(): Artifact[];
|
|
30
|
+
/**
|
|
31
|
+
* Gets the schema name.
|
|
32
|
+
*/
|
|
33
|
+
getName(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Gets the schema version.
|
|
36
|
+
*/
|
|
37
|
+
getVersion(): number;
|
|
38
|
+
/**
|
|
39
|
+
* Computes the topological build order using Kahn's algorithm.
|
|
40
|
+
* Returns artifact IDs in the order they should be built.
|
|
41
|
+
*/
|
|
42
|
+
getBuildOrder(): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Gets artifacts that are ready to be created (all dependencies completed).
|
|
45
|
+
*/
|
|
46
|
+
getNextArtifacts(completed: CompletedSet): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Checks if all artifacts in the graph are completed.
|
|
49
|
+
*/
|
|
50
|
+
isComplete(completed: CompletedSet): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Gets blocked artifacts and their unmet dependencies.
|
|
53
|
+
*/
|
|
54
|
+
getBlocked(completed: CompletedSet): BlockedArtifacts;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { loadSchema, parseSchema } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Represents an artifact dependency graph.
|
|
4
|
+
* Provides methods for querying build order, ready artifacts, and completion status.
|
|
5
|
+
*/
|
|
6
|
+
export class ArtifactGraph {
|
|
7
|
+
artifacts;
|
|
8
|
+
schema;
|
|
9
|
+
constructor(schema) {
|
|
10
|
+
this.schema = schema;
|
|
11
|
+
this.artifacts = new Map(schema.artifacts.map(a => [a.id, a]));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates an ArtifactGraph from a YAML file path.
|
|
15
|
+
*/
|
|
16
|
+
static fromYaml(filePath) {
|
|
17
|
+
const schema = loadSchema(filePath);
|
|
18
|
+
return new ArtifactGraph(schema);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates an ArtifactGraph from YAML content string.
|
|
22
|
+
*/
|
|
23
|
+
static fromYamlContent(yamlContent) {
|
|
24
|
+
const schema = parseSchema(yamlContent);
|
|
25
|
+
return new ArtifactGraph(schema);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates an ArtifactGraph from a pre-validated schema object.
|
|
29
|
+
*/
|
|
30
|
+
static fromSchema(schema) {
|
|
31
|
+
return new ArtifactGraph(schema);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Gets a single artifact by ID.
|
|
35
|
+
*/
|
|
36
|
+
getArtifact(id) {
|
|
37
|
+
return this.artifacts.get(id);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Gets all artifacts in the graph.
|
|
41
|
+
*/
|
|
42
|
+
getAllArtifacts() {
|
|
43
|
+
return Array.from(this.artifacts.values());
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Gets the schema name.
|
|
47
|
+
*/
|
|
48
|
+
getName() {
|
|
49
|
+
return this.schema.name;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Gets the schema version.
|
|
53
|
+
*/
|
|
54
|
+
getVersion() {
|
|
55
|
+
return this.schema.version;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Computes the topological build order using Kahn's algorithm.
|
|
59
|
+
* Returns artifact IDs in the order they should be built.
|
|
60
|
+
*/
|
|
61
|
+
getBuildOrder() {
|
|
62
|
+
const inDegree = new Map();
|
|
63
|
+
const dependents = new Map();
|
|
64
|
+
// Initialize all artifacts
|
|
65
|
+
for (const artifact of this.artifacts.values()) {
|
|
66
|
+
inDegree.set(artifact.id, artifact.requires.length);
|
|
67
|
+
dependents.set(artifact.id, []);
|
|
68
|
+
}
|
|
69
|
+
// Build reverse adjacency (who depends on whom)
|
|
70
|
+
for (const artifact of this.artifacts.values()) {
|
|
71
|
+
for (const req of artifact.requires) {
|
|
72
|
+
dependents.get(req).push(artifact.id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Start with roots (in-degree 0), sorted for determinism
|
|
76
|
+
const queue = [...this.artifacts.keys()]
|
|
77
|
+
.filter(id => inDegree.get(id) === 0)
|
|
78
|
+
.sort();
|
|
79
|
+
const result = [];
|
|
80
|
+
while (queue.length > 0) {
|
|
81
|
+
const current = queue.shift();
|
|
82
|
+
result.push(current);
|
|
83
|
+
// Collect newly ready artifacts, then sort before adding
|
|
84
|
+
const newlyReady = [];
|
|
85
|
+
for (const dep of dependents.get(current)) {
|
|
86
|
+
const newDegree = inDegree.get(dep) - 1;
|
|
87
|
+
inDegree.set(dep, newDegree);
|
|
88
|
+
if (newDegree === 0) {
|
|
89
|
+
newlyReady.push(dep);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
queue.push(...newlyReady.sort());
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Gets artifacts that are ready to be created (all dependencies completed).
|
|
98
|
+
*/
|
|
99
|
+
getNextArtifacts(completed) {
|
|
100
|
+
const ready = [];
|
|
101
|
+
for (const artifact of this.artifacts.values()) {
|
|
102
|
+
if (completed.has(artifact.id)) {
|
|
103
|
+
continue; // Already completed
|
|
104
|
+
}
|
|
105
|
+
const allDepsCompleted = artifact.requires.every(req => completed.has(req));
|
|
106
|
+
if (allDepsCompleted) {
|
|
107
|
+
ready.push(artifact.id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Sort for deterministic ordering
|
|
111
|
+
return ready.sort();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Checks if all artifacts in the graph are completed.
|
|
115
|
+
*/
|
|
116
|
+
isComplete(completed) {
|
|
117
|
+
for (const artifact of this.artifacts.values()) {
|
|
118
|
+
if (!completed.has(artifact.id)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Gets blocked artifacts and their unmet dependencies.
|
|
126
|
+
*/
|
|
127
|
+
getBlocked(completed) {
|
|
128
|
+
const blocked = {};
|
|
129
|
+
for (const artifact of this.artifacts.values()) {
|
|
130
|
+
if (completed.has(artifact.id)) {
|
|
131
|
+
continue; // Already completed
|
|
132
|
+
}
|
|
133
|
+
const unmetDeps = artifact.requires.filter(req => !completed.has(req));
|
|
134
|
+
if (unmetDeps.length > 0) {
|
|
135
|
+
blocked[artifact.id] = unmetDeps.sort();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return blocked;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ArtifactSchema, SchemaYamlSchema, type Artifact, type SchemaYaml, type CompletedSet, type BlockedArtifacts, } from './types.js';
|
|
2
|
+
export { loadSchema, parseSchema, SchemaValidationError } from './schema.js';
|
|
3
|
+
export { ArtifactGraph } from './graph.js';
|
|
4
|
+
export { detectCompleted } from './state.js';
|
|
5
|
+
export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, type SchemaInfo, } from './resolver.js';
|
|
6
|
+
export { loadTemplate, loadChangeContext, generateInstructions, formatChangeStatus, TemplateLoadError, type ChangeContext, type ArtifactInstructions, type DependencyInfo, type ArtifactStatus, type ChangeStatus, } from './instruction-loader.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export { ArtifactSchema, SchemaYamlSchema, } from './types.js';
|
|
3
|
+
// Schema loading and validation
|
|
4
|
+
export { loadSchema, parseSchema, SchemaValidationError } from './schema.js';
|
|
5
|
+
// Graph operations
|
|
6
|
+
export { ArtifactGraph } from './graph.js';
|
|
7
|
+
// State detection
|
|
8
|
+
export { detectCompleted } from './state.js';
|
|
9
|
+
// Schema resolution
|
|
10
|
+
export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, } from './resolver.js';
|
|
11
|
+
// Instruction loading
|
|
12
|
+
export { loadTemplate, loadChangeContext, generateInstructions, formatChangeStatus, TemplateLoadError, } from './instruction-loader.js';
|
|
13
|
+
//# sourceMappingURL=index.js.map
|