@superspec/cli 1.1.1 → 1.2.1
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 +21 -0
- package/README.md +374 -0
- package/dist/cli/index.js +763 -683
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +30 -30
- package/dist/index.js +275 -301
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/prompts/agents.md +35 -17
- package/prompts/rules.md +48 -27
- package/templates/en/checklist.md +31 -8
- package/templates/en/commands/ss-checklist.md +11 -6
- package/templates/en/commands/ss-create.md +35 -9
- package/templates/en/commands/ss-resume.md +18 -5
- package/templates/en/commands/ss-tasks.md +3 -2
- package/templates/en/proposal.md +14 -2
- package/templates/zh/checklist.md +31 -8
- package/templates/zh/commands/ss-checklist.md +11 -6
- package/templates/zh/commands/ss-create.md +34 -8
- package/templates/zh/commands/ss-resume.md +18 -5
- package/templates/zh/commands/ss-tasks.md +3 -2
- package/templates/zh/proposal.md +14 -2
package/dist/cli/index.js
CHANGED
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import { program } from "commander";
|
|
6
6
|
|
|
7
|
-
// src/commands/
|
|
8
|
-
import { existsSync as
|
|
9
|
-
import { join as
|
|
10
|
-
import { execSync as execSync2 } from "child_process";
|
|
7
|
+
// src/commands/archive.ts
|
|
8
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, renameSync } from "fs";
|
|
9
|
+
import { join as join2 } from "path";
|
|
11
10
|
|
|
12
11
|
// src/core/config.ts
|
|
13
|
-
import {
|
|
12
|
+
import { existsSync, readFileSync } from "fs";
|
|
14
13
|
import { join } from "path";
|
|
15
14
|
var DEFAULT_CONFIG = {
|
|
16
15
|
lang: "en",
|
|
@@ -72,129 +71,6 @@ function deepMerge(target, source) {
|
|
|
72
71
|
return result;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
// src/core/template.ts
|
|
76
|
-
import { existsSync as existsSync4, copyFileSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
77
|
-
import { join as join3, dirname as dirname2 } from "path";
|
|
78
|
-
|
|
79
|
-
// src/utils/paths.ts
|
|
80
|
-
import { dirname, join as join2 } from "path";
|
|
81
|
-
import { existsSync as existsSync2 } from "fs";
|
|
82
|
-
import { fileURLToPath } from "url";
|
|
83
|
-
function getPackageRoot() {
|
|
84
|
-
const __filename2 = fileURLToPath(import.meta.url);
|
|
85
|
-
let dir = dirname(__filename2);
|
|
86
|
-
while (dir !== dirname(dir)) {
|
|
87
|
-
if (existsSync2(join2(dir, "package.json")) && existsSync2(join2(dir, "templates"))) {
|
|
88
|
-
return dir;
|
|
89
|
-
}
|
|
90
|
-
dir = dirname(dir);
|
|
91
|
-
}
|
|
92
|
-
return join2(dirname(__filename2), "..", "..");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// src/utils/fs.ts
|
|
96
|
-
import { mkdirSync, existsSync as existsSync3, readdirSync } from "fs";
|
|
97
|
-
function ensureDir(dir) {
|
|
98
|
-
if (!existsSync3(dir)) {
|
|
99
|
-
mkdirSync(dir, { recursive: true });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
function resolveChangeNames(changesDir, name, archiveDirName) {
|
|
103
|
-
if (!existsSync3(changesDir)) return [];
|
|
104
|
-
if (name) return [name];
|
|
105
|
-
return readdirSync(changesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== archiveDirName).map((e) => e.name);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// src/core/template.ts
|
|
109
|
-
function resolveTemplatePath(templateName, lang = "zh") {
|
|
110
|
-
const root = getPackageRoot();
|
|
111
|
-
const langPath = join3(root, "templates", lang, templateName);
|
|
112
|
-
if (existsSync4(langPath)) return langPath;
|
|
113
|
-
const fallbackLang = lang === "zh" ? "en" : "zh";
|
|
114
|
-
const fallback = join3(root, "templates", fallbackLang, templateName);
|
|
115
|
-
if (existsSync4(fallback)) return fallback;
|
|
116
|
-
throw new Error(`Template not found: ${templateName} (lang: ${lang})`);
|
|
117
|
-
}
|
|
118
|
-
function copyTemplate(templateName, destPath, lang = "zh") {
|
|
119
|
-
const srcPath = resolveTemplatePath(templateName, lang);
|
|
120
|
-
ensureDir(dirname2(destPath));
|
|
121
|
-
copyFileSync(srcPath, destPath);
|
|
122
|
-
}
|
|
123
|
-
function renderTemplate(templateName, vars = {}, lang = "zh") {
|
|
124
|
-
const srcPath = resolveTemplatePath(templateName, lang);
|
|
125
|
-
let content = readFileSync2(srcPath, "utf-8");
|
|
126
|
-
content = processConditionals(content, vars);
|
|
127
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
128
|
-
content = content.replaceAll(`{{${key}}}`, value);
|
|
129
|
-
}
|
|
130
|
-
return content;
|
|
131
|
-
}
|
|
132
|
-
function processConditionals(content, vars) {
|
|
133
|
-
const ifRegex = /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g;
|
|
134
|
-
return content.replace(ifRegex, (match, varName, innerContent) => {
|
|
135
|
-
const value = vars[varName];
|
|
136
|
-
if (value && value.trim() !== "") {
|
|
137
|
-
return innerContent;
|
|
138
|
-
}
|
|
139
|
-
return "";
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
function writeRenderedTemplate(templateName, destPath, vars = {}, lang = "zh") {
|
|
143
|
-
const content = renderTemplate(templateName, vars, lang);
|
|
144
|
-
ensureDir(dirname2(destPath));
|
|
145
|
-
writeFileSync(destPath, content, "utf-8");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// src/utils/git.ts
|
|
149
|
-
import { execSync } from "child_process";
|
|
150
|
-
var GIT_TIMEOUT = 1e4;
|
|
151
|
-
function isGitRepo() {
|
|
152
|
-
try {
|
|
153
|
-
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore", timeout: GIT_TIMEOUT });
|
|
154
|
-
return true;
|
|
155
|
-
} catch {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
var SAFE_BRANCH_RE = /^[a-zA-Z0-9._\-/]+$/;
|
|
160
|
-
function createBranch(branchName) {
|
|
161
|
-
if (!SAFE_BRANCH_RE.test(branchName)) {
|
|
162
|
-
throw new Error(`invalid branch name: ${branchName}`);
|
|
163
|
-
}
|
|
164
|
-
execSync(`git checkout -b ${branchName}`, { stdio: "inherit", timeout: GIT_TIMEOUT });
|
|
165
|
-
}
|
|
166
|
-
function getDefaultBranch() {
|
|
167
|
-
try {
|
|
168
|
-
const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", { encoding: "utf-8", timeout: GIT_TIMEOUT }).trim();
|
|
169
|
-
return ref.replace("refs/remotes/origin/", "");
|
|
170
|
-
} catch {
|
|
171
|
-
try {
|
|
172
|
-
execSync("git rev-parse --verify main", { stdio: "ignore", timeout: GIT_TIMEOUT });
|
|
173
|
-
return "main";
|
|
174
|
-
} catch {
|
|
175
|
-
return "master";
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
function getDiffFiles(base) {
|
|
180
|
-
const baseBranch = base || getDefaultBranch();
|
|
181
|
-
try {
|
|
182
|
-
const mergeBase = execSync(`git merge-base ${baseBranch} HEAD`, { encoding: "utf-8", timeout: GIT_TIMEOUT }).trim();
|
|
183
|
-
const output = execSync(`git diff --name-status ${mergeBase}`, { encoding: "utf-8", timeout: 3e4 }).trim();
|
|
184
|
-
if (!output) return [];
|
|
185
|
-
return output.split("\n").map((line) => {
|
|
186
|
-
const [status, ...parts] = line.split(" ");
|
|
187
|
-
return { status: status.charAt(0), file: parts.join(" ") };
|
|
188
|
-
});
|
|
189
|
-
} catch {
|
|
190
|
-
return [];
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// src/prompts/index.ts
|
|
195
|
-
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
196
|
-
import { join as join4 } from "path";
|
|
197
|
-
|
|
198
74
|
// src/ui/index.ts
|
|
199
75
|
import chalk from "chalk";
|
|
200
76
|
var theme = {
|
|
@@ -243,7 +119,9 @@ function boxText(text, width = 50) {
|
|
|
243
119
|
}
|
|
244
120
|
function createBox(lines, width = 52) {
|
|
245
121
|
const top = theme.border(`${box.topLeft}${box.horizontal.repeat(width - 2)}${box.topRight}`);
|
|
246
|
-
const bottom = theme.border(
|
|
122
|
+
const bottom = theme.border(
|
|
123
|
+
`${box.bottomLeft}${box.horizontal.repeat(width - 2)}${box.bottomRight}`
|
|
124
|
+
);
|
|
247
125
|
const middle = lines.map((line) => boxText(line, width));
|
|
248
126
|
return [top, ...middle, bottom].join("\n");
|
|
249
127
|
}
|
|
@@ -299,7 +177,7 @@ function t(en, zh) {
|
|
|
299
177
|
function printSummary(items) {
|
|
300
178
|
const maxLabel = Math.max(...items.map((i) => i.label.length));
|
|
301
179
|
const width = 50;
|
|
302
|
-
console.log(theme.border(
|
|
180
|
+
console.log(theme.border(`\u256D${"\u2500".repeat(width - 2)}\u256E`));
|
|
303
181
|
for (const { label, value } of items) {
|
|
304
182
|
const padding = " ".repeat(maxLabel - label.length);
|
|
305
183
|
const line = `${theme.dim(label)}${padding} ${symbol.arrow} ${theme.highlight(value)}`;
|
|
@@ -307,181 +185,150 @@ function printSummary(items) {
|
|
|
307
185
|
const rightPad = " ".repeat(Math.max(0, width - plainLine.length - 4));
|
|
308
186
|
console.log(theme.border("\u2502 ") + line + rightPad + theme.border(" \u2502"));
|
|
309
187
|
}
|
|
310
|
-
console.log(theme.border(
|
|
188
|
+
console.log(theme.border(`\u2570${"\u2500".repeat(width - 2)}\u256F`));
|
|
311
189
|
}
|
|
312
190
|
|
|
313
|
-
// src/
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
},
|
|
325
|
-
qwen: {
|
|
326
|
-
commands: ".qwen/commands",
|
|
327
|
-
rules: ".qwen/rules",
|
|
328
|
-
rulesFile: "superspec.md"
|
|
329
|
-
},
|
|
330
|
-
opencode: {
|
|
331
|
-
commands: ".opencode/commands",
|
|
332
|
-
rules: null
|
|
333
|
-
},
|
|
334
|
-
codex: {
|
|
335
|
-
commands: ".codex/commands",
|
|
336
|
-
rules: null
|
|
337
|
-
},
|
|
338
|
-
codebuddy: {
|
|
339
|
-
commands: ".codebuddy/commands",
|
|
340
|
-
rules: ".codebuddy/rules",
|
|
341
|
-
rulesFile: "superspec.md"
|
|
342
|
-
},
|
|
343
|
-
qoder: {
|
|
344
|
-
commands: ".qoder/commands",
|
|
345
|
-
rules: ".qoder/rules",
|
|
346
|
-
rulesFile: "superspec.md"
|
|
191
|
+
// src/utils/date.ts
|
|
192
|
+
function getDateString() {
|
|
193
|
+
const d = /* @__PURE__ */ new Date();
|
|
194
|
+
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/utils/fs.ts
|
|
198
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync } from "fs";
|
|
199
|
+
function ensureDir(dir) {
|
|
200
|
+
if (!existsSync2(dir)) {
|
|
201
|
+
mkdirSync(dir, { recursive: true });
|
|
347
202
|
}
|
|
348
|
-
}
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
if (
|
|
203
|
+
}
|
|
204
|
+
function resolveChangeNames(changesDir, name, archiveDirName) {
|
|
205
|
+
if (!existsSync2(changesDir)) return [];
|
|
206
|
+
if (name) return [name];
|
|
207
|
+
return readdirSync(changesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== archiveDirName).map((e) => e.name);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/commands/archive.ts
|
|
211
|
+
async function archiveCommand(name, options) {
|
|
212
|
+
const cwd = process.cwd();
|
|
213
|
+
const config = loadConfig(cwd);
|
|
214
|
+
const changesDir = join2(cwd, config.specDir, "changes");
|
|
215
|
+
const archiveDir = join2(cwd, config.specDir, "changes", config.archive.dir);
|
|
216
|
+
if (!existsSync3(changesDir)) {
|
|
217
|
+
log.warn(`${symbol.warn} ${t("no changes directory found", "\u672A\u627E\u5230 changes \u76EE\u5F55")}`);
|
|
352
218
|
return;
|
|
353
219
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
log.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const newContent = readFileSync3(agentPromptSrc, "utf-8");
|
|
372
|
-
const wrapped = `${SS_START}
|
|
373
|
-
${newContent}
|
|
374
|
-
${SS_END}`;
|
|
375
|
-
if (existsSync5(agentsMdPath)) {
|
|
376
|
-
const existing = readFileSync3(agentsMdPath, "utf-8");
|
|
377
|
-
const startIdx = existing.indexOf(SS_START);
|
|
378
|
-
const endIdx = existing.indexOf(SS_END);
|
|
379
|
-
if (startIdx !== -1 && endIdx !== -1) {
|
|
380
|
-
const before = existing.slice(0, startIdx);
|
|
381
|
-
const after = existing.slice(endIdx + SS_END.length);
|
|
382
|
-
writeFileSync2(agentsMdPath, before + wrapped + after, "utf-8");
|
|
383
|
-
} else if (existing.includes("SuperSpec")) {
|
|
384
|
-
writeFileSync2(agentsMdPath, existing, "utf-8");
|
|
385
|
-
} else {
|
|
386
|
-
writeFileSync2(agentsMdPath, existing + "\n\n" + wrapped, "utf-8");
|
|
220
|
+
if (options.all) {
|
|
221
|
+
const entries = readdirSync2(changesDir, { withFileTypes: true }).filter(
|
|
222
|
+
(e) => e.isDirectory() && e.name !== config.archive.dir
|
|
223
|
+
);
|
|
224
|
+
if (entries.length === 0) {
|
|
225
|
+
log.warn(`${symbol.warn} ${t("no changes to archive", "\u6CA1\u6709\u53EF\u5F52\u6863\u7684\u53D8\u66F4")}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
log.info(`${symbol.start} ${t("archiving all changes...", "\u5F52\u6863\u6240\u6709\u53D8\u66F4...")}`);
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
archiveOne(entry.name, changesDir, archiveDir, config);
|
|
231
|
+
}
|
|
232
|
+
} else if (name) {
|
|
233
|
+
const changePath = join2(changesDir, name);
|
|
234
|
+
if (!existsSync3(changePath)) {
|
|
235
|
+
log.warn(`${symbol.warn} ${t(`change "${name}" not found`, `\u53D8\u66F4 "${name}" \u4E0D\u5B58\u5728`)}`);
|
|
236
|
+
return;
|
|
387
237
|
}
|
|
238
|
+
log.info(`${symbol.start} ${t(`archiving: ${name}`, `\u5F52\u6863\u53D8\u66F4: ${name}`)}`);
|
|
239
|
+
archiveOne(name, changesDir, archiveDir, config);
|
|
388
240
|
} else {
|
|
389
|
-
|
|
241
|
+
log.warn(`${symbol.warn} ${t("specify a name or use --all", "\u8BF7\u6307\u5B9A\u53D8\u66F4\u540D\u79F0\u6216\u4F7F\u7528 --all")}`);
|
|
242
|
+
return;
|
|
390
243
|
}
|
|
391
|
-
log.
|
|
244
|
+
log.info(`${symbol.start} ${t("archive done!", "\u5F52\u6863\u5B8C\u6210\uFF01")}`);
|
|
392
245
|
}
|
|
393
|
-
function
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (!existsSync5(sourceDir)) {
|
|
401
|
-
log.warn(`${symbol.warn} Commands templates not found: ${sourceDir}`);
|
|
246
|
+
function archiveOne(name, changesDir, archiveDir, config) {
|
|
247
|
+
ensureDir(archiveDir);
|
|
248
|
+
const src = join2(changesDir, name);
|
|
249
|
+
const dateStr = config.archive.datePrefix ? `${getDateString()}-` : "";
|
|
250
|
+
const dest = join2(archiveDir, `${dateStr}${name}`);
|
|
251
|
+
if (existsSync3(dest)) {
|
|
252
|
+
log.warn(` ${symbol.warn} ${t("archive target exists", "\u5F52\u6863\u76EE\u6807\u5DF2\u5B58\u5728")}: ${dest}`);
|
|
402
253
|
return;
|
|
403
254
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const srcPath = join4(sourceDir, file);
|
|
407
|
-
const destPath = join4(commandsDir, file);
|
|
408
|
-
const content = readFileSync3(srcPath, "utf-8");
|
|
409
|
-
writeFileSync2(destPath, content, "utf-8");
|
|
410
|
-
}
|
|
411
|
-
log.success(`${symbol.ok} ${config.commands}/ (${commandFiles.length} commands)`);
|
|
255
|
+
renameSync(src, dest);
|
|
256
|
+
log.success(` ${symbol.ok} ${name} \u2192 archive/${dateStr}${name}`);
|
|
412
257
|
}
|
|
413
258
|
|
|
414
|
-
// src/commands/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
config.lang = lang;
|
|
428
|
-
const aiEditor = options.ai;
|
|
429
|
-
if (aiEditor && AI_EDITORS[aiEditor]) {
|
|
430
|
-
config.aiEditor = aiEditor;
|
|
259
|
+
// src/commands/create.ts
|
|
260
|
+
import { existsSync as existsSync5 } from "fs";
|
|
261
|
+
import { join as join4 } from "path";
|
|
262
|
+
|
|
263
|
+
// src/utils/git.ts
|
|
264
|
+
import { execSync } from "child_process";
|
|
265
|
+
var GIT_TIMEOUT = 1e4;
|
|
266
|
+
function isGitRepo() {
|
|
267
|
+
try {
|
|
268
|
+
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore", timeout: GIT_TIMEOUT });
|
|
269
|
+
return true;
|
|
270
|
+
} catch {
|
|
271
|
+
return false;
|
|
431
272
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
console.log();
|
|
273
|
+
}
|
|
274
|
+
var SAFE_BRANCH_RE = /^[a-zA-Z0-9._\-/]+$/;
|
|
275
|
+
function createBranch(branchName) {
|
|
276
|
+
if (!SAFE_BRANCH_RE.test(branchName)) {
|
|
277
|
+
throw new Error(`invalid branch name: ${branchName}`);
|
|
438
278
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
for (const tpl of templateNames) {
|
|
279
|
+
execSync(`git checkout -b ${branchName}`, { stdio: "inherit", timeout: GIT_TIMEOUT });
|
|
280
|
+
}
|
|
281
|
+
function getDefaultBranch() {
|
|
282
|
+
try {
|
|
283
|
+
const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
284
|
+
encoding: "utf-8",
|
|
285
|
+
timeout: GIT_TIMEOUT
|
|
286
|
+
}).trim();
|
|
287
|
+
return ref.replace("refs/remotes/origin/", "");
|
|
288
|
+
} catch {
|
|
450
289
|
try {
|
|
451
|
-
|
|
290
|
+
execSync("git rev-parse --verify main", { stdio: "ignore", timeout: GIT_TIMEOUT });
|
|
291
|
+
return "main";
|
|
452
292
|
} catch {
|
|
293
|
+
return "master";
|
|
453
294
|
}
|
|
454
295
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
296
|
+
}
|
|
297
|
+
function getDiffFiles(base) {
|
|
298
|
+
const baseBranch = base || getDefaultBranch();
|
|
299
|
+
try {
|
|
300
|
+
const mergeBase = execSync(`git merge-base ${baseBranch} HEAD`, {
|
|
301
|
+
encoding: "utf-8",
|
|
302
|
+
timeout: GIT_TIMEOUT
|
|
303
|
+
}).trim();
|
|
304
|
+
const output = execSync(`git diff --name-status ${mergeBase}`, {
|
|
305
|
+
encoding: "utf-8",
|
|
306
|
+
timeout: 3e4
|
|
307
|
+
}).trim();
|
|
308
|
+
if (!output) return [];
|
|
309
|
+
return output.split("\n").map((line) => {
|
|
310
|
+
const [status, ...parts] = line.split(" ");
|
|
311
|
+
return { status: status.charAt(0), file: parts.join(" ") };
|
|
312
|
+
});
|
|
313
|
+
} catch {
|
|
314
|
+
return [];
|
|
465
315
|
}
|
|
466
|
-
console.log();
|
|
467
|
-
printSummary([
|
|
468
|
-
{ label: "Config", value: "superspec.config.json" },
|
|
469
|
-
{ label: "Spec dir", value: `${config.specDir}/` },
|
|
470
|
-
{ label: "AI agent", value: options.ai },
|
|
471
|
-
{ label: "Language", value: lang }
|
|
472
|
-
]);
|
|
473
|
-
log.done(t("SuperSpec initialized successfully!", "SuperSpec \u521D\u59CB\u5316\u6210\u529F\uFF01"));
|
|
474
|
-
log.dim(`${t("Next", "\u4E0B\u4E00\u6B65")}: superspec create <feature>`);
|
|
475
316
|
}
|
|
476
317
|
|
|
477
|
-
// src/
|
|
478
|
-
import { existsSync as
|
|
479
|
-
import { join as
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
318
|
+
// src/utils/paths.ts
|
|
319
|
+
import { existsSync as existsSync4 } from "fs";
|
|
320
|
+
import { dirname, join as join3 } from "path";
|
|
321
|
+
import { fileURLToPath } from "url";
|
|
322
|
+
function getPackageRoot() {
|
|
323
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
324
|
+
let dir = dirname(__filename2);
|
|
325
|
+
while (dir !== dirname(dir)) {
|
|
326
|
+
if (existsSync4(join3(dir, "package.json")) && existsSync4(join3(dir, "templates"))) {
|
|
327
|
+
return dir;
|
|
328
|
+
}
|
|
329
|
+
dir = dirname(dir);
|
|
330
|
+
}
|
|
331
|
+
return join3(dirname(__filename2), "..", "..");
|
|
485
332
|
}
|
|
486
333
|
|
|
487
334
|
// src/utils/template.ts
|
|
@@ -507,8 +354,6 @@ async function createCommand(feature, options) {
|
|
|
507
354
|
const branchPrefix = options.branchPrefix || config.branchPrefix;
|
|
508
355
|
const boost = options.boost || config.boost;
|
|
509
356
|
const strategy = options.creative ? "create" : config.strategy;
|
|
510
|
-
const description = options.description || "";
|
|
511
|
-
const lang = options.lang || config.lang || "en";
|
|
512
357
|
const templateVars = {
|
|
513
358
|
prefix: branchPrefix,
|
|
514
359
|
intentType: options.intentType,
|
|
@@ -518,9 +363,11 @@ async function createCommand(feature, options) {
|
|
|
518
363
|
};
|
|
519
364
|
const changeNameTemplate = options.changeNameTemplate || config.changeNameTemplate || "{date}-{feature}";
|
|
520
365
|
const changeFolderName = renderNameTemplate(changeNameTemplate, templateVars, false);
|
|
521
|
-
const changePath =
|
|
522
|
-
if (
|
|
523
|
-
log.warn(
|
|
366
|
+
const changePath = join4(cwd, specDir, "changes", changeFolderName);
|
|
367
|
+
if (existsSync5(changePath)) {
|
|
368
|
+
log.warn(
|
|
369
|
+
`${symbol.warn} ${t(`change "${changeFolderName}" already exists`, `\u53D8\u66F4 "${changeFolderName}" \u5DF2\u5B58\u5728`)}: ${changePath}`
|
|
370
|
+
);
|
|
524
371
|
return;
|
|
525
372
|
}
|
|
526
373
|
log.title(`${t("Creating Change", "\u521B\u5EFA\u53D8\u66F4")}: ${changeFolderName}`);
|
|
@@ -531,28 +378,11 @@ async function createCommand(feature, options) {
|
|
|
531
378
|
log.boost(`${symbol.bolt} ${t("Boost mode enabled", "\u589E\u5F3A\u6A21\u5F0F\u5DF2\u542F\u7528")}`);
|
|
532
379
|
}
|
|
533
380
|
if (strategy === "create") {
|
|
534
|
-
log.boost(
|
|
381
|
+
log.boost(
|
|
382
|
+
`${symbol.bolt} ${t("Creative mode: exploring new solutions", "\u521B\u9020\u6A21\u5F0F\uFF1A\u63A2\u7D22\u65B0\u65B9\u6848")}`
|
|
383
|
+
);
|
|
535
384
|
}
|
|
536
385
|
ensureDir(changePath);
|
|
537
|
-
const vars = {
|
|
538
|
-
name: changeFolderName,
|
|
539
|
-
date: templateVars.date,
|
|
540
|
-
boost: boost ? "true" : "false",
|
|
541
|
-
strategy,
|
|
542
|
-
description
|
|
543
|
-
};
|
|
544
|
-
const artifacts = boost ? config.boostArtifacts : config.artifacts;
|
|
545
|
-
log.section(t("Generating Artifacts", "\u751F\u6210 Artifacts"));
|
|
546
|
-
for (const artifact of artifacts) {
|
|
547
|
-
const templateFile = config.templates[artifact] || `${artifact}.md`;
|
|
548
|
-
const destPath = join6(changePath, `${artifact}.md`);
|
|
549
|
-
try {
|
|
550
|
-
writeRenderedTemplate(templateFile, destPath, vars, lang);
|
|
551
|
-
log.success(`${symbol.ok} ${artifact}.md`);
|
|
552
|
-
} catch (e) {
|
|
553
|
-
log.error(`${symbol.fail} ${artifact}.md: ${e.message}`);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
386
|
if (options.branch !== false && isGitRepo()) {
|
|
557
387
|
const branchTemplate = options.branchTemplate || config.branchTemplate || "{prefix}{date}-{feature}";
|
|
558
388
|
const branchName = renderNameTemplate(branchTemplate, templateVars, true);
|
|
@@ -565,168 +395,17 @@ async function createCommand(feature, options) {
|
|
|
565
395
|
}
|
|
566
396
|
log.done(t("Change created successfully!", "\u53D8\u66F4\u521B\u5EFA\u6210\u529F\uFF01"));
|
|
567
397
|
log.dim(`${t("Path", "\u8DEF\u5F84")}: ${specDir}/changes/${changeFolderName}/`);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// src/commands/archive.ts
|
|
577
|
-
import { existsSync as existsSync8, readdirSync as readdirSync4, renameSync } from "fs";
|
|
578
|
-
import { join as join7 } from "path";
|
|
579
|
-
async function archiveCommand(name, options) {
|
|
580
|
-
const cwd = process.cwd();
|
|
581
|
-
const config = loadConfig(cwd);
|
|
582
|
-
const changesDir = join7(cwd, config.specDir, "changes");
|
|
583
|
-
const archiveDir = join7(cwd, config.specDir, "changes", config.archive.dir);
|
|
584
|
-
if (!existsSync8(changesDir)) {
|
|
585
|
-
log.warn(`${symbol.warn} ${t("no changes directory found", "\u672A\u627E\u5230 changes \u76EE\u5F55")}`);
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
if (options.all) {
|
|
589
|
-
const entries = readdirSync4(changesDir, { withFileTypes: true }).filter(
|
|
590
|
-
(e) => e.isDirectory() && e.name !== config.archive.dir
|
|
591
|
-
);
|
|
592
|
-
if (entries.length === 0) {
|
|
593
|
-
log.warn(`${symbol.warn} ${t("no changes to archive", "\u6CA1\u6709\u53EF\u5F52\u6863\u7684\u53D8\u66F4")}`);
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
log.info(`${symbol.start} ${t("archiving all changes...", "\u5F52\u6863\u6240\u6709\u53D8\u66F4...")}`);
|
|
597
|
-
for (const entry of entries) {
|
|
598
|
-
archiveOne(entry.name, changesDir, archiveDir, config);
|
|
599
|
-
}
|
|
600
|
-
} else if (name) {
|
|
601
|
-
const changePath = join7(changesDir, name);
|
|
602
|
-
if (!existsSync8(changePath)) {
|
|
603
|
-
log.warn(`${symbol.warn} ${t(`change "${name}" not found`, `\u53D8\u66F4 "${name}" \u4E0D\u5B58\u5728`)}`);
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
log.info(`${symbol.start} ${t(`archiving: ${name}`, `\u5F52\u6863\u53D8\u66F4: ${name}`)}`);
|
|
607
|
-
archiveOne(name, changesDir, archiveDir, config);
|
|
608
|
-
} else {
|
|
609
|
-
log.warn(`${symbol.warn} ${t("specify a name or use --all", "\u8BF7\u6307\u5B9A\u53D8\u66F4\u540D\u79F0\u6216\u4F7F\u7528 --all")}`);
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
log.info(`${symbol.start} ${t("archive done!", "\u5F52\u6863\u5B8C\u6210\uFF01")}`);
|
|
613
|
-
}
|
|
614
|
-
function archiveOne(name, changesDir, archiveDir, config) {
|
|
615
|
-
ensureDir(archiveDir);
|
|
616
|
-
const src = join7(changesDir, name);
|
|
617
|
-
const dateStr = config.archive.datePrefix ? `${getDateString()}-` : "";
|
|
618
|
-
const dest = join7(archiveDir, `${dateStr}${name}`);
|
|
619
|
-
if (existsSync8(dest)) {
|
|
620
|
-
log.warn(` ${symbol.warn} ${t("archive target exists", "\u5F52\u6863\u76EE\u6807\u5DF2\u5B58\u5728")}: ${dest}`);
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
renameSync(src, dest);
|
|
624
|
-
log.success(` ${symbol.ok} ${name} \u2192 archive/${dateStr}${name}`);
|
|
398
|
+
log.dim(`${t("Templates", "\u6A21\u677F\u53C2\u8003")}: ${specDir}/templates/`);
|
|
399
|
+
const expectedArtifacts = boost ? config.boostArtifacts : config.artifacts;
|
|
400
|
+
log.dim(`${t("Expected artifacts", "\u9884\u671F Artifacts")}: ${expectedArtifacts.join(", ")}`);
|
|
401
|
+
log.dim(
|
|
402
|
+
`${t("Next", "\u4E0B\u4E00\u6B65")}: ${t("AI generates artifacts on demand via /ss-create", "AI \u6309\u9700\u901A\u8FC7 /ss-create \u751F\u6210 artifacts")}`
|
|
403
|
+
);
|
|
625
404
|
}
|
|
626
405
|
|
|
627
|
-
// src/commands/
|
|
628
|
-
import { existsSync as
|
|
629
|
-
import { join as
|
|
630
|
-
async function updateCommand() {
|
|
631
|
-
const cwd = process.cwd();
|
|
632
|
-
const config = loadConfig(cwd);
|
|
633
|
-
const specDir = join8(cwd, config.specDir);
|
|
634
|
-
const lang = config.lang || "zh";
|
|
635
|
-
if (!existsSync9(join8(cwd, "superspec.config.json"))) {
|
|
636
|
-
log.warn(`${symbol.warn} ${t("not initialized, run superspec init first", "\u5F53\u524D\u76EE\u5F55\u672A\u521D\u59CB\u5316 SuperSpec\uFF0C\u8BF7\u5148\u8FD0\u884C superspec init")}`);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
log.info(`${symbol.start} ${t("updating SuperSpec...", "\u66F4\u65B0 SuperSpec...")}`);
|
|
640
|
-
const templateNames = Object.values(config.templates).map((v) => v.endsWith(".md") ? v : `${v}.md`);
|
|
641
|
-
ensureDir(join8(specDir, "templates"));
|
|
642
|
-
for (const tpl of templateNames) {
|
|
643
|
-
try {
|
|
644
|
-
copyTemplate(tpl, join8(specDir, "templates", tpl), lang);
|
|
645
|
-
} catch {
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
log.success(` ${symbol.ok} ${t("templates updated", "\u6A21\u677F\u66F4\u65B0")} (${lang})`);
|
|
649
|
-
installAgentsMd(cwd);
|
|
650
|
-
const aiEditor = config.aiEditor;
|
|
651
|
-
if (aiEditor && AI_EDITORS[aiEditor]) {
|
|
652
|
-
installRules(cwd, aiEditor);
|
|
653
|
-
installCommands(cwd, aiEditor, lang);
|
|
654
|
-
}
|
|
655
|
-
log.info(`${symbol.start} ${t("update done!", "\u66F4\u65B0\u5B8C\u6210\uFF01")}`);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// src/commands/lint.ts
|
|
659
|
-
import { existsSync as existsSync11 } from "fs";
|
|
660
|
-
import { join as join10 } from "path";
|
|
661
|
-
|
|
662
|
-
// src/core/lint.ts
|
|
663
|
-
import { readFileSync as readFileSync4, existsSync as existsSync10, readdirSync as readdirSync5 } from "fs";
|
|
664
|
-
import { join as join9, basename } from "path";
|
|
665
|
-
function lintArtifact(filePath, targetLines, hardLines) {
|
|
666
|
-
const artifact = basename(filePath);
|
|
667
|
-
if (!existsSync10(filePath)) {
|
|
668
|
-
return { artifact, lines: 0, status: "ok", message: "not found" };
|
|
669
|
-
}
|
|
670
|
-
const content = readFileSync4(filePath, "utf-8");
|
|
671
|
-
const lines = content.split("\n").length;
|
|
672
|
-
if (lines > hardLines) {
|
|
673
|
-
return { artifact, lines, status: "error", message: `${lines} lines exceeds hard limit (${hardLines}). Must split.` };
|
|
674
|
-
}
|
|
675
|
-
if (lines > targetLines) {
|
|
676
|
-
return { artifact, lines, status: "warn", message: `${lines} lines exceeds target (${targetLines}). Consider splitting.` };
|
|
677
|
-
}
|
|
678
|
-
return { artifact, lines, status: "ok", message: `${lines} lines` };
|
|
679
|
-
}
|
|
680
|
-
function lintChange(changePath, targetLines, hardLines) {
|
|
681
|
-
if (!existsSync10(changePath)) return [];
|
|
682
|
-
const files = readdirSync5(changePath).filter((f) => f.endsWith(".md"));
|
|
683
|
-
return files.map((f) => lintArtifact(join9(changePath, f), targetLines, hardLines));
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// src/commands/lint.ts
|
|
687
|
-
async function lintCommand(name) {
|
|
688
|
-
const cwd = process.cwd();
|
|
689
|
-
const config = loadConfig(cwd);
|
|
690
|
-
const changesDir = join10(cwd, config.specDir, "changes");
|
|
691
|
-
const { targetLines, hardLines } = config.limits;
|
|
692
|
-
const names = resolveChangeNames(changesDir, name, config.archive.dir);
|
|
693
|
-
if (names.length === 0) {
|
|
694
|
-
log.warn(`${symbol.warn} ${t("no changes to lint", "\u6CA1\u6709\u53EF\u68C0\u67E5\u7684\u53D8\u66F4")}`);
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
let hasIssues = false;
|
|
698
|
-
for (const n of names) {
|
|
699
|
-
const changePath = join10(changesDir, n);
|
|
700
|
-
if (!existsSync11(changePath)) {
|
|
701
|
-
log.warn(`${symbol.warn} "${n}" ${t("not found", "\u672A\u627E\u5230")}`);
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
const results = lintChange(changePath, targetLines, hardLines);
|
|
705
|
-
log.info(`${symbol.start} ${n}`);
|
|
706
|
-
for (const r of results) {
|
|
707
|
-
if (r.status === "error") {
|
|
708
|
-
log.error(` ${symbol.fail} ${r.artifact}: ${r.message}`);
|
|
709
|
-
hasIssues = true;
|
|
710
|
-
} else if (r.status === "warn") {
|
|
711
|
-
log.warn(` ${symbol.warn} ${r.artifact}: ${r.message}`);
|
|
712
|
-
hasIssues = true;
|
|
713
|
-
} else {
|
|
714
|
-
log.success(` ${symbol.ok} ${r.artifact}: ${r.message}`);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
if (!hasIssues) {
|
|
719
|
-
log.info(`${symbol.start} ${t("all artifacts within limits", "\u6240\u6709 artifact \u5747\u5728\u9650\u5236\u8303\u56F4\u5185")}`);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// src/commands/validate.ts
|
|
724
|
-
import { existsSync as existsSync13 } from "fs";
|
|
725
|
-
import { join as join12 } from "path";
|
|
726
|
-
|
|
727
|
-
// src/core/validate.ts
|
|
728
|
-
import { readFileSync as readFileSync5, existsSync as existsSync12 } from "fs";
|
|
729
|
-
import { join as join11 } from "path";
|
|
406
|
+
// src/commands/deps.ts
|
|
407
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
408
|
+
import { join as join5 } from "path";
|
|
730
409
|
|
|
731
410
|
// src/core/frontmatter.ts
|
|
732
411
|
var FM_REGEX = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
|
|
@@ -779,149 +458,391 @@ function addDependency(content, depName) {
|
|
|
779
458
|
deps2.push(depName);
|
|
780
459
|
}
|
|
781
460
|
meta.depends_on = deps2;
|
|
782
|
-
return serializeFrontmatter(meta)
|
|
461
|
+
return `${serializeFrontmatter(meta)}
|
|
462
|
+
${body}`;
|
|
783
463
|
}
|
|
784
464
|
function removeDependency(content, depName) {
|
|
785
465
|
const { meta, body } = parseFrontmatter(content);
|
|
786
466
|
const deps2 = Array.isArray(meta.depends_on) ? meta.depends_on : [];
|
|
787
467
|
meta.depends_on = deps2.filter((d) => d !== depName);
|
|
788
|
-
return serializeFrontmatter(meta)
|
|
468
|
+
return `${serializeFrontmatter(meta)}
|
|
469
|
+
${body}`;
|
|
789
470
|
}
|
|
790
471
|
|
|
791
|
-
// src/
|
|
792
|
-
function
|
|
793
|
-
const
|
|
794
|
-
|
|
472
|
+
// src/commands/deps.ts
|
|
473
|
+
async function depsAddCommand(name, options) {
|
|
474
|
+
const cwd = process.cwd();
|
|
475
|
+
const config = loadConfig(cwd);
|
|
476
|
+
const proposalPath = join5(cwd, config.specDir, "changes", name, "proposal.md");
|
|
477
|
+
if (!existsSync6(proposalPath)) {
|
|
478
|
+
log.warn(`${symbol.warn} "${name}" ${t("not found", "\u672A\u627E\u5230")}`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const content = readFileSync2(proposalPath, "utf-8");
|
|
482
|
+
const updated = addDependency(content, options.on);
|
|
483
|
+
writeFileSync(proposalPath, updated, "utf-8");
|
|
484
|
+
log.success(`${symbol.ok} ${name} \u2192 depends_on: ${options.on}`);
|
|
795
485
|
}
|
|
796
|
-
function
|
|
797
|
-
const
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const spec = read("spec.md");
|
|
804
|
-
const tasks = read("tasks.md");
|
|
805
|
-
if (!proposal) issues.push({ level: "warn", artifact: "proposal.md", message: "missing" });
|
|
806
|
-
if (!tasks) issues.push({ level: "warn", artifact: "tasks.md", message: "missing" });
|
|
807
|
-
if (proposal && spec) {
|
|
808
|
-
const proposalGoals = extractIds(proposal, /US-\d+/g);
|
|
809
|
-
const specUS = extractIds(spec, /US-\d+/g);
|
|
810
|
-
for (const id of proposalGoals) {
|
|
811
|
-
if (!specUS.includes(id)) {
|
|
812
|
-
issues.push({ level: "error", artifact: "spec.md", message: `${id} from proposal not found in spec` });
|
|
813
|
-
}
|
|
814
|
-
}
|
|
486
|
+
async function depsRemoveCommand(name, options) {
|
|
487
|
+
const cwd = process.cwd();
|
|
488
|
+
const config = loadConfig(cwd);
|
|
489
|
+
const proposalPath = join5(cwd, config.specDir, "changes", name, "proposal.md");
|
|
490
|
+
if (!existsSync6(proposalPath)) {
|
|
491
|
+
log.warn(`${symbol.warn} "${name}" ${t("not found", "\u672A\u627E\u5230")}`);
|
|
492
|
+
return;
|
|
815
493
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
494
|
+
const content = readFileSync2(proposalPath, "utf-8");
|
|
495
|
+
const updated = removeDependency(content, options.on);
|
|
496
|
+
writeFileSync(proposalPath, updated, "utf-8");
|
|
497
|
+
log.success(`${symbol.ok} ${name} \u2192 ${t("removed dependency", "\u79FB\u9664\u4F9D\u8D56")}: ${options.on}`);
|
|
498
|
+
}
|
|
499
|
+
async function depsListCommand(name) {
|
|
500
|
+
const cwd = process.cwd();
|
|
501
|
+
const config = loadConfig(cwd);
|
|
502
|
+
const changesDir = join5(cwd, config.specDir, "changes");
|
|
503
|
+
if (name) {
|
|
504
|
+
const proposalPath = join5(changesDir, name, "proposal.md");
|
|
505
|
+
if (!existsSync6(proposalPath)) {
|
|
506
|
+
log.warn(`${symbol.warn} "${name}" ${t("not found", "\u672A\u627E\u5230")}`);
|
|
507
|
+
return;
|
|
823
508
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
509
|
+
const content = readFileSync2(proposalPath, "utf-8");
|
|
510
|
+
const { meta } = parseFrontmatter(content);
|
|
511
|
+
const deps2 = Array.isArray(meta.depends_on) ? meta.depends_on : [];
|
|
512
|
+
log.info(`${symbol.start} ${name}`);
|
|
513
|
+
if (deps2.length === 0) {
|
|
514
|
+
log.dim(` ${t("no dependencies", "\u65E0\u4F9D\u8D56")}`);
|
|
515
|
+
} else {
|
|
516
|
+
for (const d of deps2) {
|
|
517
|
+
log.dim(` \u2192 ${d}`);
|
|
827
518
|
}
|
|
828
519
|
}
|
|
520
|
+
return;
|
|
829
521
|
}
|
|
830
|
-
if (
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
522
|
+
if (!existsSync6(changesDir)) {
|
|
523
|
+
log.warn(`${symbol.warn} ${t("no changes directory", "\u672A\u627E\u5230 changes \u76EE\u5F55")}`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const entries = readdirSync3(changesDir, { withFileTypes: true }).filter(
|
|
527
|
+
(e) => e.isDirectory() && e.name !== config.archive.dir
|
|
528
|
+
);
|
|
529
|
+
log.info(`${symbol.start} ${t("dependency graph", "\u4F9D\u8D56\u5173\u7CFB")}`);
|
|
530
|
+
for (const entry of entries) {
|
|
531
|
+
const proposalPath = join5(changesDir, entry.name, "proposal.md");
|
|
532
|
+
if (!existsSync6(proposalPath)) continue;
|
|
533
|
+
const content = readFileSync2(proposalPath, "utf-8");
|
|
534
|
+
const { meta } = parseFrontmatter(content);
|
|
535
|
+
const deps2 = Array.isArray(meta.depends_on) ? meta.depends_on : [];
|
|
536
|
+
if (deps2.length > 0) {
|
|
537
|
+
log.dim(` ${entry.name} \u2192 [${deps2.join(", ")}]`);
|
|
538
|
+
} else {
|
|
539
|
+
log.dim(` ${entry.name}`);
|
|
839
540
|
}
|
|
840
541
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/commands/init.ts
|
|
545
|
+
import { execSync as execSync2 } from "child_process";
|
|
546
|
+
import { existsSync as existsSync9, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
547
|
+
import { join as join8 } from "path";
|
|
548
|
+
|
|
549
|
+
// src/core/template.ts
|
|
550
|
+
import { copyFileSync, existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
551
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
552
|
+
function resolveTemplatePath(templateName, lang = "zh") {
|
|
553
|
+
const root = getPackageRoot();
|
|
554
|
+
const langPath = join6(root, "templates", lang, templateName);
|
|
555
|
+
if (existsSync7(langPath)) return langPath;
|
|
556
|
+
const fallbackLang = lang === "zh" ? "en" : "zh";
|
|
557
|
+
const fallback = join6(root, "templates", fallbackLang, templateName);
|
|
558
|
+
if (existsSync7(fallback)) return fallback;
|
|
559
|
+
throw new Error(`Template not found: ${templateName} (lang: ${lang})`);
|
|
560
|
+
}
|
|
561
|
+
function copyTemplate(templateName, destPath, lang = "zh") {
|
|
562
|
+
const srcPath = resolveTemplatePath(templateName, lang);
|
|
563
|
+
ensureDir(dirname2(destPath));
|
|
564
|
+
copyFileSync(srcPath, destPath);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/prompts/index.ts
|
|
568
|
+
import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
569
|
+
import { join as join7 } from "path";
|
|
570
|
+
var AI_EDITORS = {
|
|
571
|
+
claude: {
|
|
572
|
+
commands: ".claude/commands",
|
|
573
|
+
rules: null
|
|
574
|
+
// Claude doesn't use rules files
|
|
575
|
+
},
|
|
576
|
+
cursor: {
|
|
577
|
+
commands: ".cursor/commands",
|
|
578
|
+
rules: ".cursor/rules",
|
|
579
|
+
rulesFile: "superspec.mdc"
|
|
580
|
+
},
|
|
581
|
+
qwen: {
|
|
582
|
+
commands: ".qwen/commands",
|
|
583
|
+
rules: ".qwen/rules",
|
|
584
|
+
rulesFile: "superspec.md"
|
|
585
|
+
},
|
|
586
|
+
opencode: {
|
|
587
|
+
commands: ".opencode/commands",
|
|
588
|
+
rules: null
|
|
589
|
+
},
|
|
590
|
+
codex: {
|
|
591
|
+
commands: ".codex/commands",
|
|
592
|
+
rules: null
|
|
593
|
+
},
|
|
594
|
+
codebuddy: {
|
|
595
|
+
commands: ".codebuddy/commands",
|
|
596
|
+
rules: ".codebuddy/rules",
|
|
597
|
+
rulesFile: "superspec.md"
|
|
598
|
+
},
|
|
599
|
+
qoder: {
|
|
600
|
+
commands: ".qoder/commands",
|
|
601
|
+
rules: ".qoder/rules",
|
|
602
|
+
rulesFile: "superspec.md"
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
function installRules(cwd, editor) {
|
|
606
|
+
const config = AI_EDITORS[editor];
|
|
607
|
+
if (!config.rules) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const rulesDir = join7(cwd, config.rules);
|
|
611
|
+
ensureDir(rulesDir);
|
|
612
|
+
const promptSrc = join7(getPackageRoot(), "prompts", "rules.md");
|
|
613
|
+
if (existsSync8(promptSrc)) {
|
|
614
|
+
const content = readFileSync4(promptSrc, "utf-8");
|
|
615
|
+
const rulesFile = "rulesFile" in config ? config.rulesFile : "superspec.md";
|
|
616
|
+
const destPath = join7(rulesDir, rulesFile);
|
|
617
|
+
writeFileSync3(destPath, content, "utf-8");
|
|
618
|
+
log.success(`${symbol.ok} ${config.rules}/${rulesFile}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
var SS_START = "<!-- superspec:start -->";
|
|
622
|
+
var SS_END = "<!-- superspec:end -->";
|
|
623
|
+
function installAgentsMd(cwd) {
|
|
624
|
+
const agentsMdPath = join7(cwd, "AGENTS.md");
|
|
625
|
+
const agentPromptSrc = join7(getPackageRoot(), "prompts", "agents.md");
|
|
626
|
+
if (!existsSync8(agentPromptSrc)) return;
|
|
627
|
+
const newContent = readFileSync4(agentPromptSrc, "utf-8");
|
|
628
|
+
const wrapped = `${SS_START}
|
|
629
|
+
${newContent}
|
|
630
|
+
${SS_END}`;
|
|
631
|
+
if (existsSync8(agentsMdPath)) {
|
|
632
|
+
const existing = readFileSync4(agentsMdPath, "utf-8");
|
|
633
|
+
const startIdx = existing.indexOf(SS_START);
|
|
634
|
+
const endIdx = existing.indexOf(SS_END);
|
|
635
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
636
|
+
const before = existing.slice(0, startIdx);
|
|
637
|
+
const after = existing.slice(endIdx + SS_END.length);
|
|
638
|
+
writeFileSync3(agentsMdPath, before + wrapped + after, "utf-8");
|
|
639
|
+
} else if (existing.includes("SuperSpec")) {
|
|
640
|
+
writeFileSync3(agentsMdPath, existing, "utf-8");
|
|
641
|
+
} else {
|
|
642
|
+
writeFileSync3(agentsMdPath, `${existing}
|
|
643
|
+
|
|
644
|
+
${wrapped}`, "utf-8");
|
|
850
645
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
646
|
+
} else {
|
|
647
|
+
writeFileSync3(agentsMdPath, wrapped, "utf-8");
|
|
648
|
+
}
|
|
649
|
+
log.success(`${symbol.ok} AGENTS.md`);
|
|
650
|
+
}
|
|
651
|
+
function installCommands(cwd, editor, lang = "zh") {
|
|
652
|
+
const config = AI_EDITORS[editor];
|
|
653
|
+
const commandsDir = join7(cwd, config.commands);
|
|
654
|
+
ensureDir(commandsDir);
|
|
655
|
+
const templatesDir = join7(getPackageRoot(), "templates", lang, "commands");
|
|
656
|
+
const fallbackDir = join7(getPackageRoot(), "templates", "zh", "commands");
|
|
657
|
+
const sourceDir = existsSync8(templatesDir) ? templatesDir : fallbackDir;
|
|
658
|
+
if (!existsSync8(sourceDir)) {
|
|
659
|
+
log.warn(`${symbol.warn} Commands templates not found: ${sourceDir}`);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const commandFiles = readdirSync4(sourceDir).filter((f) => f.endsWith(".md"));
|
|
663
|
+
for (const file of commandFiles) {
|
|
664
|
+
const srcPath = join7(sourceDir, file);
|
|
665
|
+
const destPath = join7(commandsDir, file);
|
|
666
|
+
const content = readFileSync4(srcPath, "utf-8");
|
|
667
|
+
writeFileSync3(destPath, content, "utf-8");
|
|
668
|
+
}
|
|
669
|
+
log.success(`${symbol.ok} ${config.commands}/ (${commandFiles.length} commands)`);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/commands/init.ts
|
|
673
|
+
async function initCommand(options) {
|
|
674
|
+
const cwd = process.cwd();
|
|
675
|
+
const configPath = join8(cwd, "superspec.config.json");
|
|
676
|
+
if (existsSync9(configPath) && !options.force) {
|
|
677
|
+
log.warn(
|
|
678
|
+
`${symbol.warn} ${t("superspec.config.json already exists, use --force to overwrite", "superspec.config.json \u5DF2\u5B58\u5728\uFF0C\u4F7F\u7528 --force \u8986\u76D6")}`
|
|
679
|
+
);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const lang = options.lang || "zh";
|
|
683
|
+
setLang(lang);
|
|
684
|
+
printLogo("small");
|
|
685
|
+
console.log(theme.dim(" Spec-Driven Development Toolkit\n"));
|
|
686
|
+
const config = getDefaultConfig();
|
|
687
|
+
config.lang = lang;
|
|
688
|
+
const aiEditor = options.ai;
|
|
689
|
+
if (aiEditor && AI_EDITORS[aiEditor]) {
|
|
690
|
+
config.aiEditor = aiEditor;
|
|
691
|
+
}
|
|
692
|
+
const specDir = join8(cwd, config.specDir);
|
|
693
|
+
const existingFiles = readdirSync5(cwd).filter((f) => !f.startsWith(".") && f !== "node_modules");
|
|
694
|
+
if (existingFiles.length > 0 && !options.force) {
|
|
695
|
+
log.warn(
|
|
696
|
+
`${symbol.warn} ${t(`current directory is not empty (${existingFiles.length} items)`, `\u5F53\u524D\u76EE\u5F55\u975E\u7A7A\uFF08${existingFiles.length} \u9879\uFF09`)}`
|
|
697
|
+
);
|
|
698
|
+
log.dim(
|
|
699
|
+
` ${t("template files will be merged with existing content", "\u6A21\u677F\u6587\u4EF6\u5C06\u4E0E\u73B0\u6709\u5185\u5BB9\u5408\u5E76")}`
|
|
700
|
+
);
|
|
701
|
+
console.log();
|
|
702
|
+
}
|
|
703
|
+
log.section(t("Creating Configuration", "\u521B\u5EFA\u914D\u7F6E"));
|
|
704
|
+
writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
|
|
705
|
+
`, "utf-8");
|
|
706
|
+
log.success(`${symbol.file} superspec.config.json`);
|
|
707
|
+
log.section(t("Creating Directory Structure", "\u521B\u5EFA\u76EE\u5F55\u7ED3\u6784"));
|
|
708
|
+
ensureDir(join8(specDir, "changes"));
|
|
709
|
+
ensureDir(join8(specDir, "templates"));
|
|
710
|
+
log.success(`${symbol.folder} ${config.specDir}/changes/`);
|
|
711
|
+
log.success(`${symbol.folder} ${config.specDir}/templates/`);
|
|
712
|
+
log.section(t("Installing Templates", "\u5B89\u88C5\u6A21\u677F"));
|
|
713
|
+
const templateNames = Object.values(config.templates).map(
|
|
714
|
+
(v) => v.endsWith(".md") ? v : `${v}.md`
|
|
715
|
+
);
|
|
716
|
+
for (const tpl of templateNames) {
|
|
717
|
+
try {
|
|
718
|
+
copyTemplate(tpl, join8(specDir, "templates", tpl), lang);
|
|
719
|
+
} catch {
|
|
856
720
|
}
|
|
857
721
|
}
|
|
858
|
-
|
|
722
|
+
log.success(`${symbol.ok} ${templateNames.length} ${t("templates", "\u4E2A\u6A21\u677F")} (${lang})`);
|
|
723
|
+
log.section(t("Installing AI Agent Files", "\u5B89\u88C5 AI Agent \u6587\u4EF6"));
|
|
724
|
+
installAgentsMd(cwd);
|
|
725
|
+
if (aiEditor && AI_EDITORS[aiEditor]) {
|
|
726
|
+
installRules(cwd, aiEditor);
|
|
727
|
+
installCommands(cwd, aiEditor, lang);
|
|
728
|
+
}
|
|
729
|
+
if (options.git !== false && !isGitRepo()) {
|
|
730
|
+
execSync2("git init", { cwd, stdio: "inherit" });
|
|
731
|
+
log.success(`${symbol.git} git init`);
|
|
732
|
+
}
|
|
733
|
+
console.log();
|
|
734
|
+
printSummary([
|
|
735
|
+
{ label: "Config", value: "superspec.config.json" },
|
|
736
|
+
{ label: "Spec dir", value: `${config.specDir}/` },
|
|
737
|
+
{ label: "AI agent", value: options.ai },
|
|
738
|
+
{ label: "Language", value: lang }
|
|
739
|
+
]);
|
|
740
|
+
log.done(t("SuperSpec initialized successfully!", "SuperSpec \u521D\u59CB\u5316\u6210\u529F\uFF01"));
|
|
741
|
+
log.dim(`${t("Next", "\u4E0B\u4E00\u6B65")}: superspec create <feature>`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/commands/lint.ts
|
|
745
|
+
import { existsSync as existsSync11 } from "fs";
|
|
746
|
+
import { join as join10 } from "path";
|
|
747
|
+
|
|
748
|
+
// src/core/lint.ts
|
|
749
|
+
import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync5 } from "fs";
|
|
750
|
+
import { basename, join as join9 } from "path";
|
|
751
|
+
function lintArtifact(filePath, targetLines, hardLines) {
|
|
752
|
+
const artifact = basename(filePath);
|
|
753
|
+
if (!existsSync10(filePath)) {
|
|
754
|
+
return { artifact, lines: 0, status: "ok", message: "not found" };
|
|
755
|
+
}
|
|
756
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
757
|
+
const lines = content.split("\n").length;
|
|
758
|
+
if (lines > hardLines) {
|
|
759
|
+
return {
|
|
760
|
+
artifact,
|
|
761
|
+
lines,
|
|
762
|
+
status: "error",
|
|
763
|
+
message: `${lines} lines exceeds hard limit (${hardLines}). Must split.`
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
if (lines > targetLines) {
|
|
767
|
+
return {
|
|
768
|
+
artifact,
|
|
769
|
+
lines,
|
|
770
|
+
status: "warn",
|
|
771
|
+
message: `${lines} lines exceeds target (${targetLines}). Consider splitting.`
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
return { artifact, lines, status: "ok", message: `${lines} lines` };
|
|
775
|
+
}
|
|
776
|
+
function lintChange(changePath, targetLines, hardLines) {
|
|
777
|
+
if (!existsSync10(changePath)) return [];
|
|
778
|
+
const files = readdirSync6(changePath).filter((f) => f.endsWith(".md"));
|
|
779
|
+
return files.map((f) => lintArtifact(join9(changePath, f), targetLines, hardLines));
|
|
859
780
|
}
|
|
860
781
|
|
|
861
|
-
// src/commands/
|
|
862
|
-
async function
|
|
782
|
+
// src/commands/lint.ts
|
|
783
|
+
async function lintCommand(name) {
|
|
863
784
|
const cwd = process.cwd();
|
|
864
785
|
const config = loadConfig(cwd);
|
|
865
|
-
const changesDir =
|
|
786
|
+
const changesDir = join10(cwd, config.specDir, "changes");
|
|
787
|
+
const { targetLines, hardLines } = config.limits;
|
|
866
788
|
const names = resolveChangeNames(changesDir, name, config.archive.dir);
|
|
867
789
|
if (names.length === 0) {
|
|
868
|
-
log.warn(`${symbol.warn} ${t("no changes to
|
|
790
|
+
log.warn(`${symbol.warn} ${t("no changes to lint", "\u6CA1\u6709\u53EF\u68C0\u67E5\u7684\u53D8\u66F4")}`);
|
|
869
791
|
return;
|
|
870
792
|
}
|
|
871
|
-
let
|
|
793
|
+
let hasIssues = false;
|
|
872
794
|
for (const n of names) {
|
|
873
|
-
const changePath =
|
|
874
|
-
if (!
|
|
795
|
+
const changePath = join10(changesDir, n);
|
|
796
|
+
if (!existsSync11(changePath)) {
|
|
875
797
|
log.warn(`${symbol.warn} "${n}" ${t("not found", "\u672A\u627E\u5230")}`);
|
|
876
798
|
continue;
|
|
877
799
|
}
|
|
878
|
-
const
|
|
800
|
+
const results = lintChange(changePath, targetLines, hardLines);
|
|
879
801
|
log.info(`${symbol.start} ${n}`);
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
} else {
|
|
890
|
-
log.dim(` \u2139 [${issue.artifact}] ${issue.message}`);
|
|
891
|
-
}
|
|
802
|
+
for (const r of results) {
|
|
803
|
+
if (r.status === "error") {
|
|
804
|
+
log.error(` ${symbol.fail} ${r.artifact}: ${r.message}`);
|
|
805
|
+
hasIssues = true;
|
|
806
|
+
} else if (r.status === "warn") {
|
|
807
|
+
log.warn(` ${symbol.warn} ${r.artifact}: ${r.message}`);
|
|
808
|
+
hasIssues = true;
|
|
809
|
+
} else {
|
|
810
|
+
log.success(` ${symbol.ok} ${r.artifact}: ${r.message}`);
|
|
892
811
|
}
|
|
893
812
|
}
|
|
894
813
|
}
|
|
895
|
-
if (
|
|
896
|
-
log.
|
|
897
|
-
} else {
|
|
898
|
-
log.warn(`${symbol.warn} ${totalIssues} ${t("issue(s) found", "\u4E2A\u95EE\u9898")}`);
|
|
814
|
+
if (!hasIssues) {
|
|
815
|
+
log.info(`${symbol.start} ${t("all artifacts within limits", "\u6240\u6709 artifact \u5747\u5728\u9650\u5236\u8303\u56F4\u5185")}`);
|
|
899
816
|
}
|
|
900
817
|
}
|
|
901
818
|
|
|
902
819
|
// src/commands/search.ts
|
|
903
|
-
import { existsSync as
|
|
904
|
-
import {
|
|
820
|
+
import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync6 } from "fs";
|
|
821
|
+
import { basename as basename2, join as join11 } from "path";
|
|
905
822
|
var DEFAULT_LIMIT = 50;
|
|
906
823
|
async function searchCommand(query, options) {
|
|
907
824
|
const cwd = process.cwd();
|
|
908
825
|
const config = loadConfig(cwd);
|
|
909
|
-
const changesDir =
|
|
910
|
-
if (!
|
|
826
|
+
const changesDir = join11(cwd, config.specDir, "changes");
|
|
827
|
+
if (!existsSync12(changesDir)) {
|
|
911
828
|
log.warn(`${symbol.warn} ${t("no changes directory found", "\u672A\u627E\u5230 changes \u76EE\u5F55")}`);
|
|
912
829
|
return;
|
|
913
830
|
}
|
|
914
831
|
const dirs = [];
|
|
915
|
-
const activeEntries = readdirSync7(changesDir, { withFileTypes: true }).filter(
|
|
832
|
+
const activeEntries = readdirSync7(changesDir, { withFileTypes: true }).filter(
|
|
833
|
+
(e) => e.isDirectory() && e.name !== config.archive.dir
|
|
834
|
+
);
|
|
916
835
|
for (const e of activeEntries) {
|
|
917
|
-
dirs.push({ name: e.name, path:
|
|
836
|
+
dirs.push({ name: e.name, path: join11(changesDir, e.name) });
|
|
918
837
|
}
|
|
919
838
|
if (options.archived) {
|
|
920
|
-
const archiveDir =
|
|
921
|
-
if (
|
|
922
|
-
const archivedEntries = readdirSync7(archiveDir, { withFileTypes: true }).filter(
|
|
839
|
+
const archiveDir = join11(changesDir, config.archive.dir);
|
|
840
|
+
if (existsSync12(archiveDir)) {
|
|
841
|
+
const archivedEntries = readdirSync7(archiveDir, { withFileTypes: true }).filter(
|
|
842
|
+
(e) => e.isDirectory()
|
|
843
|
+
);
|
|
923
844
|
for (const e of archivedEntries) {
|
|
924
|
-
dirs.push({ name: `${config.archive.dir}/${e.name}`, path:
|
|
845
|
+
dirs.push({ name: `${config.archive.dir}/${e.name}`, path: join11(archiveDir, e.name) });
|
|
925
846
|
}
|
|
926
847
|
}
|
|
927
848
|
}
|
|
@@ -940,14 +861,14 @@ async function searchCommand(query, options) {
|
|
|
940
861
|
}
|
|
941
862
|
const hits = [];
|
|
942
863
|
for (const dir of dirs) {
|
|
943
|
-
if (!
|
|
864
|
+
if (!existsSync12(dir.path)) continue;
|
|
944
865
|
const files = readdirSync7(dir.path).filter((f) => f.endsWith(".md"));
|
|
945
866
|
for (const file of files) {
|
|
946
867
|
if (options.artifact) {
|
|
947
|
-
const artType =
|
|
868
|
+
const artType = basename2(file, ".md");
|
|
948
869
|
if (artType !== options.artifact) continue;
|
|
949
870
|
}
|
|
950
|
-
const filePath =
|
|
871
|
+
const filePath = join11(dir.path, file);
|
|
951
872
|
const content = readFileSync6(filePath, "utf-8");
|
|
952
873
|
const lines = content.split("\n");
|
|
953
874
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -973,90 +894,20 @@ async function searchCommand(query, options) {
|
|
|
973
894
|
log.dim(` ${hit.change}/${hit.artifact}:${hit.line} ${hit.text}`);
|
|
974
895
|
}
|
|
975
896
|
if (hits.length > limit) {
|
|
976
|
-
log.dim(
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
// src/commands/deps.ts
|
|
981
|
-
import { existsSync as existsSync15, readFileSync as readFileSync7, writeFileSync as writeFileSync4, readdirSync as readdirSync8 } from "fs";
|
|
982
|
-
import { join as join14 } from "path";
|
|
983
|
-
async function depsAddCommand(name, options) {
|
|
984
|
-
const cwd = process.cwd();
|
|
985
|
-
const config = loadConfig(cwd);
|
|
986
|
-
const proposalPath = join14(cwd, config.specDir, "changes", name, "proposal.md");
|
|
987
|
-
if (!existsSync15(proposalPath)) {
|
|
988
|
-
log.warn(`${symbol.warn} "${name}" ${t("not found", "\u672A\u627E\u5230")}`);
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
const content = readFileSync7(proposalPath, "utf-8");
|
|
992
|
-
const updated = addDependency(content, options.on);
|
|
993
|
-
writeFileSync4(proposalPath, updated, "utf-8");
|
|
994
|
-
log.success(`${symbol.ok} ${name} \u2192 depends_on: ${options.on}`);
|
|
995
|
-
}
|
|
996
|
-
async function depsRemoveCommand(name, options) {
|
|
997
|
-
const cwd = process.cwd();
|
|
998
|
-
const config = loadConfig(cwd);
|
|
999
|
-
const proposalPath = join14(cwd, config.specDir, "changes", name, "proposal.md");
|
|
1000
|
-
if (!existsSync15(proposalPath)) {
|
|
1001
|
-
log.warn(`${symbol.warn} "${name}" ${t("not found", "\u672A\u627E\u5230")}`);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
const content = readFileSync7(proposalPath, "utf-8");
|
|
1005
|
-
const updated = removeDependency(content, options.on);
|
|
1006
|
-
writeFileSync4(proposalPath, updated, "utf-8");
|
|
1007
|
-
log.success(`${symbol.ok} ${name} \u2192 ${t("removed dependency", "\u79FB\u9664\u4F9D\u8D56")}: ${options.on}`);
|
|
1008
|
-
}
|
|
1009
|
-
async function depsListCommand(name) {
|
|
1010
|
-
const cwd = process.cwd();
|
|
1011
|
-
const config = loadConfig(cwd);
|
|
1012
|
-
const changesDir = join14(cwd, config.specDir, "changes");
|
|
1013
|
-
if (name) {
|
|
1014
|
-
const proposalPath = join14(changesDir, name, "proposal.md");
|
|
1015
|
-
if (!existsSync15(proposalPath)) {
|
|
1016
|
-
log.warn(`${symbol.warn} "${name}" ${t("not found", "\u672A\u627E\u5230")}`);
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
const content = readFileSync7(proposalPath, "utf-8");
|
|
1020
|
-
const { meta } = parseFrontmatter(content);
|
|
1021
|
-
const deps2 = Array.isArray(meta.depends_on) ? meta.depends_on : [];
|
|
1022
|
-
log.info(`${symbol.start} ${name}`);
|
|
1023
|
-
if (deps2.length === 0) {
|
|
1024
|
-
log.dim(` ${t("no dependencies", "\u65E0\u4F9D\u8D56")}`);
|
|
1025
|
-
} else {
|
|
1026
|
-
for (const d of deps2) {
|
|
1027
|
-
log.dim(` \u2192 ${d}`);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
if (!existsSync15(changesDir)) {
|
|
1033
|
-
log.warn(`${symbol.warn} ${t("no changes directory", "\u672A\u627E\u5230 changes \u76EE\u5F55")}`);
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
const entries = readdirSync8(changesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== config.archive.dir);
|
|
1037
|
-
log.info(`${symbol.start} ${t("dependency graph", "\u4F9D\u8D56\u5173\u7CFB")}`);
|
|
1038
|
-
for (const entry of entries) {
|
|
1039
|
-
const proposalPath = join14(changesDir, entry.name, "proposal.md");
|
|
1040
|
-
if (!existsSync15(proposalPath)) continue;
|
|
1041
|
-
const content = readFileSync7(proposalPath, "utf-8");
|
|
1042
|
-
const { meta } = parseFrontmatter(content);
|
|
1043
|
-
const deps2 = Array.isArray(meta.depends_on) ? meta.depends_on : [];
|
|
1044
|
-
if (deps2.length > 0) {
|
|
1045
|
-
log.dim(` ${entry.name} \u2192 [${deps2.join(", ")}]`);
|
|
1046
|
-
} else {
|
|
1047
|
-
log.dim(` ${entry.name}`);
|
|
1048
|
-
}
|
|
897
|
+
log.dim(
|
|
898
|
+
` ... ${hits.length - limit} ${t("more result(s), use --limit to show more", "\u6761\u66F4\u591A\u7ED3\u679C\uFF0C\u4F7F\u7528 --limit \u663E\u793A\u66F4\u591A")}`
|
|
899
|
+
);
|
|
1049
900
|
}
|
|
1050
901
|
}
|
|
1051
902
|
|
|
1052
903
|
// src/commands/status.ts
|
|
1053
|
-
import { existsSync as
|
|
1054
|
-
import { join as
|
|
904
|
+
import { existsSync as existsSync13, readdirSync as readdirSync8, readFileSync as readFileSync7 } from "fs";
|
|
905
|
+
import { join as join12 } from "path";
|
|
1055
906
|
var ARTIFACT_TYPES = ["proposal", "spec", "tasks", "clarify", "checklist"];
|
|
1056
907
|
function readStatus(changePath, artifact) {
|
|
1057
|
-
const filePath =
|
|
1058
|
-
if (!
|
|
1059
|
-
const content =
|
|
908
|
+
const filePath = join12(changePath, `${artifact}.md`);
|
|
909
|
+
if (!existsSync13(filePath)) return "\u2014";
|
|
910
|
+
const content = readFileSync7(filePath, "utf-8");
|
|
1060
911
|
const { meta } = parseFrontmatter(content);
|
|
1061
912
|
if (meta.status) return meta.status;
|
|
1062
913
|
if (content.includes("\u2705")) return "done";
|
|
@@ -1079,12 +930,12 @@ function overallStatus(statuses) {
|
|
|
1079
930
|
async function statusCommand() {
|
|
1080
931
|
const cwd = process.cwd();
|
|
1081
932
|
const config = loadConfig(cwd);
|
|
1082
|
-
const changesDir =
|
|
1083
|
-
if (!
|
|
933
|
+
const changesDir = join12(cwd, config.specDir, "changes");
|
|
934
|
+
if (!existsSync13(changesDir)) {
|
|
1084
935
|
log.warn(`${symbol.warn} ${t("no changes directory found", "\u672A\u627E\u5230 changes \u76EE\u5F55")}`);
|
|
1085
936
|
return;
|
|
1086
937
|
}
|
|
1087
|
-
const entries =
|
|
938
|
+
const entries = readdirSync8(changesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== config.archive.dir).sort((a, b) => a.name.localeCompare(b.name));
|
|
1088
939
|
if (entries.length === 0) {
|
|
1089
940
|
log.dim(` ${t("no active changes", "\u65E0\u6D3B\u8DC3\u53D8\u66F4")}`);
|
|
1090
941
|
return;
|
|
@@ -1092,7 +943,7 @@ async function statusCommand() {
|
|
|
1092
943
|
const header = ["Change", ...ARTIFACT_TYPES.map((a) => a.slice(0, 8).padEnd(8)), "Status"];
|
|
1093
944
|
const rows = [];
|
|
1094
945
|
for (const entry of entries) {
|
|
1095
|
-
const changePath =
|
|
946
|
+
const changePath = join12(changesDir, entry.name);
|
|
1096
947
|
const statuses = {};
|
|
1097
948
|
for (const art of ARTIFACT_TYPES) {
|
|
1098
949
|
statuses[art] = readStatus(changePath, art);
|
|
@@ -1115,9 +966,11 @@ async function statusCommand() {
|
|
|
1115
966
|
for (const row of rows) {
|
|
1116
967
|
console.log(formatRow(row));
|
|
1117
968
|
}
|
|
1118
|
-
const archiveDir =
|
|
1119
|
-
if (
|
|
1120
|
-
const archived =
|
|
969
|
+
const archiveDir = join12(changesDir, config.archive.dir);
|
|
970
|
+
if (existsSync13(archiveDir)) {
|
|
971
|
+
const archived = readdirSync8(archiveDir, { withFileTypes: true }).filter(
|
|
972
|
+
(e) => e.isDirectory()
|
|
973
|
+
);
|
|
1121
974
|
if (archived.length > 0) {
|
|
1122
975
|
log.dim(`
|
|
1123
976
|
${archived.length} ${t("archived change(s)", "\u4E2A\u5DF2\u5F52\u6863\u53D8\u66F4")}`);
|
|
@@ -1127,16 +980,18 @@ async function statusCommand() {
|
|
|
1127
980
|
async function listCommand(options) {
|
|
1128
981
|
const cwd = process.cwd();
|
|
1129
982
|
const config = loadConfig(cwd);
|
|
1130
|
-
const changesDir =
|
|
1131
|
-
if (!
|
|
1132
|
-
const entries =
|
|
983
|
+
const changesDir = join12(cwd, config.specDir, "changes");
|
|
984
|
+
if (!existsSync13(changesDir)) return;
|
|
985
|
+
const entries = readdirSync8(changesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== config.archive.dir).sort((a, b) => a.name.localeCompare(b.name));
|
|
1133
986
|
for (const e of entries) {
|
|
1134
987
|
console.log(e.name);
|
|
1135
988
|
}
|
|
1136
989
|
if (options.archived) {
|
|
1137
|
-
const archiveDir =
|
|
1138
|
-
if (
|
|
1139
|
-
const archived =
|
|
990
|
+
const archiveDir = join12(changesDir, config.archive.dir);
|
|
991
|
+
if (existsSync13(archiveDir)) {
|
|
992
|
+
const archived = readdirSync8(archiveDir, { withFileTypes: true }).filter(
|
|
993
|
+
(e) => e.isDirectory()
|
|
994
|
+
);
|
|
1140
995
|
for (const e of archived) {
|
|
1141
996
|
console.log(`${config.archive.dir}/${e.name}`);
|
|
1142
997
|
}
|
|
@@ -1144,18 +999,18 @@ async function listCommand(options) {
|
|
|
1144
999
|
}
|
|
1145
1000
|
}
|
|
1146
1001
|
function stripAnsi(str) {
|
|
1147
|
-
return str.replace(/\u001b\[\d+(?:;\d+)*m/g, "").replace(/[✅🟢🟡—]/
|
|
1002
|
+
return str.replace(/\u001b\[\d+(?:;\d+)*m/g, "").replace(/[✅🟢🟡—]/gu, "XX");
|
|
1148
1003
|
}
|
|
1149
1004
|
|
|
1150
1005
|
// src/commands/sync.ts
|
|
1151
|
-
import { existsSync as
|
|
1152
|
-
import { join as
|
|
1006
|
+
import { existsSync as existsSync15, writeFileSync as writeFileSync5 } from "fs";
|
|
1007
|
+
import { join as join14 } from "path";
|
|
1153
1008
|
|
|
1154
1009
|
// src/core/context.ts
|
|
1155
|
-
import {
|
|
1156
|
-
import { join as
|
|
1010
|
+
import { existsSync as existsSync14, readFileSync as readFileSync8 } from "fs";
|
|
1011
|
+
import { join as join13 } from "path";
|
|
1157
1012
|
function extractSection(body, heading) {
|
|
1158
|
-
const regex = new RegExp(`^##\\s
|
|
1013
|
+
const regex = new RegExp(`^##\\s+(?:${heading})[\\s\\S]*?$`, "im");
|
|
1159
1014
|
const match = body.match(regex);
|
|
1160
1015
|
if (!match) return [];
|
|
1161
1016
|
const startIdx = body.indexOf(match[0]) + match[0].length;
|
|
@@ -1189,12 +1044,13 @@ function parseTaskItems(body) {
|
|
|
1189
1044
|
function extractFilePaths(body) {
|
|
1190
1045
|
const paths = /* @__PURE__ */ new Set();
|
|
1191
1046
|
const regex = /`([^`]+\.[a-zA-Z]+)`/g;
|
|
1192
|
-
let match;
|
|
1193
|
-
while (
|
|
1047
|
+
let match = regex.exec(body);
|
|
1048
|
+
while (match !== null) {
|
|
1194
1049
|
const p = match[1];
|
|
1195
1050
|
if (p.includes("/") && !p.startsWith("http") && !p.includes(" ")) {
|
|
1196
1051
|
paths.add(p);
|
|
1197
1052
|
}
|
|
1053
|
+
match = regex.exec(body);
|
|
1198
1054
|
}
|
|
1199
1055
|
return [...paths];
|
|
1200
1056
|
}
|
|
@@ -1231,21 +1087,17 @@ function classifyGitChanges(gitChanges, taskFiles) {
|
|
|
1231
1087
|
}
|
|
1232
1088
|
function generateContext(changePath, changeName, options = {}) {
|
|
1233
1089
|
const read = (name) => {
|
|
1234
|
-
const p =
|
|
1235
|
-
return
|
|
1090
|
+
const p = join13(changePath, name);
|
|
1091
|
+
return existsSync14(p) ? readFileSync8(p, "utf-8") : null;
|
|
1236
1092
|
};
|
|
1237
1093
|
const proposal = read("proposal.md");
|
|
1238
1094
|
const spec = read("spec.md");
|
|
1239
1095
|
const tasks = read("tasks.md");
|
|
1240
1096
|
const clarify = read("clarify.md");
|
|
1241
|
-
let strategy = "follow";
|
|
1242
|
-
let status = "in-progress";
|
|
1243
1097
|
const mode = spec ? "boost" : "standard";
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
if (meta.status) status = meta.status;
|
|
1248
|
-
}
|
|
1098
|
+
const proposalMeta = proposal ? parseFrontmatter(proposal).meta : {};
|
|
1099
|
+
const strategy = proposalMeta.strategy || "follow";
|
|
1100
|
+
const status = proposalMeta.status || "in-progress";
|
|
1249
1101
|
const goals = [];
|
|
1250
1102
|
if (proposal) {
|
|
1251
1103
|
const { body } = parseFrontmatter(proposal);
|
|
@@ -1276,13 +1128,15 @@ function generateContext(changePath, changeName, options = {}) {
|
|
|
1276
1128
|
notes = notesSection.join("\n");
|
|
1277
1129
|
}
|
|
1278
1130
|
}
|
|
1279
|
-
const
|
|
1131
|
+
const fmData = {
|
|
1280
1132
|
name: changeName,
|
|
1281
1133
|
status,
|
|
1282
1134
|
strategy,
|
|
1283
1135
|
mode,
|
|
1284
1136
|
updated: getDateString()
|
|
1285
|
-
}
|
|
1137
|
+
};
|
|
1138
|
+
if (proposalMeta.input) fmData.input = proposalMeta.input;
|
|
1139
|
+
const fm = serializeFrontmatter(fmData);
|
|
1286
1140
|
const lines = [fm, ""];
|
|
1287
1141
|
if (goals.length > 0) {
|
|
1288
1142
|
lines.push("## Goals", ...goals, "");
|
|
@@ -1320,7 +1174,7 @@ function generateContext(changePath, changeName, options = {}) {
|
|
|
1320
1174
|
async function syncCommand(name, opts) {
|
|
1321
1175
|
const cwd = process.cwd();
|
|
1322
1176
|
const config = loadConfig(cwd);
|
|
1323
|
-
const changesDir =
|
|
1177
|
+
const changesDir = join14(cwd, config.specDir, "changes");
|
|
1324
1178
|
const names = resolveChangeNames(changesDir, name, config.archive.dir);
|
|
1325
1179
|
if (names.length === 0) {
|
|
1326
1180
|
log.warn(`${symbol.warn} ${t("no changes found", "\u672A\u627E\u5230\u53D8\u66F4")}`);
|
|
@@ -1328,8 +1182,8 @@ async function syncCommand(name, opts) {
|
|
|
1328
1182
|
}
|
|
1329
1183
|
const useGit = opts.git !== false;
|
|
1330
1184
|
for (const n of names) {
|
|
1331
|
-
const changePath =
|
|
1332
|
-
if (!
|
|
1185
|
+
const changePath = join14(changesDir, n);
|
|
1186
|
+
if (!existsSync15(changePath)) {
|
|
1333
1187
|
log.warn(`${symbol.warn} "${n}" ${t("not found", "\u672A\u627E\u5230")}`);
|
|
1334
1188
|
continue;
|
|
1335
1189
|
}
|
|
@@ -1337,30 +1191,256 @@ async function syncCommand(name, opts) {
|
|
|
1337
1191
|
gitDiff: useGit,
|
|
1338
1192
|
baseBranch: opts.base
|
|
1339
1193
|
});
|
|
1340
|
-
const destPath =
|
|
1194
|
+
const destPath = join14(changePath, "context.md");
|
|
1341
1195
|
writeFileSync5(destPath, content, "utf-8");
|
|
1342
1196
|
log.success(`${symbol.ok} ${t("synced", "\u5DF2\u540C\u6B65")} ${n}/context.md`);
|
|
1343
1197
|
}
|
|
1344
1198
|
}
|
|
1345
1199
|
|
|
1200
|
+
// src/commands/update.ts
|
|
1201
|
+
import { existsSync as existsSync16 } from "fs";
|
|
1202
|
+
import { join as join15 } from "path";
|
|
1203
|
+
async function updateCommand() {
|
|
1204
|
+
const cwd = process.cwd();
|
|
1205
|
+
const config = loadConfig(cwd);
|
|
1206
|
+
const specDir = join15(cwd, config.specDir);
|
|
1207
|
+
const lang = config.lang || "zh";
|
|
1208
|
+
if (!existsSync16(join15(cwd, "superspec.config.json"))) {
|
|
1209
|
+
log.warn(
|
|
1210
|
+
`${symbol.warn} ${t("not initialized, run superspec init first", "\u5F53\u524D\u76EE\u5F55\u672A\u521D\u59CB\u5316 SuperSpec\uFF0C\u8BF7\u5148\u8FD0\u884C superspec init")}`
|
|
1211
|
+
);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
log.info(`${symbol.start} ${t("updating SuperSpec...", "\u66F4\u65B0 SuperSpec...")}`);
|
|
1215
|
+
const templateNames = Object.values(config.templates).map(
|
|
1216
|
+
(v) => v.endsWith(".md") ? v : `${v}.md`
|
|
1217
|
+
);
|
|
1218
|
+
ensureDir(join15(specDir, "templates"));
|
|
1219
|
+
for (const tpl of templateNames) {
|
|
1220
|
+
try {
|
|
1221
|
+
copyTemplate(tpl, join15(specDir, "templates", tpl), lang);
|
|
1222
|
+
} catch {
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
log.success(` ${symbol.ok} ${t("templates updated", "\u6A21\u677F\u66F4\u65B0")} (${lang})`);
|
|
1226
|
+
installAgentsMd(cwd);
|
|
1227
|
+
const aiEditor = config.aiEditor;
|
|
1228
|
+
if (aiEditor && AI_EDITORS[aiEditor]) {
|
|
1229
|
+
installRules(cwd, aiEditor);
|
|
1230
|
+
installCommands(cwd, aiEditor, lang);
|
|
1231
|
+
}
|
|
1232
|
+
log.info(`${symbol.start} ${t("update done!", "\u66F4\u65B0\u5B8C\u6210\uFF01")}`);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/commands/validate.ts
|
|
1236
|
+
import { existsSync as existsSync18 } from "fs";
|
|
1237
|
+
import { join as join17 } from "path";
|
|
1238
|
+
|
|
1239
|
+
// src/core/validate.ts
|
|
1240
|
+
import { existsSync as existsSync17, readFileSync as readFileSync9 } from "fs";
|
|
1241
|
+
import { join as join16 } from "path";
|
|
1242
|
+
function extractIds(content, pattern) {
|
|
1243
|
+
const matches = content.match(pattern);
|
|
1244
|
+
return matches ? [...new Set(matches)] : [];
|
|
1245
|
+
}
|
|
1246
|
+
function validateChange(changePath, checkDeps = false) {
|
|
1247
|
+
const issues = [];
|
|
1248
|
+
const read = (name) => {
|
|
1249
|
+
const p = join16(changePath, name);
|
|
1250
|
+
return existsSync17(p) ? readFileSync9(p, "utf-8") : null;
|
|
1251
|
+
};
|
|
1252
|
+
const proposal = read("proposal.md");
|
|
1253
|
+
const spec = read("spec.md");
|
|
1254
|
+
const tasks = read("tasks.md");
|
|
1255
|
+
if (!proposal) issues.push({ level: "warn", artifact: "proposal.md", message: "missing" });
|
|
1256
|
+
if (!tasks) issues.push({ level: "warn", artifact: "tasks.md", message: "missing" });
|
|
1257
|
+
if (proposal && spec) {
|
|
1258
|
+
const proposalGoals = extractIds(proposal, /US-\d+/g);
|
|
1259
|
+
const specUS = extractIds(spec, /US-\d+/g);
|
|
1260
|
+
for (const id of proposalGoals) {
|
|
1261
|
+
if (!specUS.includes(id)) {
|
|
1262
|
+
issues.push({
|
|
1263
|
+
level: "error",
|
|
1264
|
+
artifact: "spec.md",
|
|
1265
|
+
message: `${id} from proposal not found in spec`
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (spec && tasks) {
|
|
1271
|
+
const specFR = extractIds(spec, /FR-\d+/g);
|
|
1272
|
+
const tasksFR = extractIds(tasks, /FR-\d+/g);
|
|
1273
|
+
for (const id of specFR) {
|
|
1274
|
+
if (!tasksFR.includes(id)) {
|
|
1275
|
+
issues.push({
|
|
1276
|
+
level: "warn",
|
|
1277
|
+
artifact: "tasks.md",
|
|
1278
|
+
message: `${id} from spec not referenced in tasks`
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
for (const id of tasksFR) {
|
|
1283
|
+
if (!specFR.includes(id)) {
|
|
1284
|
+
issues.push({
|
|
1285
|
+
level: "error",
|
|
1286
|
+
artifact: "tasks.md",
|
|
1287
|
+
message: `${id} referenced but not defined in spec`
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (spec) {
|
|
1293
|
+
const acIds = extractIds(spec, /AC-\d+\.\d+/g);
|
|
1294
|
+
const frIds = extractIds(spec, /FR-\d+/g);
|
|
1295
|
+
for (const ac of acIds) {
|
|
1296
|
+
const frNum = ac.replace("AC-", "").split(".")[0];
|
|
1297
|
+
const parentFR = `FR-${frNum}`;
|
|
1298
|
+
if (!frIds.includes(parentFR)) {
|
|
1299
|
+
issues.push({
|
|
1300
|
+
level: "warn",
|
|
1301
|
+
artifact: "spec.md",
|
|
1302
|
+
message: `${ac} has no parent ${parentFR}`
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (checkDeps && proposal) {
|
|
1308
|
+
const { meta, body } = parseFrontmatter(proposal);
|
|
1309
|
+
const fmDeps = Array.isArray(meta.depends_on) ? meta.depends_on : [];
|
|
1310
|
+
const contentRefs = extractIds(body, /depends[_ ]on[:\s]+(\S+)/gi);
|
|
1311
|
+
const mentioned = contentRefs.map((m) => m.replace(/depends[_ ]on[:\s]+/i, ""));
|
|
1312
|
+
for (const dep of mentioned) {
|
|
1313
|
+
if (!fmDeps.includes(dep)) {
|
|
1314
|
+
issues.push({
|
|
1315
|
+
level: "warn",
|
|
1316
|
+
artifact: "proposal.md",
|
|
1317
|
+
message: `content mentions "${dep}" but not in frontmatter depends_on`
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const changesDir = join16(changePath, "..");
|
|
1322
|
+
for (const dep of fmDeps) {
|
|
1323
|
+
if (!existsSync17(join16(changesDir, dep))) {
|
|
1324
|
+
issues.push({
|
|
1325
|
+
level: "error",
|
|
1326
|
+
artifact: "proposal.md",
|
|
1327
|
+
message: `depends_on "${dep}" not found in changes`
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return issues;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/commands/validate.ts
|
|
1336
|
+
async function validateCommand(name, options) {
|
|
1337
|
+
const cwd = process.cwd();
|
|
1338
|
+
const config = loadConfig(cwd);
|
|
1339
|
+
const changesDir = join17(cwd, config.specDir, "changes");
|
|
1340
|
+
const names = resolveChangeNames(changesDir, name, config.archive.dir);
|
|
1341
|
+
if (names.length === 0) {
|
|
1342
|
+
log.warn(`${symbol.warn} ${t("no changes to validate", "\u6CA1\u6709\u53EF\u9A8C\u8BC1\u7684\u53D8\u66F4")}`);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
let totalIssues = 0;
|
|
1346
|
+
for (const n of names) {
|
|
1347
|
+
const changePath = join17(changesDir, n);
|
|
1348
|
+
if (!existsSync18(changePath)) {
|
|
1349
|
+
log.warn(`${symbol.warn} "${n}" ${t("not found", "\u672A\u627E\u5230")}`);
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
const issues = validateChange(changePath, options.checkDeps);
|
|
1353
|
+
log.info(`${symbol.start} ${n}`);
|
|
1354
|
+
if (issues.length === 0) {
|
|
1355
|
+
log.success(` ${symbol.ok} ${t("all checks passed", "\u6240\u6709\u68C0\u67E5\u901A\u8FC7")}`);
|
|
1356
|
+
} else {
|
|
1357
|
+
for (const issue of issues) {
|
|
1358
|
+
totalIssues++;
|
|
1359
|
+
if (issue.level === "error") {
|
|
1360
|
+
log.error(` ${symbol.fail} [${issue.artifact}] ${issue.message}`);
|
|
1361
|
+
} else if (issue.level === "warn") {
|
|
1362
|
+
log.warn(` ${symbol.warn} [${issue.artifact}] ${issue.message}`);
|
|
1363
|
+
} else {
|
|
1364
|
+
log.dim(` \u2139 [${issue.artifact}] ${issue.message}`);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (totalIssues === 0) {
|
|
1370
|
+
log.success(`${symbol.ok} ${t("all validations passed", "\u6240\u6709\u9A8C\u8BC1\u901A\u8FC7")}`);
|
|
1371
|
+
} else {
|
|
1372
|
+
log.warn(`${symbol.warn} ${totalIssues} ${t("issue(s) found", "\u4E2A\u95EE\u9898")}`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1346
1376
|
// src/cli/index.ts
|
|
1347
1377
|
var require2 = createRequire(import.meta.url);
|
|
1348
1378
|
var pkg = require2("../../package.json");
|
|
1349
1379
|
setLang(loadConfig(process.cwd(), true).lang);
|
|
1350
|
-
program.name("superspec").description(
|
|
1351
|
-
|
|
1352
|
-
|
|
1380
|
+
program.name("superspec").description(
|
|
1381
|
+
t("Spec-driven development for AI coding assistants", "AI \u7F16\u7801\u52A9\u624B\u7684\u89C4\u683C\u9A71\u52A8\u5F00\u53D1\u5DE5\u5177")
|
|
1382
|
+
).version(pkg.version);
|
|
1383
|
+
program.command("init").description(t("Initialize SuperSpec in current project", "\u521D\u59CB\u5316 SuperSpec \u5230\u5F53\u524D\u9879\u76EE")).option(
|
|
1384
|
+
"--ai <agent>",
|
|
1385
|
+
t(
|
|
1386
|
+
"AI assistant type: claude, cursor, qwen, opencode, codex, codebuddy, qoder",
|
|
1387
|
+
"AI \u52A9\u624B\u7C7B\u578B: claude, cursor, qwen, opencode, codex, codebuddy, qoder"
|
|
1388
|
+
),
|
|
1389
|
+
"cursor"
|
|
1390
|
+
).option("--lang <lang>", t("Template language: zh, en", "\u6A21\u677F\u8BED\u8A00: zh, en"), "en").option("--force", t("Force overwrite existing config", "\u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E")).option("--no-git", t("Skip git initialization", "\u8DF3\u8FC7 git \u521D\u59CB\u5316")).action(initCommand);
|
|
1391
|
+
program.command("create <feature>").description(
|
|
1392
|
+
t(
|
|
1393
|
+
"Create change and generate proposal (-b boost mode)",
|
|
1394
|
+
"\u521B\u5EFA\u53D8\u66F4\u5E76\u751F\u6210 proposal\uFF08-b \u589E\u5F3A\u6A21\u5F0F\uFF09"
|
|
1395
|
+
)
|
|
1396
|
+
).option(
|
|
1397
|
+
"-b, --boost",
|
|
1398
|
+
t(
|
|
1399
|
+
"Boost mode, also generate spec + design + checklist",
|
|
1400
|
+
"\u589E\u5F3A\u6A21\u5F0F\uFF0C\u989D\u5916\u751F\u6210 spec + design + checklist"
|
|
1401
|
+
)
|
|
1402
|
+
).option(
|
|
1403
|
+
"-c, --creative",
|
|
1404
|
+
t("Creative mode, encourage new approaches", "\u521B\u9020\u6A21\u5F0F\uFF0C\u9F13\u52B1\u63A2\u7D22\u65B0\u65B9\u6848")
|
|
1405
|
+
).option(
|
|
1406
|
+
"-d, --description <desc>",
|
|
1407
|
+
t("Change description for context", "\u53D8\u66F4\u63CF\u8FF0\uFF0C\u7528\u4E8E\u751F\u6210\u4E0A\u4E0B\u6587")
|
|
1408
|
+
).option("--spec-dir <dir>", t("Custom spec folder name", "\u81EA\u5B9A\u4E49 spec \u6587\u4EF6\u5939\u540D\u79F0")).option("--no-branch", t("Skip git branch creation", "\u4E0D\u521B\u5EFA git \u5206\u652F")).option(
|
|
1409
|
+
"--intent-type <type>",
|
|
1410
|
+
t(
|
|
1411
|
+
"Intent type: feature, hotfix, bugfix, refactor, chore",
|
|
1412
|
+
"\u610F\u56FE\u7C7B\u578B\uFF1Afeature, hotfix, bugfix, refactor, chore"
|
|
1413
|
+
)
|
|
1414
|
+
).option("--branch-prefix <prefix>", t("Custom branch prefix", "\u81EA\u5B9A\u4E49\u5206\u652F\u524D\u7F00")).option(
|
|
1415
|
+
"--branch-template <template>",
|
|
1416
|
+
t(
|
|
1417
|
+
"Branch name template: {prefix}{date}-{feature}-{user}",
|
|
1418
|
+
"\u5206\u652F\u540D\u79F0\u6A21\u677F\uFF1A{prefix}-{date}-{feature}-{user}"
|
|
1419
|
+
)
|
|
1420
|
+
).option(
|
|
1421
|
+
"--change-name-template <template>",
|
|
1422
|
+
t("Folder name template: {date}-{feature}-{user}", "\u6587\u4EF6\u5939\u540D\u79F0\u6A21\u677F\uFF1A{date}-{feature}-{user}")
|
|
1423
|
+
).option("--user <user>", t("Developer identifier (e.g. jay)", "\u5F00\u53D1\u8005\u6807\u8BC6\uFF08\u5982 jay\uFF09")).option("--lang <lang>", t("SDD document language: zh, en", "SDD \u6587\u6863\u8BED\u8A00: zh, en")).action(createCommand);
|
|
1353
1424
|
program.command("archive [name]").description(t("Archive completed changes", "\u5F52\u6863\u5DF2\u5B8C\u6210\u7684\u53D8\u66F4")).option("--all", t("Archive all completed changes", "\u5F52\u6863\u6240\u6709\u5DF2\u5B8C\u6210\u7684\u53D8\u66F4")).action(archiveCommand);
|
|
1354
1425
|
program.command("update").description(t("Refresh agent instructions and templates", "\u5237\u65B0 agent \u6307\u4EE4\u548C\u6A21\u677F")).action(updateCommand);
|
|
1355
1426
|
program.command("lint [name]").description(t("Check artifact line limits", "\u68C0\u67E5 artifact \u884C\u6570\u662F\u5426\u8D85\u9650")).action(lintCommand);
|
|
1356
1427
|
program.command("validate [name]").description(t("Cross-validate artifact consistency", "\u4EA4\u53C9\u9A8C\u8BC1 artifact \u4E00\u81F4\u6027")).option("--check-deps", t("Also check dependency consistency", "\u540C\u65F6\u68C0\u67E5\u4F9D\u8D56\u4E00\u81F4\u6027")).action(validateCommand);
|
|
1357
|
-
program.command("search <query>").description(t("Search change contents", "\u641C\u7D22\u53D8\u66F4\u5185\u5BB9")).option("--archived", t("Include archived changes", "\u5305\u542B\u5DF2\u5F52\u6863\u7684\u53D8\u66F4")).option(
|
|
1428
|
+
program.command("search <query>").description(t("Search change contents", "\u641C\u7D22\u53D8\u66F4\u5185\u5BB9")).option("--archived", t("Include archived changes", "\u5305\u542B\u5DF2\u5F52\u6863\u7684\u53D8\u66F4")).option(
|
|
1429
|
+
"--artifact <type>",
|
|
1430
|
+
t(
|
|
1431
|
+
"Filter by artifact type (proposal/spec/tasks/clarify/checklist)",
|
|
1432
|
+
"\u6309 artifact \u7C7B\u578B\u8FC7\u6EE4 (proposal/spec/tasks/clarify/checklist)"
|
|
1433
|
+
)
|
|
1434
|
+
).option("--limit <n>", t("Max results to show (default: 50)", "\u6700\u5927\u663E\u793A\u7ED3\u679C\u6570\uFF08\u9ED8\u8BA4 50\uFF09")).option("-E, --regex", t("Use regex pattern matching", "\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D")).action(searchCommand);
|
|
1358
1435
|
var deps = program.command("deps").description(t("Manage spec dependencies", "\u7BA1\u7406 spec \u4F9D\u8D56"));
|
|
1359
1436
|
deps.command("list [name]").description(t("View dependency graph", "\u67E5\u770B\u4F9D\u8D56\u5173\u7CFB")).action(depsListCommand);
|
|
1360
1437
|
deps.command("add <name>").description(t("Add spec dependency", "\u6DFB\u52A0 spec \u4F9D\u8D56")).requiredOption("--on <other>", t("Dependency spec name", "\u4F9D\u8D56\u7684 spec \u540D\u79F0")).action(depsAddCommand);
|
|
1361
1438
|
deps.command("remove <name>").description(t("Remove spec dependency", "\u79FB\u9664 spec \u4F9D\u8D56")).requiredOption("--on <other>", t("Dependency to remove", "\u8981\u79FB\u9664\u7684\u4F9D\u8D56\u540D\u79F0")).action(depsRemoveCommand);
|
|
1362
1439
|
program.command("status").description(t("View all change statuses", "\u67E5\u770B\u6240\u6709\u53D8\u66F4\u72B6\u6001")).action(statusCommand);
|
|
1363
1440
|
program.command("list").description(t("List change names (for scripting)", "\u5217\u51FA\u53D8\u66F4\u540D\u79F0\uFF08\u7528\u4E8E\u811A\u672C\uFF09")).option("--archived", t("Include archived changes", "\u5305\u542B\u5DF2\u5F52\u6863\u7684\u53D8\u66F4")).action(listCommand);
|
|
1364
|
-
program.command("sync [name]").description(t("Sync git changes to context.md", "\u540C\u6B65 git \u53D8\u66F4\u5230 context.md")).option(
|
|
1441
|
+
program.command("sync [name]").description(t("Sync git changes to context.md", "\u540C\u6B65 git \u53D8\u66F4\u5230 context.md")).option(
|
|
1442
|
+
"--base <branch>",
|
|
1443
|
+
t("Base branch (default: main/master)", "\u57FA\u51C6\u5206\u652F\uFF08\u9ED8\u8BA4 main/master\uFF09")
|
|
1444
|
+
).option("--no-git", t("Skip git diff collection", "\u4E0D\u6536\u96C6 git diff \u4FE1\u606F")).action(syncCommand);
|
|
1365
1445
|
program.parse();
|
|
1366
1446
|
//# sourceMappingURL=index.js.map
|