@oevery/rsp 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -97
- package/README.zh-CN.md +105 -94
- package/dist/cli.mjs +808 -254
- package/package.json +1 -1
- package/rules/rsp-rules.md +67 -92
package/dist/cli.mjs
CHANGED
|
@@ -5,10 +5,10 @@ import { resolve } from "path";
|
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
6
|
import { defineCommand, runMain } from "citty";
|
|
7
7
|
|
|
8
|
-
// src/commands/
|
|
8
|
+
// src/commands/add-rules.ts
|
|
9
9
|
import { existsSync as existsSync3 } from "fs";
|
|
10
|
-
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
11
|
-
import { basename, dirname as dirname2, join as join3
|
|
10
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
11
|
+
import { basename, dirname as dirname2, join as join3 } from "path";
|
|
12
12
|
|
|
13
13
|
// src/core/config.ts
|
|
14
14
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -21,6 +21,8 @@ import pc from "picocolors";
|
|
|
21
21
|
import { existsSync } from "fs";
|
|
22
22
|
import { readdir, stat } from "fs/promises";
|
|
23
23
|
import { join, relative } from "path";
|
|
24
|
+
var RSP_AGENTS_BEGIN = "<!-- rsp:begin -->";
|
|
25
|
+
var RSP_AGENTS_END = "<!-- rsp:end -->";
|
|
24
26
|
function parseYamlLines(lines) {
|
|
25
27
|
const result = {};
|
|
26
28
|
let currentKey = null;
|
|
@@ -59,7 +61,7 @@ function parseYamlLines(lines) {
|
|
|
59
61
|
return result;
|
|
60
62
|
}
|
|
61
63
|
function parseFrontmatter(content) {
|
|
62
|
-
const m = content.match(/^---\n([\s\S]*?)\n(?:---|\.\.\.)\n/);
|
|
64
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)\r?\n/);
|
|
63
65
|
if (!m)
|
|
64
66
|
return null;
|
|
65
67
|
return parseYamlLines(m[1].split("\n"));
|
|
@@ -76,6 +78,8 @@ async function walkMarkdownFiles(dir) {
|
|
|
76
78
|
try {
|
|
77
79
|
const items = await readdir(dir, { withFileTypes: true });
|
|
78
80
|
for (const item of items) {
|
|
81
|
+
if (item.name.startsWith("."))
|
|
82
|
+
continue;
|
|
79
83
|
const filePath = join(dir, item.name);
|
|
80
84
|
if (item.isDirectory())
|
|
81
85
|
files.push(...await walkMarkdownFiles(filePath));
|
|
@@ -91,6 +95,8 @@ async function walkFiles(dir) {
|
|
|
91
95
|
try {
|
|
92
96
|
const items = await readdir(dir, { withFileTypes: true });
|
|
93
97
|
for (const item of items) {
|
|
98
|
+
if (item.name.startsWith("."))
|
|
99
|
+
continue;
|
|
94
100
|
const filePath = join(dir, item.name);
|
|
95
101
|
if (item.isDirectory())
|
|
96
102
|
files.push(...await walkFiles(filePath));
|
|
@@ -134,6 +140,112 @@ tags:
|
|
|
134
140
|
-
|
|
135
141
|
`;
|
|
136
142
|
}
|
|
143
|
+
function renderRspAgentsBlock() {
|
|
144
|
+
return `${RSP_AGENTS_BEGIN}
|
|
145
|
+
## RSP Entry
|
|
146
|
+
|
|
147
|
+
Read in order:
|
|
148
|
+
1. .rsp/rules/*.md
|
|
149
|
+
2. .rsp/specs/INDEX.md
|
|
150
|
+
3. .rsp/specs/design.md
|
|
151
|
+
4. .rsp/active.d/ and matching .rsp/features/*.md
|
|
152
|
+
|
|
153
|
+
Guidelines:
|
|
154
|
+
- .rsp/rules/rsp-rules.md is required
|
|
155
|
+
- .rsp/rules/project-rules.md is optional
|
|
156
|
+
- Keep project design in .rsp/specs/
|
|
157
|
+
- Add extra spec files only when they have durable value
|
|
158
|
+
- Do not create a feature named 'init'
|
|
159
|
+
${RSP_AGENTS_END}`;
|
|
160
|
+
}
|
|
161
|
+
function upsertRspAgentsBlock(content) {
|
|
162
|
+
const block = renderRspAgentsBlock();
|
|
163
|
+
const managedRe = /<!-- rsp:begin -->[\s\S]*?<!-- rsp:end -->/;
|
|
164
|
+
if (managedRe.test(content)) {
|
|
165
|
+
const next2 = content.replace(managedRe, block);
|
|
166
|
+
return { content: next2, changed: next2 !== content };
|
|
167
|
+
}
|
|
168
|
+
const trimmed = content.trimStart();
|
|
169
|
+
const next = trimmed ? `${block}
|
|
170
|
+
|
|
171
|
+
${trimmed}` : `${block}
|
|
172
|
+
`;
|
|
173
|
+
return { content: next, changed: true };
|
|
174
|
+
}
|
|
175
|
+
function hasRspAgentsBlock(content) {
|
|
176
|
+
return content.includes(RSP_AGENTS_BEGIN) && content.includes(RSP_AGENTS_END);
|
|
177
|
+
}
|
|
178
|
+
function generateDesignContent(projectName) {
|
|
179
|
+
return `# Project Design: ${projectName}
|
|
180
|
+
|
|
181
|
+
## Purpose
|
|
182
|
+
- <what this project is responsible for>
|
|
183
|
+
- <who or what it serves>
|
|
184
|
+
|
|
185
|
+
## Scope
|
|
186
|
+
- In scope:
|
|
187
|
+
- <major capability or boundary>
|
|
188
|
+
- Out of scope:
|
|
189
|
+
- <intentional non-goal>
|
|
190
|
+
|
|
191
|
+
## Structure
|
|
192
|
+
- <important directory or subsystem> \u2014 <responsibility>
|
|
193
|
+
|
|
194
|
+
## Constraints
|
|
195
|
+
- <cross-cutting technical or operational constraint>
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
function generateProjectRulesContent(projectName) {
|
|
199
|
+
return `---
|
|
200
|
+
name: project-rules
|
|
201
|
+
description: Project-specific rules for ${projectName}
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
# Project Rules
|
|
205
|
+
|
|
206
|
+
## Scope
|
|
207
|
+
- <rules that apply across the project>
|
|
208
|
+
|
|
209
|
+
## Validation
|
|
210
|
+
- <preferred validation commands>
|
|
211
|
+
|
|
212
|
+
## Conventions
|
|
213
|
+
- <project-specific conventions>
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
function generateRulesContent(name) {
|
|
217
|
+
const title = toTitleCase(name);
|
|
218
|
+
return `---
|
|
219
|
+
name: ${name}
|
|
220
|
+
description: Project-specific rules for ${title}
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
# ${title}
|
|
224
|
+
|
|
225
|
+
## Scope
|
|
226
|
+
- <what this rules file governs>
|
|
227
|
+
|
|
228
|
+
## Rules
|
|
229
|
+
- <stable local rule>
|
|
230
|
+
|
|
231
|
+
## Validation
|
|
232
|
+
- <relevant validation command or check>
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
function generateSpecContent(name) {
|
|
236
|
+
const title = toTitleCase(name);
|
|
237
|
+
return `# ${title}
|
|
238
|
+
|
|
239
|
+
## Purpose
|
|
240
|
+
- <why this project-level spec exists>
|
|
241
|
+
|
|
242
|
+
## Details
|
|
243
|
+
- <important project-level detail>
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
function toTitleCase(value) {
|
|
247
|
+
return value.split(/[-/]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
248
|
+
}
|
|
137
249
|
function detectDeltaSections(content) {
|
|
138
250
|
const match = content.match(/^## Spec\n([\s\S]*?)(?=\n## |\n---|\n\.\.\.)/m);
|
|
139
251
|
if (!match) {
|
|
@@ -204,6 +316,7 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
204
316
|
var PKG_ROOT = join2(__dirname, "..");
|
|
205
317
|
var RSP_DIR = ".rsp";
|
|
206
318
|
var ACTIVE_DIR = join2(RSP_DIR, "active.d");
|
|
319
|
+
var ARCHIVES_DIR = join2(RSP_DIR, "archives");
|
|
207
320
|
var LOCK_PATH = join2(RSP_DIR, ".lock");
|
|
208
321
|
var CONFIG_PATH = join2(RSP_DIR, "config.yaml");
|
|
209
322
|
var VALID_STATUSES = ["draft", "ready", "in-progress", "blocked", "done"];
|
|
@@ -295,40 +408,204 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
295
408
|
}
|
|
296
409
|
}
|
|
297
410
|
|
|
298
|
-
// src/commands/
|
|
299
|
-
|
|
411
|
+
// src/commands/add-rules.ts
|
|
412
|
+
var NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
413
|
+
async function detectProjectName() {
|
|
414
|
+
try {
|
|
415
|
+
if (existsSync3("package.json")) {
|
|
416
|
+
const raw = await readFile3("package.json", "utf-8");
|
|
417
|
+
const pkg = JSON.parse(raw);
|
|
418
|
+
if (pkg.name && pkg.name !== "")
|
|
419
|
+
return pkg.name;
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
return basename(process.cwd());
|
|
424
|
+
}
|
|
425
|
+
async function addRules(name, projectName = "project") {
|
|
426
|
+
if (!name || !NAME_RE.test(name)) {
|
|
427
|
+
console.error(` ${pc.red("Error:")} rule name must be kebab-case with optional subdirectory`);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
const rspRulesPath = join3(RSP_DIR, "rules", "rsp-rules.md");
|
|
431
|
+
const designPath = join3(RSP_DIR, "specs", "design.md");
|
|
432
|
+
if (!existsSync3(rspRulesPath) || !existsSync3(designPath)) {
|
|
433
|
+
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
434
|
+
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
return withRspLock("add-rules", async () => {
|
|
438
|
+
const dir = join3(RSP_DIR, "rules");
|
|
439
|
+
const path = join3(dir, `${name}.md`);
|
|
440
|
+
if (existsSync3(path)) {
|
|
441
|
+
console.error(` ${pc.red("Error:")} rule file already exists: .rsp/rules/${name}.md`);
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
445
|
+
const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
|
|
446
|
+
const content = name === "project-rules" ? generateProjectRulesContent(resolvedProjectName) : generateRulesContent(name);
|
|
447
|
+
await writeFile2(path, content);
|
|
448
|
+
console.log(` ${pc.green("Created:")} .rsp/rules/${name}.md
|
|
449
|
+
`);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/commands/add-spec.ts
|
|
454
|
+
import { existsSync as existsSync5 } from "fs";
|
|
455
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
456
|
+
import { basename as basename3, dirname as dirname3, join as join5 } from "path";
|
|
457
|
+
|
|
458
|
+
// src/commands/specs-index.ts
|
|
459
|
+
import { existsSync as existsSync4 } from "fs";
|
|
460
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
461
|
+
import { basename as basename2, join as join4, relative as relative2 } from "path";
|
|
462
|
+
async function buildSpecsIndex({ acquireLock = true } = {}) {
|
|
300
463
|
if (acquireLock)
|
|
301
|
-
return withRspLock("
|
|
302
|
-
const
|
|
303
|
-
if (!
|
|
304
|
-
console.log(` ${pc.dim("No
|
|
464
|
+
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false }));
|
|
465
|
+
const specsDir = join4(RSP_DIR, "specs");
|
|
466
|
+
if (!existsSync4(specsDir)) {
|
|
467
|
+
console.log(` ${pc.dim("No specs directory.")}
|
|
305
468
|
`);
|
|
306
469
|
return;
|
|
307
470
|
}
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
471
|
+
const specFiles = (await walkMarkdownFiles(specsDir)).filter((fp) => basename2(fp) !== "INDEX.md").sort();
|
|
472
|
+
const lines = [];
|
|
473
|
+
lines.push("# Specs Index");
|
|
474
|
+
lines.push("");
|
|
475
|
+
lines.push("_Project-level specs and design notes._");
|
|
476
|
+
lines.push("");
|
|
477
|
+
if (specFiles.length === 0) {
|
|
478
|
+
lines.push("_No project-level specs yet._");
|
|
479
|
+
} else {
|
|
480
|
+
lines.push("| File | Title | Summary |");
|
|
481
|
+
lines.push("|------|-------|---------|");
|
|
482
|
+
for (const fp of specFiles) {
|
|
483
|
+
const rel = relative2(specsDir, fp);
|
|
484
|
+
const content = await readFile4(fp, "utf-8");
|
|
485
|
+
const fm = parseFrontmatter(content);
|
|
486
|
+
const title = extractTitle(content) || rel.replace(/\.md$/, "");
|
|
487
|
+
const summary = extractSummary(content, fm?.summary);
|
|
488
|
+
lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
lines.push("");
|
|
492
|
+
lines.push("_Auto-generated by `rsp specs-index`._");
|
|
493
|
+
await writeFile3(join4(specsDir, "INDEX.md"), lines.join("\n"));
|
|
494
|
+
console.log(` ${pc.green("INDEX.md updated:")} ${specFiles.length} spec file(s).
|
|
495
|
+
`);
|
|
496
|
+
}
|
|
497
|
+
function extractTitle(content) {
|
|
498
|
+
for (const rawLine of content.split("\n")) {
|
|
499
|
+
if (!rawLine.startsWith("# "))
|
|
500
|
+
continue;
|
|
501
|
+
return rawLine.slice(2).trim();
|
|
502
|
+
}
|
|
503
|
+
return "";
|
|
504
|
+
}
|
|
505
|
+
function extractSummary(content, frontmatterSummary) {
|
|
506
|
+
if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
|
|
507
|
+
return frontmatterSummary.trim();
|
|
508
|
+
const lines = content.split("\n");
|
|
509
|
+
let inFrontmatter = false;
|
|
510
|
+
for (const rawLine of lines) {
|
|
511
|
+
const line = rawLine.trim();
|
|
512
|
+
if (line === "---" || line === "...") {
|
|
513
|
+
inFrontmatter = !inFrontmatter;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (inFrontmatter || line === "")
|
|
517
|
+
continue;
|
|
518
|
+
if (line.startsWith("#"))
|
|
519
|
+
continue;
|
|
520
|
+
if (line.startsWith("##"))
|
|
521
|
+
continue;
|
|
522
|
+
return line.replace(/^[-*]\s*/, "").trim();
|
|
523
|
+
}
|
|
524
|
+
return "";
|
|
525
|
+
}
|
|
526
|
+
function escapeCell(value) {
|
|
527
|
+
return value.replace(/\|/g, "\\|");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/commands/add-spec.ts
|
|
531
|
+
var NAME_RE2 = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
532
|
+
async function detectProjectName2() {
|
|
533
|
+
try {
|
|
534
|
+
if (existsSync5("package.json")) {
|
|
535
|
+
const raw = await readFile5("package.json", "utf-8");
|
|
536
|
+
const pkg = JSON.parse(raw);
|
|
537
|
+
if (pkg.name && pkg.name !== "")
|
|
538
|
+
return pkg.name;
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
return basename3(process.cwd());
|
|
543
|
+
}
|
|
544
|
+
async function addSpec(name, projectName = "project") {
|
|
545
|
+
if (!name || !NAME_RE2.test(name)) {
|
|
546
|
+
console.error(` ${pc.red("Error:")} spec name must be kebab-case with optional subdirectory`);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
const rspRulesPath = join5(RSP_DIR, "rules", "rsp-rules.md");
|
|
550
|
+
const designPath = join5(RSP_DIR, "specs", "design.md");
|
|
551
|
+
if (!existsSync5(rspRulesPath) || !existsSync5(designPath)) {
|
|
552
|
+
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
553
|
+
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
return withRspLock("add-spec", async () => {
|
|
557
|
+
const dir = join5(RSP_DIR, "specs");
|
|
558
|
+
const path = join5(dir, `${name}.md`);
|
|
559
|
+
if (existsSync5(path)) {
|
|
560
|
+
console.error(` ${pc.red("Error:")} spec file already exists: .rsp/specs/${name}.md`);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
564
|
+
const resolvedProjectName = projectName === "project" ? await detectProjectName2() : projectName;
|
|
565
|
+
const content = name === "design" ? generateDesignContent(resolvedProjectName) : generateSpecContent(name);
|
|
566
|
+
await writeFile4(path, content);
|
|
567
|
+
await buildSpecsIndex({ acquireLock: false });
|
|
568
|
+
console.log(` ${pc.green("Created:")} .rsp/specs/${name}.md
|
|
569
|
+
`);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/commands/archive-index.ts
|
|
574
|
+
import { existsSync as existsSync6 } from "fs";
|
|
575
|
+
import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
576
|
+
import { basename as basename4, dirname as dirname4, join as join6, relative as relative3 } from "path";
|
|
577
|
+
async function buildArchiveIndex({ acquireLock = true } = {}) {
|
|
578
|
+
if (acquireLock)
|
|
579
|
+
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false }));
|
|
580
|
+
const archiveDir = join6(RSP_DIR, "archives");
|
|
581
|
+
if (!existsSync6(archiveDir)) {
|
|
582
|
+
console.log(` ${pc.dim("No archives directory.")}
|
|
311
583
|
`);
|
|
312
584
|
return;
|
|
313
585
|
}
|
|
586
|
+
const archiveFiles = (await walkMarkdownFiles(archiveDir)).filter((fp) => basename4(fp) !== "INDEX.md").sort();
|
|
314
587
|
const lines = [];
|
|
315
588
|
lines.push("# Archive Index");
|
|
316
589
|
lines.push("");
|
|
317
590
|
lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
|
|
318
591
|
lines.push("");
|
|
319
|
-
|
|
320
|
-
|
|
592
|
+
if (archiveFiles.length === 0) {
|
|
593
|
+
lines.push("_No archived features yet._");
|
|
594
|
+
} else {
|
|
595
|
+
lines.push("| Date | Feature | Summary |");
|
|
596
|
+
lines.push("|------|---------|---------|");
|
|
597
|
+
}
|
|
321
598
|
for (const fp of archiveFiles) {
|
|
322
|
-
const base =
|
|
323
|
-
const archiveRel =
|
|
324
|
-
const archiveDirPart =
|
|
599
|
+
const base = basename4(fp);
|
|
600
|
+
const archiveRel = relative3(archiveDir, fp);
|
|
601
|
+
const archiveDirPart = dirname4(archiveRel);
|
|
325
602
|
const dateMatch = base.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.md$/);
|
|
326
603
|
const date = dateMatch ? dateMatch[1] : "\u2014";
|
|
327
604
|
const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
|
|
328
605
|
const featureName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
|
|
329
606
|
let summary = "";
|
|
330
607
|
try {
|
|
331
|
-
const content = await
|
|
608
|
+
const content = await readFile6(fp, "utf-8");
|
|
332
609
|
const fm = parseFrontmatter(content);
|
|
333
610
|
if (fm?.summary)
|
|
334
611
|
summary = String(fm.summary);
|
|
@@ -344,18 +621,18 @@ async function buildArchiveIndex({ acquireLock = true } = {}) {
|
|
|
344
621
|
}
|
|
345
622
|
lines.push("");
|
|
346
623
|
lines.push("_Auto-generated by `rsp archive-index`._");
|
|
347
|
-
const indexPath =
|
|
348
|
-
await
|
|
624
|
+
const indexPath = join6(archiveDir, "INDEX.md");
|
|
625
|
+
await writeFile5(indexPath, lines.join("\n"));
|
|
349
626
|
console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived feature(s).
|
|
350
627
|
`);
|
|
351
628
|
}
|
|
352
629
|
|
|
353
630
|
// src/commands/check.ts
|
|
354
|
-
import { existsSync as
|
|
355
|
-
import { readFile as
|
|
356
|
-
import { join as
|
|
631
|
+
import { existsSync as existsSync7 } from "fs";
|
|
632
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
633
|
+
import { join as join7, relative as relative4 } from "path";
|
|
357
634
|
async function runCheck() {
|
|
358
|
-
const featuresDir =
|
|
635
|
+
const featuresDir = join7(RSP_DIR, "features");
|
|
359
636
|
const config = await loadRspConfig();
|
|
360
637
|
const validStatuses = resolveStatuses(config);
|
|
361
638
|
const validPriorities = resolvePriorities(config);
|
|
@@ -365,34 +642,34 @@ async function runCheck() {
|
|
|
365
642
|
console.log();
|
|
366
643
|
console.log(` ${pc.bold("RSP check")}`);
|
|
367
644
|
console.log();
|
|
368
|
-
if (
|
|
645
|
+
if (existsSync7(ACTIVE_DIR)) {
|
|
369
646
|
const entries = await walkFiles(ACTIVE_DIR);
|
|
370
647
|
for (const entryPath of entries) {
|
|
371
|
-
const entryContent =
|
|
372
|
-
const markerContent = (await
|
|
648
|
+
const entryContent = relative4(ACTIVE_DIR, entryPath);
|
|
649
|
+
const markerContent = (await readFile7(entryPath, "utf-8")).trim();
|
|
373
650
|
if (markerContent) {
|
|
374
651
|
console.log(` ${pc.yellow("\u26A0")} active.d/${entryContent} should be an empty marker file; path is the source of truth`);
|
|
375
652
|
warnings++;
|
|
376
653
|
}
|
|
377
|
-
const fp =
|
|
378
|
-
if (!
|
|
654
|
+
const fp = join7(featuresDir, `${entryContent}.md`);
|
|
655
|
+
if (!existsSync7(fp)) {
|
|
379
656
|
console.log(` ${pc.red("\u2717")} active.d/${entryContent} points to "${entryContent}" but features/${entryContent}.md not found`);
|
|
380
657
|
errors++;
|
|
381
658
|
}
|
|
382
659
|
}
|
|
383
660
|
}
|
|
384
|
-
const featureFiles =
|
|
661
|
+
const featureFiles = existsSync7(featuresDir) ? await walkMarkdownFiles(featuresDir) : [];
|
|
385
662
|
if (featureFiles.length === 0) {
|
|
386
663
|
console.log(` ${pc.dim("No feature files to check.")}
|
|
387
664
|
`);
|
|
388
|
-
return;
|
|
665
|
+
return 0;
|
|
389
666
|
}
|
|
390
667
|
const allDeps = /* @__PURE__ */ new Map();
|
|
391
668
|
for (const fp of featureFiles) {
|
|
392
669
|
const name = featureNameFromPath(featuresDir, fp);
|
|
393
670
|
let content;
|
|
394
671
|
try {
|
|
395
|
-
content = await
|
|
672
|
+
content = await readFile7(fp, "utf-8");
|
|
396
673
|
} catch {
|
|
397
674
|
console.log(` ${pc.red("\u2717")} ${name} \u2014 unable to read file`);
|
|
398
675
|
errors++;
|
|
@@ -487,8 +764,8 @@ async function runCheck() {
|
|
|
487
764
|
}
|
|
488
765
|
seen.add(dep);
|
|
489
766
|
allDeps.set(name, [...allDeps.get(name) || [], dep]);
|
|
490
|
-
const depFp =
|
|
491
|
-
if (!
|
|
767
|
+
const depFp = join7(featuresDir, `${dep}.md`);
|
|
768
|
+
if (!existsSync7(depFp)) {
|
|
492
769
|
console.log(` ${pc.red("\u2717")} ${name} \u2014 depends on "${dep}" but features/${dep}.md not found`);
|
|
493
770
|
errors++;
|
|
494
771
|
}
|
|
@@ -511,9 +788,9 @@ async function runCheck() {
|
|
|
511
788
|
}
|
|
512
789
|
|
|
513
790
|
// src/commands/close-feature.ts
|
|
514
|
-
import { existsSync as
|
|
515
|
-
import {
|
|
516
|
-
import { basename as
|
|
791
|
+
import { existsSync as existsSync8 } from "fs";
|
|
792
|
+
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile8, rename, rmdir, unlink as unlink2 } from "fs/promises";
|
|
793
|
+
import { basename as basename5, dirname as dirname5, join as join8 } from "path";
|
|
517
794
|
async function closeFeature(name) {
|
|
518
795
|
return withRspLock("close-feature", async () => {
|
|
519
796
|
if (!name) {
|
|
@@ -524,128 +801,104 @@ async function closeFeature(name) {
|
|
|
524
801
|
console.error(` ${pc.red("Error:")} feature name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
525
802
|
process.exit(1);
|
|
526
803
|
}
|
|
527
|
-
const srcPath =
|
|
528
|
-
if (!
|
|
804
|
+
const srcPath = join8(RSP_DIR, "features", `${name}.md`);
|
|
805
|
+
if (!existsSync8(srcPath)) {
|
|
529
806
|
console.error(` ${pc.red("Feature not found:")} .rsp/features/${name}.md`);
|
|
530
807
|
process.exit(1);
|
|
531
808
|
}
|
|
809
|
+
const dependents = await findDependents(name);
|
|
810
|
+
if (dependents.length > 0) {
|
|
811
|
+
console.error(` ${pc.red("Error:")} cannot close "${name}" because it is still referenced by: ${dependents.join(", ")}`);
|
|
812
|
+
console.error(` ${pc.dim("Remove or update those depends-on entries before archiving this feature.")}`);
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
532
815
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
533
|
-
const dir =
|
|
534
|
-
const base =
|
|
535
|
-
const archiveSubdir = dir !== "." ?
|
|
816
|
+
const dir = dirname5(name);
|
|
817
|
+
const base = basename5(name);
|
|
818
|
+
const archiveSubdir = dir !== "." ? join8(ARCHIVES_DIR, dir) : ARCHIVES_DIR;
|
|
536
819
|
const archiveName = `${date}_${base}.md`;
|
|
537
|
-
await
|
|
538
|
-
await rename(srcPath,
|
|
820
|
+
await mkdir4(archiveSubdir, { recursive: true });
|
|
821
|
+
await rename(srcPath, join8(archiveSubdir, archiveName));
|
|
539
822
|
let activeCleared = false;
|
|
540
|
-
const activeEntry =
|
|
541
|
-
if (
|
|
823
|
+
const activeEntry = join8(ACTIVE_DIR, name);
|
|
824
|
+
if (existsSync8(activeEntry)) {
|
|
542
825
|
await unlink2(activeEntry);
|
|
543
|
-
|
|
544
|
-
while (parent !== ACTIVE_DIR) {
|
|
545
|
-
try {
|
|
546
|
-
const remaining = await readdir2(parent);
|
|
547
|
-
if (remaining.length === 0)
|
|
548
|
-
await rmdir(parent);
|
|
549
|
-
else
|
|
550
|
-
break;
|
|
551
|
-
} catch {
|
|
552
|
-
break;
|
|
553
|
-
}
|
|
554
|
-
parent = dirname3(parent);
|
|
555
|
-
}
|
|
826
|
+
await cleanupEmptyParentDirs(activeEntry, ACTIVE_DIR);
|
|
556
827
|
activeCleared = true;
|
|
557
828
|
}
|
|
829
|
+
await cleanupEmptyParentDirs(srcPath, join8(RSP_DIR, "features"));
|
|
558
830
|
const clearedMsg = activeCleared ? ` ${pc.dim("active.d cleared")}
|
|
559
831
|
` : "";
|
|
560
832
|
console.log(` ${pc.green("Archived:")} ${archiveName}
|
|
561
833
|
${clearedMsg}`);
|
|
562
834
|
await buildArchiveIndex({ acquireLock: false });
|
|
563
835
|
await runCheckIfHasDependents(name);
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const archiveRelPath = dir !== "." ? join5(dir, archiveName) : archiveName;
|
|
836
|
+
if (existsSync8(".git")) {
|
|
837
|
+
const archiveRelPath = dir !== "." ? join8(dir, archiveName) : archiveName;
|
|
567
838
|
console.log(` ${pc.cyan("Git workflow:")}
|
|
568
839
|
`);
|
|
569
|
-
console.log(` git add .rsp/
|
|
840
|
+
console.log(` git add .rsp/archives/${archiveRelPath}`);
|
|
570
841
|
console.log(` git commit -m "feat: complete ${name}"`);
|
|
571
842
|
console.log();
|
|
572
843
|
}
|
|
573
844
|
});
|
|
574
845
|
}
|
|
846
|
+
async function findDependents(name) {
|
|
847
|
+
const featuresDir = join8(RSP_DIR, "features");
|
|
848
|
+
const allFeatureFiles = await walkMarkdownFiles(featuresDir);
|
|
849
|
+
const dependents = [];
|
|
850
|
+
for (const fp of allFeatureFiles) {
|
|
851
|
+
const featureName = fp === join8(featuresDir, `${name}.md`) ? name : fp.replace(`${featuresDir}/`, "").replace(/\.md$/, "");
|
|
852
|
+
if (featureName === name)
|
|
853
|
+
continue;
|
|
854
|
+
const content = await readFile8(fp, "utf-8");
|
|
855
|
+
const fm = parseFrontmatter(content);
|
|
856
|
+
if (!fm || !fm["depends-on"])
|
|
857
|
+
continue;
|
|
858
|
+
const deps = Array.isArray(fm["depends-on"]) ? fm["depends-on"] : [fm["depends-on"]];
|
|
859
|
+
if (deps.includes(name))
|
|
860
|
+
dependents.push(featureName);
|
|
861
|
+
}
|
|
862
|
+
return dependents.sort();
|
|
863
|
+
}
|
|
575
864
|
async function runCheckIfHasDependents(name) {
|
|
576
865
|
try {
|
|
577
|
-
const
|
|
578
|
-
const allFeatureFiles = await walkMarkdownFiles(featuresDir);
|
|
579
|
-
let hasDependents = false;
|
|
580
|
-
for (const fp of allFeatureFiles) {
|
|
581
|
-
const content = await readFile5(fp, "utf-8");
|
|
582
|
-
const fm = parseFrontmatter(content);
|
|
583
|
-
if (!fm || !fm["depends-on"])
|
|
584
|
-
continue;
|
|
585
|
-
const deps = Array.isArray(fm["depends-on"]) ? fm["depends-on"] : [fm["depends-on"]];
|
|
586
|
-
if (deps.includes(name)) {
|
|
587
|
-
hasDependents = true;
|
|
588
|
-
break;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
866
|
+
const hasDependents = (await findDependents(name)).length > 0;
|
|
591
867
|
if (hasDependents)
|
|
592
868
|
await runCheck();
|
|
593
869
|
} catch {
|
|
594
870
|
console.warn(` ${pc.dim("\u26A0 could not verify dependents for")} "${name}"`);
|
|
595
871
|
}
|
|
596
872
|
}
|
|
597
|
-
async function
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const escaped = name.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
601
|
-
const lineRe = new RegExp(`^\\| \\d{4}-\\d{2}-\\d{2} \\| ${escaped} \\|`, "m");
|
|
602
|
-
const archiveFile = await (async () => {
|
|
873
|
+
async function cleanupEmptyParentDirs(path, stopDir) {
|
|
874
|
+
let parent = dirname5(path);
|
|
875
|
+
while (parent !== stopDir) {
|
|
603
876
|
try {
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
})();
|
|
610
|
-
let summary = "\u2014";
|
|
611
|
-
if (archiveFile) {
|
|
612
|
-
try {
|
|
613
|
-
const content = await readFile5(join5(archiveDir, archiveFile), "utf-8");
|
|
614
|
-
const m = content.match(/^## Spec[\s\S]*?\n- Summary:[ \t]*([^\n]+)$/m);
|
|
615
|
-
if (m)
|
|
616
|
-
summary = m[1].trim();
|
|
877
|
+
const remaining = await readdir2(parent);
|
|
878
|
+
if (remaining.length === 0)
|
|
879
|
+
await rmdir(parent);
|
|
880
|
+
else
|
|
881
|
+
break;
|
|
617
882
|
} catch {
|
|
883
|
+
break;
|
|
618
884
|
}
|
|
619
|
-
|
|
620
|
-
const line = `| ${date} | ${name} | ${summary} |
|
|
621
|
-
`;
|
|
622
|
-
try {
|
|
623
|
-
let existing = "";
|
|
624
|
-
if (existsSync5(specIndexPath))
|
|
625
|
-
existing = await readFile5(specIndexPath, "utf-8");
|
|
626
|
-
if (lineRe.test(existing))
|
|
627
|
-
return;
|
|
628
|
-
if (!existing.includes("| Date | Feature | Summary |")) {
|
|
629
|
-
await writeFile3(specIndexPath, `# Specs Index
|
|
630
|
-
|
|
631
|
-
| Date | Feature | Summary |
|
|
632
|
-
|------|---------|---------|
|
|
633
|
-
${line}
|
|
634
|
-
`);
|
|
635
|
-
} else {
|
|
636
|
-
await appendFile(specIndexPath, line);
|
|
637
|
-
}
|
|
638
|
-
} catch {
|
|
885
|
+
parent = dirname5(parent);
|
|
639
886
|
}
|
|
640
887
|
}
|
|
641
888
|
|
|
642
889
|
// src/commands/deps.ts
|
|
643
|
-
import { existsSync as
|
|
644
|
-
import { readFile as
|
|
645
|
-
import { join as
|
|
646
|
-
async function showDependencies(
|
|
647
|
-
const
|
|
648
|
-
const
|
|
890
|
+
import { existsSync as existsSync9 } from "fs";
|
|
891
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
892
|
+
import { join as join9 } from "path";
|
|
893
|
+
async function showDependencies(options = false) {
|
|
894
|
+
const resolved = typeof options === "boolean" ? { mermaid: options } : options;
|
|
895
|
+
const mermaid = Boolean(resolved.mermaid);
|
|
896
|
+
const focus = resolved.focus?.trim();
|
|
897
|
+
const reverse = resolved.reverse?.trim();
|
|
898
|
+
if (focus && reverse)
|
|
899
|
+
throw new Error("Use either --focus or --reverse, not both");
|
|
900
|
+
const featuresDir = join9(RSP_DIR, "features");
|
|
901
|
+
const featureFiles = existsSync9(featuresDir) ? await walkMarkdownFiles(featuresDir) : [];
|
|
649
902
|
console.log();
|
|
650
903
|
console.log(` ${pc.bold("RSP dependencies")}`);
|
|
651
904
|
console.log();
|
|
@@ -660,7 +913,7 @@ async function showDependencies(mermaid = false) {
|
|
|
660
913
|
const name = featureNameFromPath(featuresDir, fp);
|
|
661
914
|
let content;
|
|
662
915
|
try {
|
|
663
|
-
content = await
|
|
916
|
+
content = await readFile9(fp, "utf-8");
|
|
664
917
|
} catch {
|
|
665
918
|
continue;
|
|
666
919
|
}
|
|
@@ -674,18 +927,41 @@ async function showDependencies(mermaid = false) {
|
|
|
674
927
|
dependents.set(dep, list);
|
|
675
928
|
}
|
|
676
929
|
}
|
|
930
|
+
let visibleRecords = [...records];
|
|
931
|
+
if (focus) {
|
|
932
|
+
const related = /* @__PURE__ */ new Set([focus]);
|
|
933
|
+
const target = records.find((record) => record.name === focus);
|
|
934
|
+
if (target) {
|
|
935
|
+
for (const dep of target.deps)
|
|
936
|
+
related.add(dep);
|
|
937
|
+
}
|
|
938
|
+
for (const dependent of dependents.get(focus) || [])
|
|
939
|
+
related.add(dependent);
|
|
940
|
+
visibleRecords = records.filter((record) => related.has(record.name));
|
|
941
|
+
} else if (reverse) {
|
|
942
|
+
const related = new Set(dependents.get(reverse) || []);
|
|
943
|
+
visibleRecords = records.filter((record) => related.has(record.name));
|
|
944
|
+
}
|
|
945
|
+
if ((focus || reverse) && visibleRecords.length === 0) {
|
|
946
|
+
console.log(` ${pc.dim("No dependency matches found for the current filter.")}
|
|
947
|
+
`);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
677
950
|
if (mermaid) {
|
|
678
951
|
console.log("```mermaid");
|
|
679
952
|
console.log("graph LR");
|
|
680
953
|
const safeId = (s) => s.replace(/\//g, "__slash__").replace(/\W/g, "_");
|
|
681
|
-
for (const { name } of
|
|
954
|
+
for (const { name } of visibleRecords) {
|
|
682
955
|
const id = safeId(name);
|
|
683
956
|
const label = name.replace(/"/g, "#quot;");
|
|
684
957
|
console.log(` ${id}["${label}"]`);
|
|
685
958
|
}
|
|
686
|
-
|
|
959
|
+
const visibleNames = new Set(visibleRecords.map((record) => record.name));
|
|
960
|
+
for (const { name, deps } of visibleRecords) {
|
|
687
961
|
const fromId = safeId(name);
|
|
688
962
|
for (const dep of deps) {
|
|
963
|
+
if (!visibleNames.has(dep))
|
|
964
|
+
continue;
|
|
689
965
|
const toId = safeId(dep);
|
|
690
966
|
console.log(` ${fromId} --> ${toId}`);
|
|
691
967
|
}
|
|
@@ -696,7 +972,7 @@ async function showDependencies(mermaid = false) {
|
|
|
696
972
|
}
|
|
697
973
|
console.log(` ${"Feature".padEnd(28)} ${"Depends On".padEnd(28)} Dependents`);
|
|
698
974
|
console.log(` ${"\u2500".repeat(28)} ${"\u2500".repeat(28)} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
699
|
-
for (const { name, deps } of
|
|
975
|
+
for (const { name, deps } of visibleRecords.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
700
976
|
const depText = deps.length > 0 ? deps.join(", ") : "\u2014";
|
|
701
977
|
const dependentText = (dependents.get(name) || []).length > 0 ? pc.dim((dependents.get(name) || []).join(", ")) : "\u2014";
|
|
702
978
|
console.log(` ${name.padEnd(28)} ${depText.padEnd(28)} ${dependentText}`);
|
|
@@ -704,36 +980,149 @@ async function showDependencies(mermaid = false) {
|
|
|
704
980
|
console.log();
|
|
705
981
|
}
|
|
706
982
|
|
|
707
|
-
// src/commands/
|
|
708
|
-
import { existsSync as
|
|
709
|
-
import {
|
|
710
|
-
import {
|
|
711
|
-
async function
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
983
|
+
// src/commands/doctor.ts
|
|
984
|
+
import { existsSync as existsSync10 } from "fs";
|
|
985
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
986
|
+
import { join as join10, relative as relative5 } from "path";
|
|
987
|
+
async function runDoctor() {
|
|
988
|
+
let issues = 0;
|
|
989
|
+
console.log();
|
|
990
|
+
console.log(` ${pc.bold("RSP doctor")}`);
|
|
991
|
+
console.log();
|
|
992
|
+
const rspRulesPath = join10(RSP_DIR, "rules", "rsp-rules.md");
|
|
993
|
+
const designPath = join10(RSP_DIR, "specs", "design.md");
|
|
994
|
+
const specsIndexPath = join10(RSP_DIR, "specs", "INDEX.md");
|
|
995
|
+
const archivesIndexPath = join10(RSP_DIR, "archives", "INDEX.md");
|
|
996
|
+
issues += reportCheck(".rsp exists", existsSync10(RSP_DIR), "Run: rsp init");
|
|
997
|
+
issues += reportCheck("rules/rsp-rules.md exists", existsSync10(rspRulesPath), "Run: rsp init");
|
|
998
|
+
issues += reportCheck("specs/design.md exists", existsSync10(designPath), "Run: rsp init");
|
|
999
|
+
issues += reportCheck("specs/INDEX.md exists", existsSync10(specsIndexPath), "Run: rsp specs-index");
|
|
1000
|
+
issues += reportCheck("archives/INDEX.md exists", existsSync10(archivesIndexPath), "Run: rsp archive-index");
|
|
1001
|
+
issues += await checkAgents();
|
|
1002
|
+
issues += await checkGeneratedIndex(specsIndexPath, "_Auto-generated by `rsp specs-index`._", "specs/INDEX.md has generated signature", "Run: rsp specs-index");
|
|
1003
|
+
issues += await checkGeneratedIndex(archivesIndexPath, "_Auto-generated by `rsp archive-index`._", "archives/INDEX.md has generated signature", "Run: rsp archive-index");
|
|
1004
|
+
issues += await checkArchiveNaming();
|
|
1005
|
+
issues += await checkConfigSemantics();
|
|
1006
|
+
issues += await checkActiveFeatureConsistency();
|
|
1007
|
+
if (issues === 0)
|
|
1008
|
+
console.log(`
|
|
1009
|
+
${pc.green("\u2713")} RSP setup looks healthy.
|
|
1010
|
+
`);
|
|
1011
|
+
else
|
|
1012
|
+
console.log(`
|
|
1013
|
+
${pc.yellow(String(issues))} issue(s) detected.
|
|
1014
|
+
`);
|
|
1015
|
+
return issues;
|
|
1016
|
+
}
|
|
1017
|
+
function reportCheck(label, ok, fixHint) {
|
|
1018
|
+
if (ok) {
|
|
1019
|
+
console.log(` ${pc.green("\u2713")} ${label}`);
|
|
1020
|
+
return 0;
|
|
720
1021
|
}
|
|
721
|
-
|
|
1022
|
+
console.log(` ${pc.yellow("!")} ${label}`);
|
|
1023
|
+
if (fixHint)
|
|
1024
|
+
console.log(` ${pc.dim(fixHint)}`);
|
|
1025
|
+
return 1;
|
|
1026
|
+
}
|
|
1027
|
+
async function checkAgents() {
|
|
1028
|
+
if (!existsSync10("AGENTS.md")) {
|
|
1029
|
+
console.log(` ${pc.yellow("!")} AGENTS.md missing`);
|
|
1030
|
+
console.log(` ${pc.dim("Run: rsp init --agents-mode managed")}`);
|
|
1031
|
+
return 1;
|
|
1032
|
+
}
|
|
1033
|
+
const agents = await readFile10("AGENTS.md", "utf-8");
|
|
1034
|
+
if (hasRspAgentsBlock(agents)) {
|
|
1035
|
+
console.log(` ${pc.green("\u2713")} AGENTS.md contains managed RSP block`);
|
|
1036
|
+
return 0;
|
|
1037
|
+
}
|
|
1038
|
+
console.log(` ${pc.yellow("!")} AGENTS.md missing managed RSP block`);
|
|
1039
|
+
console.log(` ${pc.dim("Run: rsp init --agents-mode managed")}`);
|
|
1040
|
+
return 1;
|
|
1041
|
+
}
|
|
1042
|
+
async function checkGeneratedIndex(path, signature, label, fixHint) {
|
|
1043
|
+
if (!existsSync10(path))
|
|
1044
|
+
return 0;
|
|
1045
|
+
const content = await readFile10(path, "utf-8");
|
|
1046
|
+
if (content.includes(signature)) {
|
|
1047
|
+
console.log(` ${pc.green("\u2713")} ${label}`);
|
|
1048
|
+
return 0;
|
|
1049
|
+
}
|
|
1050
|
+
console.log(` ${pc.yellow("!")} ${label}`);
|
|
1051
|
+
console.log(` ${pc.dim(fixHint)}`);
|
|
1052
|
+
return 1;
|
|
1053
|
+
}
|
|
1054
|
+
async function checkArchiveNaming() {
|
|
1055
|
+
const archivesDir = join10(RSP_DIR, "archives");
|
|
1056
|
+
const archiveFiles = existsSync10(archivesDir) ? (await walkMarkdownFiles(archivesDir)).filter((fp) => relative5(archivesDir, fp) !== "INDEX.md") : [];
|
|
1057
|
+
const invalidFiles = archiveFiles.map((fp) => relative5(archivesDir, fp)).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(fp.split("/").at(-1) || "") === false);
|
|
1058
|
+
if (invalidFiles.length === 0) {
|
|
1059
|
+
console.log(` ${pc.green("\u2713")} archived feature files follow the date_name.md convention`);
|
|
1060
|
+
return 0;
|
|
1061
|
+
}
|
|
1062
|
+
console.log(` ${pc.yellow("!")} archived feature files with invalid names: ${invalidFiles.join(", ")}`);
|
|
1063
|
+
console.log(` ${pc.dim("Rename archive files to YYYY-MM-DD_name.md and run: rsp archive-index")}`);
|
|
1064
|
+
return 1;
|
|
1065
|
+
}
|
|
1066
|
+
async function checkConfigSemantics() {
|
|
1067
|
+
if (!existsSync10(CONFIG_PATH))
|
|
1068
|
+
return 0;
|
|
1069
|
+
const content = await readFile10(CONFIG_PATH, "utf-8");
|
|
1070
|
+
const parsed = parseYamlLines(content.split("\n"));
|
|
1071
|
+
let issues = 0;
|
|
1072
|
+
issues += reportConfigListField(parsed, "statuses");
|
|
1073
|
+
issues += reportConfigListField(parsed, "priorities");
|
|
1074
|
+
issues += reportConfigListField(parsed, "required_sections");
|
|
1075
|
+
return issues;
|
|
1076
|
+
}
|
|
1077
|
+
function reportConfigListField(parsed, key) {
|
|
1078
|
+
if (!(key in parsed))
|
|
1079
|
+
return 0;
|
|
1080
|
+
const value = parsed[key];
|
|
1081
|
+
if (!Array.isArray(value)) {
|
|
1082
|
+
console.log(` ${pc.yellow("!")} config.yaml field "${key}" must be a YAML list`);
|
|
1083
|
+
console.log(` ${pc.dim(`Update .rsp/config.yaml so "${key}" uses "key:" followed by indented "- item" entries`)}`);
|
|
1084
|
+
return 1;
|
|
1085
|
+
}
|
|
1086
|
+
const items = value.map(String);
|
|
1087
|
+
const duplicates = [...new Set(items.filter((item, index) => items.indexOf(item) !== index))];
|
|
1088
|
+
if (duplicates.length === 0) {
|
|
1089
|
+
console.log(` ${pc.green("\u2713")} config.yaml field "${key}" has valid list semantics`);
|
|
1090
|
+
return 0;
|
|
1091
|
+
}
|
|
1092
|
+
console.log(` ${pc.yellow("!")} config.yaml field "${key}" contains duplicate entries: ${duplicates.join(", ")}`);
|
|
1093
|
+
console.log(` ${pc.dim(`Remove duplicate values from .rsp/config.yaml:${key}`)}`);
|
|
1094
|
+
return 1;
|
|
1095
|
+
}
|
|
1096
|
+
async function checkActiveFeatureConsistency() {
|
|
1097
|
+
const featuresDir = join10(RSP_DIR, "features");
|
|
1098
|
+
const featureFiles = existsSync10(featuresDir) ? await walkMarkdownFiles(featuresDir) : [];
|
|
1099
|
+
const featureNames = new Set(featureFiles.map((fp) => relative5(featuresDir, fp).replace(/\.md$/, "")));
|
|
1100
|
+
const activeEntries = existsSync10(ACTIVE_DIR) ? await walkFiles(ACTIVE_DIR) : [];
|
|
1101
|
+
const activeNames = new Set(activeEntries.map((fp) => relative5(ACTIVE_DIR, fp)));
|
|
1102
|
+
const missingFeatureFiles = [...activeNames].filter((name) => !featureNames.has(name));
|
|
1103
|
+
const unstagedActiveMarkers = [...featureNames].filter((name) => !activeNames.has(name));
|
|
1104
|
+
let issues = 0;
|
|
1105
|
+
if (missingFeatureFiles.length === 0) {
|
|
1106
|
+
console.log(` ${pc.green("\u2713")} every active marker maps to a feature file`);
|
|
1107
|
+
} else {
|
|
1108
|
+
console.log(` ${pc.yellow("!")} active markers without matching feature files: ${missingFeatureFiles.join(", ")}`);
|
|
1109
|
+
console.log(` ${pc.dim("Run: remove stale files in .rsp/active.d/ or recreate the feature with rsp new <name>")}`);
|
|
1110
|
+
issues++;
|
|
1111
|
+
}
|
|
1112
|
+
if (unstagedActiveMarkers.length === 0) {
|
|
1113
|
+
console.log(` ${pc.green("\u2713")} every feature file has an active marker`);
|
|
1114
|
+
} else {
|
|
1115
|
+
console.log(` ${pc.yellow("!")} feature files without active markers: ${unstagedActiveMarkers.join(", ")}`);
|
|
1116
|
+
console.log(` ${pc.dim("Run: recreate the marker with rsp new <name> or archive the feature with rsp close <name>")}`);
|
|
1117
|
+
issues++;
|
|
1118
|
+
}
|
|
1119
|
+
return issues;
|
|
722
1120
|
}
|
|
723
|
-
var AGENTS_BODY = `## Collaboration Mode
|
|
724
|
-
This project uses **RSP** (Rules, Specs, Plans) \u2014 see \`.rsp/rules/\` for the full workflow definition (start with \`rsp-rules.md\`).
|
|
725
|
-
|
|
726
|
-
Key conventions:
|
|
727
|
-
- \`.rsp/rules/\` \u2014 technical constraints and coding conventions
|
|
728
|
-
- \`.rsp/specs/\` \u2014 project-level architecture and design reference
|
|
729
|
-
- \`.rsp/features/<name>.md\` \u2014 feature definitions (spec + plan + tests)
|
|
730
|
-
- \`.rsp/active.d/\` \u2014 currently active features (path = feature name)
|
|
731
|
-
- \`.rsp/archive/\` \u2014 completed features
|
|
732
1121
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1122
|
+
// src/commands/init.ts
|
|
1123
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1124
|
+
import { mkdir as mkdir5, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
1125
|
+
import { basename as basename6, join as join11 } from "path";
|
|
737
1126
|
var CONFIG_TEMPLATE = `# RSP project configuration
|
|
738
1127
|
# Uncomment any section below to override defaults.
|
|
739
1128
|
#
|
|
@@ -759,68 +1148,86 @@ var CONFIG_TEMPLATE = `# RSP project configuration
|
|
|
759
1148
|
# - Spec
|
|
760
1149
|
# - Plan
|
|
761
1150
|
`;
|
|
762
|
-
async function
|
|
763
|
-
|
|
1151
|
+
async function detectProjectName3() {
|
|
1152
|
+
try {
|
|
1153
|
+
if (existsSync11("package.json")) {
|
|
1154
|
+
const raw = await readFile11("package.json", "utf-8");
|
|
1155
|
+
const pkg = JSON.parse(raw);
|
|
1156
|
+
if (pkg.name && pkg.name !== "")
|
|
1157
|
+
return pkg.name;
|
|
1158
|
+
}
|
|
1159
|
+
} catch {
|
|
1160
|
+
}
|
|
1161
|
+
return basename6(process.cwd());
|
|
1162
|
+
}
|
|
1163
|
+
async function ensureFile(path, content) {
|
|
1164
|
+
if (existsSync11(path))
|
|
1165
|
+
return false;
|
|
1166
|
+
await writeFile6(path, content);
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
function toTitle(projectName) {
|
|
1170
|
+
return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
1171
|
+
}
|
|
1172
|
+
async function initProject(args = {}) {
|
|
1173
|
+
const isNew = !existsSync11(RSP_DIR);
|
|
1174
|
+
const projectName = await detectProjectName3();
|
|
764
1175
|
return withRspLock("init", async () => {
|
|
765
1176
|
const dirs = [
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1177
|
+
join11(RSP_DIR, "rules"),
|
|
1178
|
+
join11(RSP_DIR, "specs"),
|
|
1179
|
+
join11(RSP_DIR, "features"),
|
|
1180
|
+
join11(RSP_DIR, "active.d"),
|
|
1181
|
+
join11(RSP_DIR, "archives")
|
|
770
1182
|
];
|
|
771
|
-
let created = false;
|
|
772
1183
|
for (const d of dirs)
|
|
773
|
-
await
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
1184
|
+
await mkdir5(d, { recursive: true });
|
|
1185
|
+
let created = false;
|
|
1186
|
+
const bundledRules = await readFile11(join11(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1187
|
+
created = await ensureFile(join11(RSP_DIR, "rules", "rsp-rules.md"), bundledRules) || created;
|
|
1188
|
+
created = await ensureFile(join11(RSP_DIR, "config.yaml"), CONFIG_TEMPLATE) || created;
|
|
1189
|
+
const createdSpecsIndex = await ensureFile(join11(RSP_DIR, "specs", "INDEX.md"), "# Specs Index\n\n_Project-level specs and design notes._\n");
|
|
1190
|
+
created = createdSpecsIndex || created;
|
|
1191
|
+
const createdDesign = await ensureFile(join11(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
|
|
1192
|
+
created = createdDesign || created;
|
|
1193
|
+
const createdArchivesIndex = await ensureFile(join11(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
|
|
1194
|
+
created = createdArchivesIndex || created;
|
|
1195
|
+
created = await ensureFile(join11(RSP_DIR, "features", ".gitkeep"), "") || created;
|
|
1196
|
+
created = await ensureFile(join11(RSP_DIR, "active.d", ".gitkeep"), "") || created;
|
|
1197
|
+
created = await ensureFile(join11(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
|
|
1198
|
+
if (args.withProjectRules)
|
|
1199
|
+
created = await ensureFile(join11(RSP_DIR, "rules", "project-rules.md"), generateProjectRulesContent(projectName)) || created;
|
|
1200
|
+
if (createdSpecsIndex || createdDesign)
|
|
1201
|
+
await buildSpecsIndex({ acquireLock: false });
|
|
1202
|
+
if (createdArchivesIndex)
|
|
1203
|
+
await buildArchiveIndex({ acquireLock: false });
|
|
794
1204
|
const agentsPath = "AGENTS.md";
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const h2Match = existing.match(/^## /m);
|
|
801
|
-
if (h2Match) {
|
|
802
|
-
const before = existing.slice(0, h2Match.index);
|
|
803
|
-
const after = existing.slice(h2Match.index);
|
|
804
|
-
await writeFile4(agentsPath, `${before}${AGENTS_BODY}
|
|
805
|
-
|
|
806
|
-
---
|
|
807
|
-
|
|
808
|
-
${after}`);
|
|
809
|
-
} else {
|
|
810
|
-
await writeFile4(agentsPath, `${existing}
|
|
1205
|
+
const title = toTitle(projectName);
|
|
1206
|
+
const mode = args.agentsMode ?? "managed";
|
|
1207
|
+
if (!existsSync11(agentsPath)) {
|
|
1208
|
+
if (mode === "managed") {
|
|
1209
|
+
await writeFile6(agentsPath, `# ${title}
|
|
811
1210
|
|
|
812
|
-
${
|
|
1211
|
+
${upsertRspAgentsBlock("").content}
|
|
813
1212
|
`);
|
|
814
|
-
}
|
|
815
1213
|
created = true;
|
|
816
1214
|
}
|
|
817
|
-
} else {
|
|
818
|
-
const
|
|
819
|
-
|
|
1215
|
+
} else if (mode === "managed") {
|
|
1216
|
+
const existing = await readFile11(agentsPath, "utf-8");
|
|
1217
|
+
const next = upsertRspAgentsBlock(existing);
|
|
1218
|
+
if (next.changed) {
|
|
1219
|
+
await writeFile6(agentsPath, next.content);
|
|
1220
|
+
created = true;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (mode === "print") {
|
|
1224
|
+
const managed = existsSync11(agentsPath) ? upsertRspAgentsBlock(await readFile11(agentsPath, "utf-8")).content : `# ${title}
|
|
820
1225
|
|
|
821
|
-
${
|
|
1226
|
+
${renderRspAgentsBlock()}
|
|
1227
|
+
`;
|
|
1228
|
+
console.log(`
|
|
1229
|
+
${managed}
|
|
822
1230
|
`);
|
|
823
|
-
created = true;
|
|
824
1231
|
}
|
|
825
1232
|
if (isNew) {
|
|
826
1233
|
console.log(`
|
|
@@ -829,9 +1236,11 @@ ${AGENTS_BODY}
|
|
|
829
1236
|
console.log(` Created: .rsp/rules/
|
|
830
1237
|
.rsp/specs/
|
|
831
1238
|
.rsp/features/
|
|
832
|
-
.rsp/
|
|
1239
|
+
.rsp/active.d/
|
|
1240
|
+
.rsp/archives/
|
|
833
1241
|
.rsp/.gitignore
|
|
834
1242
|
.rsp/config.yaml
|
|
1243
|
+
.rsp/specs/design.md
|
|
835
1244
|
AGENTS.md
|
|
836
1245
|
`);
|
|
837
1246
|
console.log(` ${pc.cyan("Next:")} rsp new <name>
|
|
@@ -850,34 +1259,44 @@ ${AGENTS_BODY}
|
|
|
850
1259
|
}
|
|
851
1260
|
|
|
852
1261
|
// src/commands/new-feature.ts
|
|
853
|
-
import { existsSync as
|
|
854
|
-
import { mkdir as
|
|
855
|
-
import { dirname as
|
|
1262
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1263
|
+
import { mkdir as mkdir6, writeFile as writeFile7 } from "fs/promises";
|
|
1264
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
856
1265
|
async function newFeature(name, summary = "") {
|
|
1266
|
+
if (!name) {
|
|
1267
|
+
console.error(` ${pc.red("Usage:")} rsp new <name> [summary]`);
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
}
|
|
1270
|
+
if (/^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(name) === false) {
|
|
1271
|
+
console.error(` ${pc.red("Error:")} feature name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
if (name === "init") {
|
|
1275
|
+
console.error(` ${pc.red("Error:")} "init" is a reserved workflow name, not a feature name`);
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1278
|
+
const rspRulesPath = join12(RSP_DIR, "rules", "rsp-rules.md");
|
|
1279
|
+
const designPath = join12(RSP_DIR, "specs", "design.md");
|
|
1280
|
+
if (!existsSync12(rspRulesPath) || !existsSync12(designPath)) {
|
|
1281
|
+
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
1282
|
+
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
1283
|
+
process.exit(1);
|
|
1284
|
+
}
|
|
857
1285
|
return withRspLock("new-feature", async () => {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
862
|
-
if (/^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(name) === false) {
|
|
863
|
-
console.error(` ${pc.red("Error:")} feature name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
864
|
-
process.exit(1);
|
|
865
|
-
}
|
|
866
|
-
const featurePath = join8(RSP_DIR, "features", `${name}.md`);
|
|
867
|
-
await mkdir4(dirname4(featurePath), { recursive: true });
|
|
868
|
-
const existed = existsSync8(featurePath);
|
|
1286
|
+
const featurePath = join12(RSP_DIR, "features", `${name}.md`);
|
|
1287
|
+
await mkdir6(dirname6(featurePath), { recursive: true });
|
|
1288
|
+
const existed = existsSync12(featurePath);
|
|
869
1289
|
if (!existed) {
|
|
870
1290
|
const content = generateFeatureContent(name, summary);
|
|
871
|
-
await
|
|
1291
|
+
await writeFile7(featurePath, content);
|
|
872
1292
|
}
|
|
873
|
-
await
|
|
874
|
-
const activeEntry =
|
|
875
|
-
await
|
|
876
|
-
await
|
|
1293
|
+
await mkdir6(ACTIVE_DIR, { recursive: true });
|
|
1294
|
+
const activeEntry = join12(ACTIVE_DIR, name);
|
|
1295
|
+
await mkdir6(dirname6(activeEntry), { recursive: true });
|
|
1296
|
+
await writeFile7(activeEntry, "");
|
|
877
1297
|
if (!existed) {
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
if (existsSync8(archiveIndex))
|
|
1298
|
+
const archiveIndex = join12(ARCHIVES_DIR, "INDEX.md");
|
|
1299
|
+
if (existsSync12(archiveIndex))
|
|
881
1300
|
await buildArchiveIndex({ acquireLock: false });
|
|
882
1301
|
}
|
|
883
1302
|
const label = existed ? "Using" : pc.green("Created");
|
|
@@ -888,27 +1307,27 @@ async function newFeature(name, summary = "") {
|
|
|
888
1307
|
}
|
|
889
1308
|
|
|
890
1309
|
// src/commands/status.ts
|
|
891
|
-
import { existsSync as
|
|
892
|
-
import { readFile as
|
|
893
|
-
import { join as
|
|
1310
|
+
import { existsSync as existsSync13 } from "fs";
|
|
1311
|
+
import { readFile as readFile12, stat as stat2 } from "fs/promises";
|
|
1312
|
+
import { join as join13, relative as relative6 } from "path";
|
|
894
1313
|
var COL_FEATURE = 30;
|
|
895
1314
|
var COL_STATUS = 16;
|
|
896
1315
|
var COL_AGE = 7;
|
|
897
1316
|
var COL_PROGRESS = 10;
|
|
898
|
-
async function showStatus() {
|
|
899
|
-
const featuresDir =
|
|
1317
|
+
async function showStatus(options = {}) {
|
|
1318
|
+
const featuresDir = join13(RSP_DIR, "features");
|
|
900
1319
|
const activeSet = /* @__PURE__ */ new Set();
|
|
901
|
-
if (
|
|
1320
|
+
if (existsSync13(ACTIVE_DIR)) {
|
|
902
1321
|
try {
|
|
903
1322
|
const entries = await walkFiles(ACTIVE_DIR);
|
|
904
1323
|
for (const entryPath of entries) {
|
|
905
|
-
const name =
|
|
1324
|
+
const name = relative6(ACTIVE_DIR, entryPath);
|
|
906
1325
|
activeSet.add(name);
|
|
907
1326
|
}
|
|
908
1327
|
} catch {
|
|
909
1328
|
}
|
|
910
1329
|
}
|
|
911
|
-
const featureFiles =
|
|
1330
|
+
const featureFiles = existsSync13(featuresDir) ? await walkMarkdownFiles(featuresDir) : [];
|
|
912
1331
|
const featureMap = {};
|
|
913
1332
|
for (const fp of featureFiles) {
|
|
914
1333
|
const name = featureNameFromPath(featuresDir, fp);
|
|
@@ -941,7 +1360,7 @@ async function showStatus() {
|
|
|
941
1360
|
let isBlocked = false;
|
|
942
1361
|
if (fp) {
|
|
943
1362
|
try {
|
|
944
|
-
const content = await
|
|
1363
|
+
const content = await readFile12(fp, "utf-8");
|
|
945
1364
|
const fm = parseFrontmatter(content);
|
|
946
1365
|
status = fm?.status || "\u2014";
|
|
947
1366
|
isBlocked = status === "blocked";
|
|
@@ -960,11 +1379,28 @@ async function showStatus() {
|
|
|
960
1379
|
}
|
|
961
1380
|
records.push({ name, status, progress, age, depends, isActive, isBlocked });
|
|
962
1381
|
}
|
|
1382
|
+
const filteredRecords = records.filter((record) => {
|
|
1383
|
+
if (options.active && !record.isActive)
|
|
1384
|
+
return false;
|
|
1385
|
+
if (options.blocked && !record.isBlocked)
|
|
1386
|
+
return false;
|
|
1387
|
+
if (typeof options.stale === "number") {
|
|
1388
|
+
const ageDays = Number(record.age);
|
|
1389
|
+
if (!Number.isFinite(ageDays) || ageDays < options.stale)
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
return true;
|
|
1393
|
+
});
|
|
1394
|
+
if (filteredRecords.length === 0) {
|
|
1395
|
+
console.log(` ${pc.dim("No features match the current filters.")}`);
|
|
1396
|
+
console.log();
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
963
1399
|
const pad = (s, w) => s.padEnd(w);
|
|
964
1400
|
console.log(` ${pad("Feature", COL_FEATURE)} ${pad("Status", COL_STATUS)} ${pad("Age(d)", COL_AGE)} ${pad("Progress", COL_PROGRESS)} Depends On`);
|
|
965
1401
|
console.log(` ${"\u2500".repeat(COL_FEATURE)} ${"\u2500".repeat(COL_STATUS)} ${"\u2500".repeat(COL_AGE)} ${"\u2500".repeat(COL_PROGRESS)} ${"\u2500".repeat(30)}`);
|
|
966
1402
|
let blockedCount = 0;
|
|
967
|
-
for (const r of
|
|
1403
|
+
for (const r of filteredRecords) {
|
|
968
1404
|
const marker = r.isActive ? pc.cyan("*") : " ";
|
|
969
1405
|
const namePadded = r.name.padEnd(COL_FEATURE - 1);
|
|
970
1406
|
const statusPadded = String(r.status).padEnd(COL_STATUS);
|
|
@@ -978,24 +1414,25 @@ async function showStatus() {
|
|
|
978
1414
|
blockedCount++;
|
|
979
1415
|
}
|
|
980
1416
|
console.log();
|
|
981
|
-
const total =
|
|
982
|
-
const doneCount =
|
|
983
|
-
|
|
1417
|
+
const total = filteredRecords.length;
|
|
1418
|
+
const doneCount = filteredRecords.filter((r) => r.status === "done").length;
|
|
1419
|
+
const activeCount = filteredRecords.filter((r) => r.isActive).length;
|
|
1420
|
+
console.log(` ${pc.bold("Summary:")} ${total} feature(s), ${activeCount} active, ${doneCount} done, ${pc.yellow(String(blockedCount))} blocked`);
|
|
984
1421
|
console.log();
|
|
985
1422
|
await showArchiveTrend();
|
|
986
1423
|
if (blockedCount > 0) {
|
|
987
|
-
const blocked =
|
|
1424
|
+
const blocked = filteredRecords.filter((r) => r.isBlocked);
|
|
988
1425
|
console.log(` ${pc.yellow("Blocked:")} ${blocked.map((r) => r.name).join(", ")}`);
|
|
989
1426
|
console.log();
|
|
990
1427
|
}
|
|
991
1428
|
}
|
|
992
1429
|
async function showArchiveTrend() {
|
|
993
|
-
const archiveDir =
|
|
994
|
-
const indexPath =
|
|
995
|
-
if (!
|
|
1430
|
+
const archiveDir = join13(RSP_DIR, "archives");
|
|
1431
|
+
const indexPath = join13(archiveDir, "INDEX.md");
|
|
1432
|
+
if (!existsSync13(indexPath))
|
|
996
1433
|
return;
|
|
997
1434
|
try {
|
|
998
|
-
const content = await
|
|
1435
|
+
const content = await readFile12(indexPath, "utf-8");
|
|
999
1436
|
const dateRe = /^\| (\d{4}-\d{2})-\d{2} \|/gm;
|
|
1000
1437
|
const counts = {};
|
|
1001
1438
|
let match;
|
|
@@ -1023,8 +1460,23 @@ var initCommand = defineCommand({
|
|
|
1023
1460
|
name: "init",
|
|
1024
1461
|
description: "Scaffold .rsp/ + AGENTS.md in current project"
|
|
1025
1462
|
},
|
|
1026
|
-
|
|
1027
|
-
|
|
1463
|
+
args: {
|
|
1464
|
+
"agents-mode": {
|
|
1465
|
+
type: "string",
|
|
1466
|
+
description: "How to update AGENTS.md: managed, skip, or print",
|
|
1467
|
+
default: "managed"
|
|
1468
|
+
},
|
|
1469
|
+
"with-project-rules": {
|
|
1470
|
+
type: "boolean",
|
|
1471
|
+
description: "Create .rsp/rules/project-rules.md",
|
|
1472
|
+
default: false
|
|
1473
|
+
}
|
|
1474
|
+
},
|
|
1475
|
+
async run({ args }) {
|
|
1476
|
+
await initProject({
|
|
1477
|
+
agentsMode: args["agents-mode"] === "skip" || args["agents-mode"] === "print" ? args["agents-mode"] : "managed",
|
|
1478
|
+
withProjectRules: Boolean(args["with-project-rules"])
|
|
1479
|
+
});
|
|
1028
1480
|
}
|
|
1029
1481
|
});
|
|
1030
1482
|
var newFeatureCommand = defineCommand({
|
|
@@ -1044,10 +1496,52 @@ var newFeatureCommand = defineCommand({
|
|
|
1044
1496
|
await newFeature(args.name, summary);
|
|
1045
1497
|
}
|
|
1046
1498
|
});
|
|
1499
|
+
var addRulesCommand = defineCommand({
|
|
1500
|
+
meta: {
|
|
1501
|
+
name: "rules",
|
|
1502
|
+
description: "Create .rsp/rules/<name>.md"
|
|
1503
|
+
},
|
|
1504
|
+
args: {
|
|
1505
|
+
name: {
|
|
1506
|
+
type: "positional",
|
|
1507
|
+
description: "Rules file name",
|
|
1508
|
+
required: true
|
|
1509
|
+
}
|
|
1510
|
+
},
|
|
1511
|
+
async run({ args }) {
|
|
1512
|
+
await addRules(args.name);
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
var addSpecCommand = defineCommand({
|
|
1516
|
+
meta: {
|
|
1517
|
+
name: "spec",
|
|
1518
|
+
description: "Create .rsp/specs/<name>.md and rebuild specs index"
|
|
1519
|
+
},
|
|
1520
|
+
args: {
|
|
1521
|
+
name: {
|
|
1522
|
+
type: "positional",
|
|
1523
|
+
description: "Spec file name",
|
|
1524
|
+
required: true
|
|
1525
|
+
}
|
|
1526
|
+
},
|
|
1527
|
+
async run({ args }) {
|
|
1528
|
+
await addSpec(args.name);
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
var addCommand = defineCommand({
|
|
1532
|
+
meta: {
|
|
1533
|
+
name: "add",
|
|
1534
|
+
description: "Add optional rules or spec files"
|
|
1535
|
+
},
|
|
1536
|
+
subCommands: {
|
|
1537
|
+
rules: addRulesCommand,
|
|
1538
|
+
spec: addSpecCommand
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1047
1541
|
var closeFeatureCommand = defineCommand({
|
|
1048
1542
|
meta: {
|
|
1049
1543
|
name: "close",
|
|
1050
|
-
description: "Archive to .rsp/
|
|
1544
|
+
description: "Archive to .rsp/archives/"
|
|
1051
1545
|
},
|
|
1052
1546
|
args: {
|
|
1053
1547
|
name: {
|
|
@@ -1065,8 +1559,33 @@ var statusCommand = defineCommand({
|
|
|
1065
1559
|
name: "status",
|
|
1066
1560
|
description: "Show project status summary"
|
|
1067
1561
|
},
|
|
1068
|
-
|
|
1069
|
-
|
|
1562
|
+
args: {
|
|
1563
|
+
active: {
|
|
1564
|
+
type: "boolean",
|
|
1565
|
+
description: "Show only active features",
|
|
1566
|
+
default: false
|
|
1567
|
+
},
|
|
1568
|
+
blocked: {
|
|
1569
|
+
type: "boolean",
|
|
1570
|
+
description: "Show only blocked features",
|
|
1571
|
+
default: false
|
|
1572
|
+
},
|
|
1573
|
+
stale: {
|
|
1574
|
+
type: "string",
|
|
1575
|
+
description: "Show only features with age >= days"
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
async run({ args }) {
|
|
1579
|
+
const stale = args.stale === void 0 ? void 0 : Number(args.stale);
|
|
1580
|
+
if (args.stale !== void 0 && (Number.isFinite(stale) === false || Number.isInteger(stale) === false || stale < 0)) {
|
|
1581
|
+
console.error(` Error: --stale must be a non-negative integer number of days`);
|
|
1582
|
+
process.exit(1);
|
|
1583
|
+
}
|
|
1584
|
+
await showStatus({
|
|
1585
|
+
active: Boolean(args.active),
|
|
1586
|
+
blocked: Boolean(args.blocked),
|
|
1587
|
+
stale
|
|
1588
|
+
});
|
|
1070
1589
|
}
|
|
1071
1590
|
});
|
|
1072
1591
|
var checkCommand = defineCommand({
|
|
@@ -1090,21 +1609,53 @@ var depsCommand = defineCommand({
|
|
|
1090
1609
|
type: "boolean",
|
|
1091
1610
|
description: "Output as Mermaid.js graph",
|
|
1092
1611
|
default: false
|
|
1612
|
+
},
|
|
1613
|
+
focus: {
|
|
1614
|
+
type: "string",
|
|
1615
|
+
description: "Show only a feature and its direct dependency neighborhood"
|
|
1616
|
+
},
|
|
1617
|
+
reverse: {
|
|
1618
|
+
type: "string",
|
|
1619
|
+
description: "Show only features that depend on the given feature"
|
|
1093
1620
|
}
|
|
1094
1621
|
},
|
|
1095
1622
|
async run({ args }) {
|
|
1096
|
-
await showDependencies(
|
|
1623
|
+
await showDependencies({
|
|
1624
|
+
mermaid: Boolean(args.mermaid),
|
|
1625
|
+
focus: typeof args.focus === "string" ? args.focus : void 0,
|
|
1626
|
+
reverse: typeof args.reverse === "string" ? args.reverse : void 0
|
|
1627
|
+
});
|
|
1097
1628
|
}
|
|
1098
1629
|
});
|
|
1099
1630
|
var archiveIndexCommand = defineCommand({
|
|
1100
1631
|
meta: {
|
|
1101
1632
|
name: "archive-index",
|
|
1102
|
-
description: "Regenerate
|
|
1633
|
+
description: "Regenerate archives INDEX.md"
|
|
1103
1634
|
},
|
|
1104
1635
|
async run() {
|
|
1105
1636
|
await buildArchiveIndex();
|
|
1106
1637
|
}
|
|
1107
1638
|
});
|
|
1639
|
+
var specsIndexCommand = defineCommand({
|
|
1640
|
+
meta: {
|
|
1641
|
+
name: "specs-index",
|
|
1642
|
+
description: "Regenerate specs INDEX.md"
|
|
1643
|
+
},
|
|
1644
|
+
async run() {
|
|
1645
|
+
await buildSpecsIndex();
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
var doctorCommand = defineCommand({
|
|
1649
|
+
meta: {
|
|
1650
|
+
name: "doctor",
|
|
1651
|
+
description: "Check RSP setup health and common integration issues"
|
|
1652
|
+
},
|
|
1653
|
+
async run() {
|
|
1654
|
+
const issues = await runDoctor();
|
|
1655
|
+
if (issues > 0)
|
|
1656
|
+
process.exit(1);
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1108
1659
|
async function runCli(rawArgs = process.argv.slice(2)) {
|
|
1109
1660
|
const version = await getVersion();
|
|
1110
1661
|
const main = defineCommand({
|
|
@@ -1115,12 +1666,15 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
1115
1666
|
},
|
|
1116
1667
|
subCommands: {
|
|
1117
1668
|
"init": initCommand,
|
|
1669
|
+
"add": addCommand,
|
|
1118
1670
|
"new": newFeatureCommand,
|
|
1119
1671
|
"close": closeFeatureCommand,
|
|
1120
1672
|
"status": statusCommand,
|
|
1121
1673
|
"check": checkCommand,
|
|
1122
1674
|
"deps": depsCommand,
|
|
1123
|
-
"archive-index": archiveIndexCommand
|
|
1675
|
+
"archive-index": archiveIndexCommand,
|
|
1676
|
+
"specs-index": specsIndexCommand,
|
|
1677
|
+
"doctor": doctorCommand
|
|
1124
1678
|
}
|
|
1125
1679
|
});
|
|
1126
1680
|
await runMain(main, { rawArgs });
|