@oevery/rsp 1.0.3 → 1.2.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 +120 -96
- package/README.zh-CN.md +120 -93
- package/dist/cli.mjs +853 -255
- package/package.json +1 -1
- package/rules/rsp-rules.md +68 -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));
|
|
@@ -106,17 +112,51 @@ function featureNameFromPath(featuresDir, filePath) {
|
|
|
106
112
|
return rel.replace(/\.md$/, "");
|
|
107
113
|
}
|
|
108
114
|
function generateFeatureContent(name, summary = "") {
|
|
115
|
+
if (name === "project-setup") {
|
|
116
|
+
return `---
|
|
117
|
+
status: draft
|
|
118
|
+
priority: medium
|
|
119
|
+
tags:
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
# Feature: project-setup
|
|
123
|
+
|
|
124
|
+
## Spec
|
|
125
|
+
- Summary: ${summary || "Capture the project model, boundaries, and stable local constraints"}
|
|
126
|
+
- Requirements:
|
|
127
|
+
- Project purpose, scope, and structure are reflected in .rsp/specs/design.md
|
|
128
|
+
- Stable validation or workflow constraints are reflected in .rsp/rules/project-rules.md when needed
|
|
129
|
+
- Constraints:
|
|
130
|
+
- Avoid duplicating durable project facts across feature notes, specs, and rules
|
|
131
|
+
|
|
132
|
+
## Plan
|
|
133
|
+
- [ ] Review the repository structure, entrypoints, and primary outputs
|
|
134
|
+
- [ ] Fill .rsp/specs/design.md with durable architecture facts
|
|
135
|
+
- [ ] Add .rsp/rules/project-rules.md if stable local rules or validation steps exist
|
|
136
|
+
|
|
137
|
+
## Tests
|
|
138
|
+
- [ ] Run rsp doctor
|
|
139
|
+
- [ ] Run project-native validation if available
|
|
140
|
+
|
|
141
|
+
## Notes (optional)
|
|
142
|
+
- <facts discovered during project setup>
|
|
143
|
+
|
|
144
|
+
## Blockers
|
|
145
|
+
-
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
109
148
|
return `---
|
|
110
149
|
status: draft
|
|
111
150
|
priority: medium
|
|
112
151
|
tags:
|
|
113
152
|
---
|
|
153
|
+
|
|
114
154
|
# Feature: ${name}
|
|
115
155
|
|
|
116
156
|
## Spec
|
|
117
157
|
- Summary: ${summary || "<one-line summary>"}
|
|
118
158
|
- Requirements:
|
|
119
|
-
-
|
|
159
|
+
- <verifiable requirement>
|
|
120
160
|
- Constraints:
|
|
121
161
|
-
|
|
122
162
|
|
|
@@ -134,6 +174,111 @@ tags:
|
|
|
134
174
|
-
|
|
135
175
|
`;
|
|
136
176
|
}
|
|
177
|
+
function renderRspAgentsBlock() {
|
|
178
|
+
return `${RSP_AGENTS_BEGIN}
|
|
179
|
+
## RSP Entry
|
|
180
|
+
|
|
181
|
+
Read in order:
|
|
182
|
+
1. .rsp/rules/*.md
|
|
183
|
+
2. .rsp/specs/INDEX.md
|
|
184
|
+
3. .rsp/specs/design.md
|
|
185
|
+
4. .rsp/active.d/ and matching .rsp/features/*.md
|
|
186
|
+
|
|
187
|
+
Guidelines:
|
|
188
|
+
- .rsp/rules/rsp-rules.md is required
|
|
189
|
+
- .rsp/rules/project-rules.md is optional
|
|
190
|
+
- Keep project design in .rsp/specs/
|
|
191
|
+
- Add extra spec files only when they have durable value
|
|
192
|
+
${RSP_AGENTS_END}`;
|
|
193
|
+
}
|
|
194
|
+
function upsertRspAgentsBlock(content) {
|
|
195
|
+
const block = renderRspAgentsBlock();
|
|
196
|
+
const managedRe = /<!-- rsp:begin -->[\s\S]*?<!-- rsp:end -->/;
|
|
197
|
+
if (managedRe.test(content)) {
|
|
198
|
+
const next2 = content.replace(managedRe, block);
|
|
199
|
+
return { content: next2, changed: next2 !== content };
|
|
200
|
+
}
|
|
201
|
+
const trimmed = content.trimStart();
|
|
202
|
+
const next = trimmed ? `${block}
|
|
203
|
+
|
|
204
|
+
${trimmed}` : `${block}
|
|
205
|
+
`;
|
|
206
|
+
return { content: next, changed: true };
|
|
207
|
+
}
|
|
208
|
+
function hasRspAgentsBlock(content) {
|
|
209
|
+
return content.includes(RSP_AGENTS_BEGIN) && content.includes(RSP_AGENTS_END);
|
|
210
|
+
}
|
|
211
|
+
function generateDesignContent(projectName) {
|
|
212
|
+
return `# Project Design: ${projectName}
|
|
213
|
+
|
|
214
|
+
## Purpose
|
|
215
|
+
- <what this project is responsible for>
|
|
216
|
+
- <who or what it serves>
|
|
217
|
+
|
|
218
|
+
## Scope
|
|
219
|
+
- In scope:
|
|
220
|
+
- <major capability or boundary>
|
|
221
|
+
- Out of scope:
|
|
222
|
+
- <intentional non-goal>
|
|
223
|
+
|
|
224
|
+
## Structure
|
|
225
|
+
- <important directory or subsystem> \u2014 <responsibility>
|
|
226
|
+
|
|
227
|
+
## Constraints
|
|
228
|
+
- <cross-cutting technical or operational constraint, when it affects architecture>
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
function generateProjectRulesContent(projectName) {
|
|
232
|
+
return `---
|
|
233
|
+
name: project-rules
|
|
234
|
+
description: Project-specific rules for ${projectName}
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
# Project Rules
|
|
238
|
+
|
|
239
|
+
## Scope
|
|
240
|
+
- <stable local rules, workflow constraints, or validation expectations>
|
|
241
|
+
|
|
242
|
+
## Validation
|
|
243
|
+
- <preferred stable validation commands, not temporary troubleshooting steps>
|
|
244
|
+
|
|
245
|
+
## Conventions
|
|
246
|
+
- <project-specific conventions>
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
function generateRulesContent(name) {
|
|
250
|
+
const title = toTitleCase(name);
|
|
251
|
+
return `---
|
|
252
|
+
name: ${name}
|
|
253
|
+
description: Project-specific rules for ${title}
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
# ${title}
|
|
257
|
+
|
|
258
|
+
## Scope
|
|
259
|
+
- <what this rules file governs>
|
|
260
|
+
|
|
261
|
+
## Rules
|
|
262
|
+
- <stable local rule>
|
|
263
|
+
|
|
264
|
+
## Validation
|
|
265
|
+
- <relevant validation command or check>
|
|
266
|
+
`;
|
|
267
|
+
}
|
|
268
|
+
function generateSpecContent(name) {
|
|
269
|
+
const title = toTitleCase(name);
|
|
270
|
+
return `# ${title}
|
|
271
|
+
|
|
272
|
+
## Purpose
|
|
273
|
+
- <why this project-level spec exists>
|
|
274
|
+
|
|
275
|
+
## Details
|
|
276
|
+
- <important project-level detail>
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
function toTitleCase(value) {
|
|
280
|
+
return value.split(/[-/]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
281
|
+
}
|
|
137
282
|
function detectDeltaSections(content) {
|
|
138
283
|
const match = content.match(/^## Spec\n([\s\S]*?)(?=\n## |\n---|\n\.\.\.)/m);
|
|
139
284
|
if (!match) {
|
|
@@ -204,6 +349,7 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
204
349
|
var PKG_ROOT = join2(__dirname, "..");
|
|
205
350
|
var RSP_DIR = ".rsp";
|
|
206
351
|
var ACTIVE_DIR = join2(RSP_DIR, "active.d");
|
|
352
|
+
var ARCHIVES_DIR = join2(RSP_DIR, "archives");
|
|
207
353
|
var LOCK_PATH = join2(RSP_DIR, ".lock");
|
|
208
354
|
var CONFIG_PATH = join2(RSP_DIR, "config.yaml");
|
|
209
355
|
var VALID_STATUSES = ["draft", "ready", "in-progress", "blocked", "done"];
|
|
@@ -295,40 +441,204 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
295
441
|
}
|
|
296
442
|
}
|
|
297
443
|
|
|
298
|
-
// src/commands/
|
|
299
|
-
|
|
444
|
+
// src/commands/add-rules.ts
|
|
445
|
+
var NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
446
|
+
async function detectProjectName() {
|
|
447
|
+
try {
|
|
448
|
+
if (existsSync3("package.json")) {
|
|
449
|
+
const raw = await readFile3("package.json", "utf-8");
|
|
450
|
+
const pkg = JSON.parse(raw);
|
|
451
|
+
if (pkg.name && pkg.name !== "")
|
|
452
|
+
return pkg.name;
|
|
453
|
+
}
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
return basename(process.cwd());
|
|
457
|
+
}
|
|
458
|
+
async function addRules(name, projectName = "project") {
|
|
459
|
+
if (!name || !NAME_RE.test(name)) {
|
|
460
|
+
console.error(` ${pc.red("Error:")} rule name must be kebab-case with optional subdirectory`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
const rspRulesPath = join3(RSP_DIR, "rules", "rsp-rules.md");
|
|
464
|
+
const designPath = join3(RSP_DIR, "specs", "design.md");
|
|
465
|
+
if (!existsSync3(rspRulesPath) || !existsSync3(designPath)) {
|
|
466
|
+
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
467
|
+
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
return withRspLock("add-rules", async () => {
|
|
471
|
+
const dir = join3(RSP_DIR, "rules");
|
|
472
|
+
const path = join3(dir, `${name}.md`);
|
|
473
|
+
if (existsSync3(path)) {
|
|
474
|
+
console.error(` ${pc.red("Error:")} rule file already exists: .rsp/rules/${name}.md`);
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
478
|
+
const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
|
|
479
|
+
const content = name === "project-rules" ? generateProjectRulesContent(resolvedProjectName) : generateRulesContent(name);
|
|
480
|
+
await writeFile2(path, content);
|
|
481
|
+
console.log(` ${pc.green("Created:")} .rsp/rules/${name}.md
|
|
482
|
+
`);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/commands/add-spec.ts
|
|
487
|
+
import { existsSync as existsSync5 } from "fs";
|
|
488
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
489
|
+
import { basename as basename3, dirname as dirname3, join as join5 } from "path";
|
|
490
|
+
|
|
491
|
+
// src/commands/specs-index.ts
|
|
492
|
+
import { existsSync as existsSync4 } from "fs";
|
|
493
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
494
|
+
import { basename as basename2, join as join4, relative as relative2 } from "path";
|
|
495
|
+
async function buildSpecsIndex({ acquireLock = true } = {}) {
|
|
300
496
|
if (acquireLock)
|
|
301
|
-
return withRspLock("
|
|
302
|
-
const
|
|
303
|
-
if (!
|
|
304
|
-
console.log(` ${pc.dim("No
|
|
497
|
+
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false }));
|
|
498
|
+
const specsDir = join4(RSP_DIR, "specs");
|
|
499
|
+
if (!existsSync4(specsDir)) {
|
|
500
|
+
console.log(` ${pc.dim("No specs directory.")}
|
|
305
501
|
`);
|
|
306
502
|
return;
|
|
307
503
|
}
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
504
|
+
const specFiles = (await walkMarkdownFiles(specsDir)).filter((fp) => basename2(fp) !== "INDEX.md").sort();
|
|
505
|
+
const lines = [];
|
|
506
|
+
lines.push("# Specs Index");
|
|
507
|
+
lines.push("");
|
|
508
|
+
lines.push("_Project-level specs and design notes._");
|
|
509
|
+
lines.push("");
|
|
510
|
+
if (specFiles.length === 0) {
|
|
511
|
+
lines.push("_No project-level specs yet._");
|
|
512
|
+
} else {
|
|
513
|
+
lines.push("| File | Title | Summary |");
|
|
514
|
+
lines.push("|------|-------|---------|");
|
|
515
|
+
for (const fp of specFiles) {
|
|
516
|
+
const rel = relative2(specsDir, fp);
|
|
517
|
+
const content = await readFile4(fp, "utf-8");
|
|
518
|
+
const fm = parseFrontmatter(content);
|
|
519
|
+
const title = extractTitle(content) || rel.replace(/\.md$/, "");
|
|
520
|
+
const summary = extractSummary(content, fm?.summary);
|
|
521
|
+
lines.push(`| ${escapeCell(rel)} | ${escapeCell(title)} | ${escapeCell(summary || "\u2014")} |`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
lines.push("");
|
|
525
|
+
lines.push("_Auto-generated by `rsp specs-index`._");
|
|
526
|
+
await writeFile3(join4(specsDir, "INDEX.md"), lines.join("\n"));
|
|
527
|
+
console.log(` ${pc.green("INDEX.md updated:")} ${specFiles.length} spec file(s).
|
|
528
|
+
`);
|
|
529
|
+
}
|
|
530
|
+
function extractTitle(content) {
|
|
531
|
+
for (const rawLine of content.split("\n")) {
|
|
532
|
+
if (!rawLine.startsWith("# "))
|
|
533
|
+
continue;
|
|
534
|
+
return rawLine.slice(2).trim();
|
|
535
|
+
}
|
|
536
|
+
return "";
|
|
537
|
+
}
|
|
538
|
+
function extractSummary(content, frontmatterSummary) {
|
|
539
|
+
if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
|
|
540
|
+
return frontmatterSummary.trim();
|
|
541
|
+
const lines = content.split("\n");
|
|
542
|
+
let inFrontmatter = false;
|
|
543
|
+
for (const rawLine of lines) {
|
|
544
|
+
const line = rawLine.trim();
|
|
545
|
+
if (line === "---" || line === "...") {
|
|
546
|
+
inFrontmatter = !inFrontmatter;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (inFrontmatter || line === "")
|
|
550
|
+
continue;
|
|
551
|
+
if (line.startsWith("#"))
|
|
552
|
+
continue;
|
|
553
|
+
if (line.startsWith("##"))
|
|
554
|
+
continue;
|
|
555
|
+
return line.replace(/^[-*]\s*/, "").trim();
|
|
556
|
+
}
|
|
557
|
+
return "";
|
|
558
|
+
}
|
|
559
|
+
function escapeCell(value) {
|
|
560
|
+
return value.replace(/\|/g, "\\|");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/commands/add-spec.ts
|
|
564
|
+
var NAME_RE2 = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
565
|
+
async function detectProjectName2() {
|
|
566
|
+
try {
|
|
567
|
+
if (existsSync5("package.json")) {
|
|
568
|
+
const raw = await readFile5("package.json", "utf-8");
|
|
569
|
+
const pkg = JSON.parse(raw);
|
|
570
|
+
if (pkg.name && pkg.name !== "")
|
|
571
|
+
return pkg.name;
|
|
572
|
+
}
|
|
573
|
+
} catch {
|
|
574
|
+
}
|
|
575
|
+
return basename3(process.cwd());
|
|
576
|
+
}
|
|
577
|
+
async function addSpec(name, projectName = "project") {
|
|
578
|
+
if (!name || !NAME_RE2.test(name)) {
|
|
579
|
+
console.error(` ${pc.red("Error:")} spec name must be kebab-case with optional subdirectory`);
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
const rspRulesPath = join5(RSP_DIR, "rules", "rsp-rules.md");
|
|
583
|
+
const designPath = join5(RSP_DIR, "specs", "design.md");
|
|
584
|
+
if (!existsSync5(rspRulesPath) || !existsSync5(designPath)) {
|
|
585
|
+
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
586
|
+
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
return withRspLock("add-spec", async () => {
|
|
590
|
+
const dir = join5(RSP_DIR, "specs");
|
|
591
|
+
const path = join5(dir, `${name}.md`);
|
|
592
|
+
if (existsSync5(path)) {
|
|
593
|
+
console.error(` ${pc.red("Error:")} spec file already exists: .rsp/specs/${name}.md`);
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
597
|
+
const resolvedProjectName = projectName === "project" ? await detectProjectName2() : projectName;
|
|
598
|
+
const content = name === "design" ? generateDesignContent(resolvedProjectName) : generateSpecContent(name);
|
|
599
|
+
await writeFile4(path, content);
|
|
600
|
+
await buildSpecsIndex({ acquireLock: false });
|
|
601
|
+
console.log(` ${pc.green("Created:")} .rsp/specs/${name}.md
|
|
602
|
+
`);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/commands/archive-index.ts
|
|
607
|
+
import { existsSync as existsSync6 } from "fs";
|
|
608
|
+
import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
609
|
+
import { basename as basename4, dirname as dirname4, join as join6, relative as relative3 } from "path";
|
|
610
|
+
async function buildArchiveIndex({ acquireLock = true } = {}) {
|
|
611
|
+
if (acquireLock)
|
|
612
|
+
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false }));
|
|
613
|
+
const archiveDir = join6(RSP_DIR, "archives");
|
|
614
|
+
if (!existsSync6(archiveDir)) {
|
|
615
|
+
console.log(` ${pc.dim("No archives directory.")}
|
|
311
616
|
`);
|
|
312
617
|
return;
|
|
313
618
|
}
|
|
619
|
+
const archiveFiles = (await walkMarkdownFiles(archiveDir)).filter((fp) => basename4(fp) !== "INDEX.md").sort();
|
|
314
620
|
const lines = [];
|
|
315
621
|
lines.push("# Archive Index");
|
|
316
622
|
lines.push("");
|
|
317
623
|
lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
|
|
318
624
|
lines.push("");
|
|
319
|
-
|
|
320
|
-
|
|
625
|
+
if (archiveFiles.length === 0) {
|
|
626
|
+
lines.push("_No archived features yet._");
|
|
627
|
+
} else {
|
|
628
|
+
lines.push("| Date | Feature | Summary |");
|
|
629
|
+
lines.push("|------|---------|---------|");
|
|
630
|
+
}
|
|
321
631
|
for (const fp of archiveFiles) {
|
|
322
|
-
const base =
|
|
323
|
-
const archiveRel =
|
|
324
|
-
const archiveDirPart =
|
|
632
|
+
const base = basename4(fp);
|
|
633
|
+
const archiveRel = relative3(archiveDir, fp);
|
|
634
|
+
const archiveDirPart = dirname4(archiveRel);
|
|
325
635
|
const dateMatch = base.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.md$/);
|
|
326
636
|
const date = dateMatch ? dateMatch[1] : "\u2014";
|
|
327
637
|
const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
|
|
328
638
|
const featureName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
|
|
329
639
|
let summary = "";
|
|
330
640
|
try {
|
|
331
|
-
const content = await
|
|
641
|
+
const content = await readFile6(fp, "utf-8");
|
|
332
642
|
const fm = parseFrontmatter(content);
|
|
333
643
|
if (fm?.summary)
|
|
334
644
|
summary = String(fm.summary);
|
|
@@ -344,18 +654,18 @@ async function buildArchiveIndex({ acquireLock = true } = {}) {
|
|
|
344
654
|
}
|
|
345
655
|
lines.push("");
|
|
346
656
|
lines.push("_Auto-generated by `rsp archive-index`._");
|
|
347
|
-
const indexPath =
|
|
348
|
-
await
|
|
657
|
+
const indexPath = join6(archiveDir, "INDEX.md");
|
|
658
|
+
await writeFile5(indexPath, lines.join("\n"));
|
|
349
659
|
console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived feature(s).
|
|
350
660
|
`);
|
|
351
661
|
}
|
|
352
662
|
|
|
353
663
|
// src/commands/check.ts
|
|
354
|
-
import { existsSync as
|
|
355
|
-
import { readFile as
|
|
356
|
-
import { join as
|
|
664
|
+
import { existsSync as existsSync7 } from "fs";
|
|
665
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
666
|
+
import { join as join7, relative as relative4 } from "path";
|
|
357
667
|
async function runCheck() {
|
|
358
|
-
const featuresDir =
|
|
668
|
+
const featuresDir = join7(RSP_DIR, "features");
|
|
359
669
|
const config = await loadRspConfig();
|
|
360
670
|
const validStatuses = resolveStatuses(config);
|
|
361
671
|
const validPriorities = resolvePriorities(config);
|
|
@@ -365,34 +675,34 @@ async function runCheck() {
|
|
|
365
675
|
console.log();
|
|
366
676
|
console.log(` ${pc.bold("RSP check")}`);
|
|
367
677
|
console.log();
|
|
368
|
-
if (
|
|
678
|
+
if (existsSync7(ACTIVE_DIR)) {
|
|
369
679
|
const entries = await walkFiles(ACTIVE_DIR);
|
|
370
680
|
for (const entryPath of entries) {
|
|
371
|
-
const entryContent =
|
|
372
|
-
const markerContent = (await
|
|
681
|
+
const entryContent = relative4(ACTIVE_DIR, entryPath);
|
|
682
|
+
const markerContent = (await readFile7(entryPath, "utf-8")).trim();
|
|
373
683
|
if (markerContent) {
|
|
374
684
|
console.log(` ${pc.yellow("\u26A0")} active.d/${entryContent} should be an empty marker file; path is the source of truth`);
|
|
375
685
|
warnings++;
|
|
376
686
|
}
|
|
377
|
-
const fp =
|
|
378
|
-
if (!
|
|
687
|
+
const fp = join7(featuresDir, `${entryContent}.md`);
|
|
688
|
+
if (!existsSync7(fp)) {
|
|
379
689
|
console.log(` ${pc.red("\u2717")} active.d/${entryContent} points to "${entryContent}" but features/${entryContent}.md not found`);
|
|
380
690
|
errors++;
|
|
381
691
|
}
|
|
382
692
|
}
|
|
383
693
|
}
|
|
384
|
-
const featureFiles =
|
|
694
|
+
const featureFiles = existsSync7(featuresDir) ? await walkMarkdownFiles(featuresDir) : [];
|
|
385
695
|
if (featureFiles.length === 0) {
|
|
386
696
|
console.log(` ${pc.dim("No feature files to check.")}
|
|
387
697
|
`);
|
|
388
|
-
return;
|
|
698
|
+
return 0;
|
|
389
699
|
}
|
|
390
700
|
const allDeps = /* @__PURE__ */ new Map();
|
|
391
701
|
for (const fp of featureFiles) {
|
|
392
702
|
const name = featureNameFromPath(featuresDir, fp);
|
|
393
703
|
let content;
|
|
394
704
|
try {
|
|
395
|
-
content = await
|
|
705
|
+
content = await readFile7(fp, "utf-8");
|
|
396
706
|
} catch {
|
|
397
707
|
console.log(` ${pc.red("\u2717")} ${name} \u2014 unable to read file`);
|
|
398
708
|
errors++;
|
|
@@ -487,8 +797,8 @@ async function runCheck() {
|
|
|
487
797
|
}
|
|
488
798
|
seen.add(dep);
|
|
489
799
|
allDeps.set(name, [...allDeps.get(name) || [], dep]);
|
|
490
|
-
const depFp =
|
|
491
|
-
if (!
|
|
800
|
+
const depFp = join7(featuresDir, `${dep}.md`);
|
|
801
|
+
if (!existsSync7(depFp)) {
|
|
492
802
|
console.log(` ${pc.red("\u2717")} ${name} \u2014 depends on "${dep}" but features/${dep}.md not found`);
|
|
493
803
|
errors++;
|
|
494
804
|
}
|
|
@@ -511,9 +821,9 @@ async function runCheck() {
|
|
|
511
821
|
}
|
|
512
822
|
|
|
513
823
|
// src/commands/close-feature.ts
|
|
514
|
-
import { existsSync as
|
|
515
|
-
import {
|
|
516
|
-
import { basename as
|
|
824
|
+
import { existsSync as existsSync8 } from "fs";
|
|
825
|
+
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile8, rename, rmdir, unlink as unlink2 } from "fs/promises";
|
|
826
|
+
import { basename as basename5, dirname as dirname5, join as join8 } from "path";
|
|
517
827
|
async function closeFeature(name) {
|
|
518
828
|
return withRspLock("close-feature", async () => {
|
|
519
829
|
if (!name) {
|
|
@@ -524,128 +834,104 @@ async function closeFeature(name) {
|
|
|
524
834
|
console.error(` ${pc.red("Error:")} feature name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
525
835
|
process.exit(1);
|
|
526
836
|
}
|
|
527
|
-
const srcPath =
|
|
528
|
-
if (!
|
|
837
|
+
const srcPath = join8(RSP_DIR, "features", `${name}.md`);
|
|
838
|
+
if (!existsSync8(srcPath)) {
|
|
529
839
|
console.error(` ${pc.red("Feature not found:")} .rsp/features/${name}.md`);
|
|
530
840
|
process.exit(1);
|
|
531
841
|
}
|
|
842
|
+
const dependents = await findDependents(name);
|
|
843
|
+
if (dependents.length > 0) {
|
|
844
|
+
console.error(` ${pc.red("Error:")} cannot close "${name}" because it is still referenced by: ${dependents.join(", ")}`);
|
|
845
|
+
console.error(` ${pc.dim("Remove or update those depends-on entries before archiving this feature.")}`);
|
|
846
|
+
process.exit(1);
|
|
847
|
+
}
|
|
532
848
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
533
|
-
const dir =
|
|
534
|
-
const base =
|
|
535
|
-
const archiveSubdir = dir !== "." ?
|
|
849
|
+
const dir = dirname5(name);
|
|
850
|
+
const base = basename5(name);
|
|
851
|
+
const archiveSubdir = dir !== "." ? join8(ARCHIVES_DIR, dir) : ARCHIVES_DIR;
|
|
536
852
|
const archiveName = `${date}_${base}.md`;
|
|
537
|
-
await
|
|
538
|
-
await rename(srcPath,
|
|
853
|
+
await mkdir4(archiveSubdir, { recursive: true });
|
|
854
|
+
await rename(srcPath, join8(archiveSubdir, archiveName));
|
|
539
855
|
let activeCleared = false;
|
|
540
|
-
const activeEntry =
|
|
541
|
-
if (
|
|
856
|
+
const activeEntry = join8(ACTIVE_DIR, name);
|
|
857
|
+
if (existsSync8(activeEntry)) {
|
|
542
858
|
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
|
-
}
|
|
859
|
+
await cleanupEmptyParentDirs(activeEntry, ACTIVE_DIR);
|
|
556
860
|
activeCleared = true;
|
|
557
861
|
}
|
|
862
|
+
await cleanupEmptyParentDirs(srcPath, join8(RSP_DIR, "features"));
|
|
558
863
|
const clearedMsg = activeCleared ? ` ${pc.dim("active.d cleared")}
|
|
559
864
|
` : "";
|
|
560
865
|
console.log(` ${pc.green("Archived:")} ${archiveName}
|
|
561
866
|
${clearedMsg}`);
|
|
562
867
|
await buildArchiveIndex({ acquireLock: false });
|
|
563
868
|
await runCheckIfHasDependents(name);
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const archiveRelPath = dir !== "." ? join5(dir, archiveName) : archiveName;
|
|
869
|
+
if (existsSync8(".git")) {
|
|
870
|
+
const archiveRelPath = dir !== "." ? join8(dir, archiveName) : archiveName;
|
|
567
871
|
console.log(` ${pc.cyan("Git workflow:")}
|
|
568
872
|
`);
|
|
569
|
-
console.log(` git add .rsp/
|
|
873
|
+
console.log(` git add .rsp/archives/${archiveRelPath}`);
|
|
570
874
|
console.log(` git commit -m "feat: complete ${name}"`);
|
|
571
875
|
console.log();
|
|
572
876
|
}
|
|
573
877
|
});
|
|
574
878
|
}
|
|
879
|
+
async function findDependents(name) {
|
|
880
|
+
const featuresDir = join8(RSP_DIR, "features");
|
|
881
|
+
const allFeatureFiles = await walkMarkdownFiles(featuresDir);
|
|
882
|
+
const dependents = [];
|
|
883
|
+
for (const fp of allFeatureFiles) {
|
|
884
|
+
const featureName = fp === join8(featuresDir, `${name}.md`) ? name : fp.replace(`${featuresDir}/`, "").replace(/\.md$/, "");
|
|
885
|
+
if (featureName === name)
|
|
886
|
+
continue;
|
|
887
|
+
const content = await readFile8(fp, "utf-8");
|
|
888
|
+
const fm = parseFrontmatter(content);
|
|
889
|
+
if (!fm || !fm["depends-on"])
|
|
890
|
+
continue;
|
|
891
|
+
const deps = Array.isArray(fm["depends-on"]) ? fm["depends-on"] : [fm["depends-on"]];
|
|
892
|
+
if (deps.includes(name))
|
|
893
|
+
dependents.push(featureName);
|
|
894
|
+
}
|
|
895
|
+
return dependents.sort();
|
|
896
|
+
}
|
|
575
897
|
async function runCheckIfHasDependents(name) {
|
|
576
898
|
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
|
-
}
|
|
899
|
+
const hasDependents = (await findDependents(name)).length > 0;
|
|
591
900
|
if (hasDependents)
|
|
592
901
|
await runCheck();
|
|
593
902
|
} catch {
|
|
594
903
|
console.warn(` ${pc.dim("\u26A0 could not verify dependents for")} "${name}"`);
|
|
595
904
|
}
|
|
596
905
|
}
|
|
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 () => {
|
|
603
|
-
try {
|
|
604
|
-
const entries = await walkMarkdownFiles(archiveDir);
|
|
605
|
-
return entries.find((fp) => basename2(fp).startsWith(`${date}_`) && basename2(fp).endsWith(".md"));
|
|
606
|
-
} catch {
|
|
607
|
-
return null;
|
|
608
|
-
}
|
|
609
|
-
})();
|
|
610
|
-
let summary = "\u2014";
|
|
611
|
-
if (archiveFile) {
|
|
906
|
+
async function cleanupEmptyParentDirs(path, stopDir) {
|
|
907
|
+
let parent = dirname5(path);
|
|
908
|
+
while (parent !== stopDir) {
|
|
612
909
|
try {
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
910
|
+
const remaining = await readdir2(parent);
|
|
911
|
+
if (remaining.length === 0)
|
|
912
|
+
await rmdir(parent);
|
|
913
|
+
else
|
|
914
|
+
break;
|
|
617
915
|
} catch {
|
|
916
|
+
break;
|
|
618
917
|
}
|
|
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 {
|
|
918
|
+
parent = dirname5(parent);
|
|
639
919
|
}
|
|
640
920
|
}
|
|
641
921
|
|
|
642
922
|
// src/commands/deps.ts
|
|
643
|
-
import { existsSync as
|
|
644
|
-
import { readFile as
|
|
645
|
-
import { join as
|
|
646
|
-
async function showDependencies(
|
|
647
|
-
const
|
|
648
|
-
const
|
|
923
|
+
import { existsSync as existsSync9 } from "fs";
|
|
924
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
925
|
+
import { join as join9 } from "path";
|
|
926
|
+
async function showDependencies(options = false) {
|
|
927
|
+
const resolved = typeof options === "boolean" ? { mermaid: options } : options;
|
|
928
|
+
const mermaid = Boolean(resolved.mermaid);
|
|
929
|
+
const focus = resolved.focus?.trim();
|
|
930
|
+
const reverse = resolved.reverse?.trim();
|
|
931
|
+
if (focus && reverse)
|
|
932
|
+
throw new Error("Use either --focus or --reverse, not both");
|
|
933
|
+
const featuresDir = join9(RSP_DIR, "features");
|
|
934
|
+
const featureFiles = existsSync9(featuresDir) ? await walkMarkdownFiles(featuresDir) : [];
|
|
649
935
|
console.log();
|
|
650
936
|
console.log(` ${pc.bold("RSP dependencies")}`);
|
|
651
937
|
console.log();
|
|
@@ -660,7 +946,7 @@ async function showDependencies(mermaid = false) {
|
|
|
660
946
|
const name = featureNameFromPath(featuresDir, fp);
|
|
661
947
|
let content;
|
|
662
948
|
try {
|
|
663
|
-
content = await
|
|
949
|
+
content = await readFile9(fp, "utf-8");
|
|
664
950
|
} catch {
|
|
665
951
|
continue;
|
|
666
952
|
}
|
|
@@ -674,18 +960,41 @@ async function showDependencies(mermaid = false) {
|
|
|
674
960
|
dependents.set(dep, list);
|
|
675
961
|
}
|
|
676
962
|
}
|
|
963
|
+
let visibleRecords = [...records];
|
|
964
|
+
if (focus) {
|
|
965
|
+
const related = /* @__PURE__ */ new Set([focus]);
|
|
966
|
+
const target = records.find((record) => record.name === focus);
|
|
967
|
+
if (target) {
|
|
968
|
+
for (const dep of target.deps)
|
|
969
|
+
related.add(dep);
|
|
970
|
+
}
|
|
971
|
+
for (const dependent of dependents.get(focus) || [])
|
|
972
|
+
related.add(dependent);
|
|
973
|
+
visibleRecords = records.filter((record) => related.has(record.name));
|
|
974
|
+
} else if (reverse) {
|
|
975
|
+
const related = new Set(dependents.get(reverse) || []);
|
|
976
|
+
visibleRecords = records.filter((record) => related.has(record.name));
|
|
977
|
+
}
|
|
978
|
+
if ((focus || reverse) && visibleRecords.length === 0) {
|
|
979
|
+
console.log(` ${pc.dim("No dependency matches found for the current filter.")}
|
|
980
|
+
`);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
677
983
|
if (mermaid) {
|
|
678
984
|
console.log("```mermaid");
|
|
679
985
|
console.log("graph LR");
|
|
680
986
|
const safeId = (s) => s.replace(/\//g, "__slash__").replace(/\W/g, "_");
|
|
681
|
-
for (const { name } of
|
|
987
|
+
for (const { name } of visibleRecords) {
|
|
682
988
|
const id = safeId(name);
|
|
683
989
|
const label = name.replace(/"/g, "#quot;");
|
|
684
990
|
console.log(` ${id}["${label}"]`);
|
|
685
991
|
}
|
|
686
|
-
|
|
992
|
+
const visibleNames = new Set(visibleRecords.map((record) => record.name));
|
|
993
|
+
for (const { name, deps } of visibleRecords) {
|
|
687
994
|
const fromId = safeId(name);
|
|
688
995
|
for (const dep of deps) {
|
|
996
|
+
if (!visibleNames.has(dep))
|
|
997
|
+
continue;
|
|
689
998
|
const toId = safeId(dep);
|
|
690
999
|
console.log(` ${fromId} --> ${toId}`);
|
|
691
1000
|
}
|
|
@@ -696,7 +1005,7 @@ async function showDependencies(mermaid = false) {
|
|
|
696
1005
|
}
|
|
697
1006
|
console.log(` ${"Feature".padEnd(28)} ${"Depends On".padEnd(28)} Dependents`);
|
|
698
1007
|
console.log(` ${"\u2500".repeat(28)} ${"\u2500".repeat(28)} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
699
|
-
for (const { name, deps } of
|
|
1008
|
+
for (const { name, deps } of visibleRecords.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
700
1009
|
const depText = deps.length > 0 ? deps.join(", ") : "\u2014";
|
|
701
1010
|
const dependentText = (dependents.get(name) || []).length > 0 ? pc.dim((dependents.get(name) || []).join(", ")) : "\u2014";
|
|
702
1011
|
console.log(` ${name.padEnd(28)} ${depText.padEnd(28)} ${dependentText}`);
|
|
@@ -704,36 +1013,149 @@ async function showDependencies(mermaid = false) {
|
|
|
704
1013
|
console.log();
|
|
705
1014
|
}
|
|
706
1015
|
|
|
707
|
-
// src/commands/
|
|
708
|
-
import { existsSync as
|
|
709
|
-
import {
|
|
710
|
-
import {
|
|
711
|
-
async function
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1016
|
+
// src/commands/doctor.ts
|
|
1017
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1018
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1019
|
+
import { join as join10, relative as relative5 } from "path";
|
|
1020
|
+
async function runDoctor() {
|
|
1021
|
+
let issues = 0;
|
|
1022
|
+
console.log();
|
|
1023
|
+
console.log(` ${pc.bold("RSP doctor")}`);
|
|
1024
|
+
console.log();
|
|
1025
|
+
const rspRulesPath = join10(RSP_DIR, "rules", "rsp-rules.md");
|
|
1026
|
+
const designPath = join10(RSP_DIR, "specs", "design.md");
|
|
1027
|
+
const specsIndexPath = join10(RSP_DIR, "specs", "INDEX.md");
|
|
1028
|
+
const archivesIndexPath = join10(RSP_DIR, "archives", "INDEX.md");
|
|
1029
|
+
issues += reportCheck(".rsp exists", existsSync10(RSP_DIR), "Run: rsp init");
|
|
1030
|
+
issues += reportCheck("rules/rsp-rules.md exists", existsSync10(rspRulesPath), "Run: rsp init");
|
|
1031
|
+
issues += reportCheck("specs/design.md exists", existsSync10(designPath), "Run: rsp init");
|
|
1032
|
+
issues += reportCheck("specs/INDEX.md exists", existsSync10(specsIndexPath), "Run: rsp specs-index");
|
|
1033
|
+
issues += reportCheck("archives/INDEX.md exists", existsSync10(archivesIndexPath), "Run: rsp archive-index");
|
|
1034
|
+
issues += await checkAgents();
|
|
1035
|
+
issues += await checkGeneratedIndex(specsIndexPath, "_Auto-generated by `rsp specs-index`._", "specs/INDEX.md has generated signature", "Run: rsp specs-index");
|
|
1036
|
+
issues += await checkGeneratedIndex(archivesIndexPath, "_Auto-generated by `rsp archive-index`._", "archives/INDEX.md has generated signature", "Run: rsp archive-index");
|
|
1037
|
+
issues += await checkArchiveNaming();
|
|
1038
|
+
issues += await checkConfigSemantics();
|
|
1039
|
+
issues += await checkActiveFeatureConsistency();
|
|
1040
|
+
if (issues === 0)
|
|
1041
|
+
console.log(`
|
|
1042
|
+
${pc.green("\u2713")} RSP setup looks healthy.
|
|
1043
|
+
`);
|
|
1044
|
+
else
|
|
1045
|
+
console.log(`
|
|
1046
|
+
${pc.yellow(String(issues))} issue(s) detected.
|
|
1047
|
+
`);
|
|
1048
|
+
return issues;
|
|
1049
|
+
}
|
|
1050
|
+
function reportCheck(label, ok, fixHint) {
|
|
1051
|
+
if (ok) {
|
|
1052
|
+
console.log(` ${pc.green("\u2713")} ${label}`);
|
|
1053
|
+
return 0;
|
|
720
1054
|
}
|
|
721
|
-
|
|
1055
|
+
console.log(` ${pc.yellow("!")} ${label}`);
|
|
1056
|
+
if (fixHint)
|
|
1057
|
+
console.log(` ${pc.dim(fixHint)}`);
|
|
1058
|
+
return 1;
|
|
1059
|
+
}
|
|
1060
|
+
async function checkAgents() {
|
|
1061
|
+
if (!existsSync10("AGENTS.md")) {
|
|
1062
|
+
console.log(` ${pc.yellow("!")} AGENTS.md missing`);
|
|
1063
|
+
console.log(` ${pc.dim("Run: rsp init --agents-mode managed")}`);
|
|
1064
|
+
return 1;
|
|
1065
|
+
}
|
|
1066
|
+
const agents = await readFile10("AGENTS.md", "utf-8");
|
|
1067
|
+
if (hasRspAgentsBlock(agents)) {
|
|
1068
|
+
console.log(` ${pc.green("\u2713")} AGENTS.md contains managed RSP block`);
|
|
1069
|
+
return 0;
|
|
1070
|
+
}
|
|
1071
|
+
console.log(` ${pc.yellow("!")} AGENTS.md missing managed RSP block`);
|
|
1072
|
+
console.log(` ${pc.dim("Run: rsp init --agents-mode managed")}`);
|
|
1073
|
+
return 1;
|
|
1074
|
+
}
|
|
1075
|
+
async function checkGeneratedIndex(path, signature, label, fixHint) {
|
|
1076
|
+
if (!existsSync10(path))
|
|
1077
|
+
return 0;
|
|
1078
|
+
const content = await readFile10(path, "utf-8");
|
|
1079
|
+
if (content.includes(signature)) {
|
|
1080
|
+
console.log(` ${pc.green("\u2713")} ${label}`);
|
|
1081
|
+
return 0;
|
|
1082
|
+
}
|
|
1083
|
+
console.log(` ${pc.yellow("!")} ${label}`);
|
|
1084
|
+
console.log(` ${pc.dim(fixHint)}`);
|
|
1085
|
+
return 1;
|
|
1086
|
+
}
|
|
1087
|
+
async function checkArchiveNaming() {
|
|
1088
|
+
const archivesDir = join10(RSP_DIR, "archives");
|
|
1089
|
+
const archiveFiles = existsSync10(archivesDir) ? (await walkMarkdownFiles(archivesDir)).filter((fp) => relative5(archivesDir, fp) !== "INDEX.md") : [];
|
|
1090
|
+
const invalidFiles = archiveFiles.map((fp) => relative5(archivesDir, fp)).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(fp.split("/").at(-1) || "") === false);
|
|
1091
|
+
if (invalidFiles.length === 0) {
|
|
1092
|
+
console.log(` ${pc.green("\u2713")} archived feature files follow the date_name.md convention`);
|
|
1093
|
+
return 0;
|
|
1094
|
+
}
|
|
1095
|
+
console.log(` ${pc.yellow("!")} archived feature files with invalid names: ${invalidFiles.join(", ")}`);
|
|
1096
|
+
console.log(` ${pc.dim("Rename archive files to YYYY-MM-DD_name.md and run: rsp archive-index")}`);
|
|
1097
|
+
return 1;
|
|
1098
|
+
}
|
|
1099
|
+
async function checkConfigSemantics() {
|
|
1100
|
+
if (!existsSync10(CONFIG_PATH))
|
|
1101
|
+
return 0;
|
|
1102
|
+
const content = await readFile10(CONFIG_PATH, "utf-8");
|
|
1103
|
+
const parsed = parseYamlLines(content.split("\n"));
|
|
1104
|
+
let issues = 0;
|
|
1105
|
+
issues += reportConfigListField(parsed, "statuses");
|
|
1106
|
+
issues += reportConfigListField(parsed, "priorities");
|
|
1107
|
+
issues += reportConfigListField(parsed, "required_sections");
|
|
1108
|
+
return issues;
|
|
1109
|
+
}
|
|
1110
|
+
function reportConfigListField(parsed, key) {
|
|
1111
|
+
if (!(key in parsed))
|
|
1112
|
+
return 0;
|
|
1113
|
+
const value = parsed[key];
|
|
1114
|
+
if (!Array.isArray(value)) {
|
|
1115
|
+
console.log(` ${pc.yellow("!")} config.yaml field "${key}" must be a YAML list`);
|
|
1116
|
+
console.log(` ${pc.dim(`Update .rsp/config.yaml so "${key}" uses "key:" followed by indented "- item" entries`)}`);
|
|
1117
|
+
return 1;
|
|
1118
|
+
}
|
|
1119
|
+
const items = value.map(String);
|
|
1120
|
+
const duplicates = [...new Set(items.filter((item, index) => items.indexOf(item) !== index))];
|
|
1121
|
+
if (duplicates.length === 0) {
|
|
1122
|
+
console.log(` ${pc.green("\u2713")} config.yaml field "${key}" has valid list semantics`);
|
|
1123
|
+
return 0;
|
|
1124
|
+
}
|
|
1125
|
+
console.log(` ${pc.yellow("!")} config.yaml field "${key}" contains duplicate entries: ${duplicates.join(", ")}`);
|
|
1126
|
+
console.log(` ${pc.dim(`Remove duplicate values from .rsp/config.yaml:${key}`)}`);
|
|
1127
|
+
return 1;
|
|
1128
|
+
}
|
|
1129
|
+
async function checkActiveFeatureConsistency() {
|
|
1130
|
+
const featuresDir = join10(RSP_DIR, "features");
|
|
1131
|
+
const featureFiles = existsSync10(featuresDir) ? await walkMarkdownFiles(featuresDir) : [];
|
|
1132
|
+
const featureNames = new Set(featureFiles.map((fp) => relative5(featuresDir, fp).replace(/\.md$/, "")));
|
|
1133
|
+
const activeEntries = existsSync10(ACTIVE_DIR) ? await walkFiles(ACTIVE_DIR) : [];
|
|
1134
|
+
const activeNames = new Set(activeEntries.map((fp) => relative5(ACTIVE_DIR, fp)));
|
|
1135
|
+
const missingFeatureFiles = [...activeNames].filter((name) => !featureNames.has(name));
|
|
1136
|
+
const unstagedActiveMarkers = [...featureNames].filter((name) => !activeNames.has(name));
|
|
1137
|
+
let issues = 0;
|
|
1138
|
+
if (missingFeatureFiles.length === 0) {
|
|
1139
|
+
console.log(` ${pc.green("\u2713")} every active marker maps to a feature file`);
|
|
1140
|
+
} else {
|
|
1141
|
+
console.log(` ${pc.yellow("!")} active markers without matching feature files: ${missingFeatureFiles.join(", ")}`);
|
|
1142
|
+
console.log(` ${pc.dim("Run: remove stale files in .rsp/active.d/ or recreate the feature with rsp new <name>")}`);
|
|
1143
|
+
issues++;
|
|
1144
|
+
}
|
|
1145
|
+
if (unstagedActiveMarkers.length === 0) {
|
|
1146
|
+
console.log(` ${pc.green("\u2713")} every feature file has an active marker`);
|
|
1147
|
+
} else {
|
|
1148
|
+
console.log(` ${pc.yellow("!")} feature files without active markers: ${unstagedActiveMarkers.join(", ")}`);
|
|
1149
|
+
console.log(` ${pc.dim("Run: recreate the marker with rsp new <name> or archive the feature with rsp close <name>")}`);
|
|
1150
|
+
issues++;
|
|
1151
|
+
}
|
|
1152
|
+
return issues;
|
|
722
1153
|
}
|
|
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
1154
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1155
|
+
// src/commands/init.ts
|
|
1156
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1157
|
+
import { mkdir as mkdir5, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
1158
|
+
import { basename as basename6, join as join11 } from "path";
|
|
737
1159
|
var CONFIG_TEMPLATE = `# RSP project configuration
|
|
738
1160
|
# Uncomment any section below to override defaults.
|
|
739
1161
|
#
|
|
@@ -759,82 +1181,111 @@ var CONFIG_TEMPLATE = `# RSP project configuration
|
|
|
759
1181
|
# - Spec
|
|
760
1182
|
# - Plan
|
|
761
1183
|
`;
|
|
762
|
-
async function
|
|
763
|
-
|
|
1184
|
+
async function detectProjectName3() {
|
|
1185
|
+
try {
|
|
1186
|
+
if (existsSync11("package.json")) {
|
|
1187
|
+
const raw = await readFile11("package.json", "utf-8");
|
|
1188
|
+
const pkg = JSON.parse(raw);
|
|
1189
|
+
if (pkg.name && pkg.name !== "")
|
|
1190
|
+
return pkg.name;
|
|
1191
|
+
}
|
|
1192
|
+
} catch {
|
|
1193
|
+
}
|
|
1194
|
+
return basename6(process.cwd());
|
|
1195
|
+
}
|
|
1196
|
+
async function ensureFile(path, content) {
|
|
1197
|
+
if (existsSync11(path))
|
|
1198
|
+
return false;
|
|
1199
|
+
await writeFile6(path, content);
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
function toTitle(projectName) {
|
|
1203
|
+
return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
1204
|
+
}
|
|
1205
|
+
async function initProject(args = {}) {
|
|
1206
|
+
const isNew = !existsSync11(RSP_DIR);
|
|
1207
|
+
const projectName = await detectProjectName3();
|
|
764
1208
|
return withRspLock("init", async () => {
|
|
765
1209
|
const dirs = [
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1210
|
+
join11(RSP_DIR, "rules"),
|
|
1211
|
+
join11(RSP_DIR, "specs"),
|
|
1212
|
+
join11(RSP_DIR, "features"),
|
|
1213
|
+
join11(RSP_DIR, "active.d"),
|
|
1214
|
+
join11(RSP_DIR, "archives")
|
|
770
1215
|
];
|
|
771
|
-
let created = false;
|
|
772
1216
|
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
|
-
created =
|
|
1217
|
+
await mkdir5(d, { recursive: true });
|
|
1218
|
+
let created = false;
|
|
1219
|
+
const bundledRules = await readFile11(join11(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1220
|
+
created = await ensureFile(join11(RSP_DIR, "rules", "rsp-rules.md"), bundledRules) || created;
|
|
1221
|
+
created = await ensureFile(join11(RSP_DIR, "config.yaml"), CONFIG_TEMPLATE) || created;
|
|
1222
|
+
const createdSpecsIndex = await ensureFile(join11(RSP_DIR, "specs", "INDEX.md"), "# Specs Index\n\n_Project-level specs and design notes._\n");
|
|
1223
|
+
created = createdSpecsIndex || created;
|
|
1224
|
+
const createdDesign = await ensureFile(join11(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
|
|
1225
|
+
created = createdDesign || created;
|
|
1226
|
+
const createdArchivesIndex = await ensureFile(join11(RSP_DIR, "archives", "INDEX.md"), "# Archive Index\n");
|
|
1227
|
+
created = createdArchivesIndex || created;
|
|
1228
|
+
created = await ensureFile(join11(RSP_DIR, "features", ".gitkeep"), "") || created;
|
|
1229
|
+
created = await ensureFile(join11(RSP_DIR, "active.d", ".gitkeep"), "") || created;
|
|
1230
|
+
created = await ensureFile(join11(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
|
|
1231
|
+
if (args.withProjectRules)
|
|
1232
|
+
created = await ensureFile(join11(RSP_DIR, "rules", "project-rules.md"), generateProjectRulesContent(projectName)) || created;
|
|
1233
|
+
if (args.withProjectSetup) {
|
|
1234
|
+
const featurePath = join11(RSP_DIR, "features", "project-setup.md");
|
|
1235
|
+
const activeMarker = join11(RSP_DIR, "active.d", "project-setup");
|
|
1236
|
+
created = await ensureFile(featurePath, generateFeatureContent("project-setup")) || created;
|
|
1237
|
+
created = await ensureFile(activeMarker, "") || created;
|
|
793
1238
|
}
|
|
1239
|
+
if (createdSpecsIndex || createdDesign)
|
|
1240
|
+
await buildSpecsIndex({ acquireLock: false });
|
|
1241
|
+
if (createdArchivesIndex)
|
|
1242
|
+
await buildArchiveIndex({ acquireLock: false });
|
|
794
1243
|
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}
|
|
1244
|
+
const title = toTitle(projectName);
|
|
1245
|
+
const mode = args.agentsMode ?? "managed";
|
|
1246
|
+
if (!existsSync11(agentsPath)) {
|
|
1247
|
+
if (mode === "managed") {
|
|
1248
|
+
await writeFile6(agentsPath, `# ${title}
|
|
805
1249
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
${after}`);
|
|
809
|
-
} else {
|
|
810
|
-
await writeFile4(agentsPath, `${existing}
|
|
811
|
-
|
|
812
|
-
${AGENTS_BODY}
|
|
1250
|
+
${upsertRspAgentsBlock("").content}
|
|
813
1251
|
`);
|
|
814
|
-
}
|
|
815
1252
|
created = true;
|
|
816
1253
|
}
|
|
817
|
-
} else {
|
|
818
|
-
const
|
|
819
|
-
|
|
1254
|
+
} else if (mode === "managed") {
|
|
1255
|
+
const existing = await readFile11(agentsPath, "utf-8");
|
|
1256
|
+
const next = upsertRspAgentsBlock(existing);
|
|
1257
|
+
if (next.changed) {
|
|
1258
|
+
await writeFile6(agentsPath, next.content);
|
|
1259
|
+
created = true;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (mode === "print") {
|
|
1263
|
+
const managed = existsSync11(agentsPath) ? upsertRspAgentsBlock(await readFile11(agentsPath, "utf-8")).content : `# ${title}
|
|
820
1264
|
|
|
821
|
-
${
|
|
1265
|
+
${renderRspAgentsBlock()}
|
|
1266
|
+
`;
|
|
1267
|
+
console.log(`
|
|
1268
|
+
${managed}
|
|
822
1269
|
`);
|
|
823
|
-
created = true;
|
|
824
1270
|
}
|
|
825
1271
|
if (isNew) {
|
|
1272
|
+
const createdProjectSetup = args.withProjectSetup ? "\n .rsp/features/project-setup.md\n .rsp/active.d/project-setup" : "";
|
|
1273
|
+
const next = args.withProjectSetup ? "fill .rsp/features/project-setup.md" : "rsp new project-setup";
|
|
826
1274
|
console.log(`
|
|
827
1275
|
${pc.green("RSP scaffolded.")}
|
|
828
1276
|
`);
|
|
829
1277
|
console.log(` Created: .rsp/rules/
|
|
830
1278
|
.rsp/specs/
|
|
831
1279
|
.rsp/features/
|
|
832
|
-
.rsp/
|
|
1280
|
+
.rsp/active.d/
|
|
1281
|
+
.rsp/archives/
|
|
833
1282
|
.rsp/.gitignore
|
|
834
1283
|
.rsp/config.yaml
|
|
1284
|
+
.rsp/specs/design.md${createdProjectSetup}
|
|
835
1285
|
AGENTS.md
|
|
836
1286
|
`);
|
|
837
|
-
console.log(` ${pc.cyan("Next:")}
|
|
1287
|
+
console.log(` ${pc.cyan("Next:")} ${next}
|
|
1288
|
+
${pc.dim("Then:")} fill .rsp/specs/design.md
|
|
838
1289
|
${pc.dim("Also:")} rsp status rsp deps
|
|
839
1290
|
`);
|
|
840
1291
|
} else if (created) {
|
|
@@ -850,34 +1301,40 @@ ${AGENTS_BODY}
|
|
|
850
1301
|
}
|
|
851
1302
|
|
|
852
1303
|
// src/commands/new-feature.ts
|
|
853
|
-
import { existsSync as
|
|
854
|
-
import { mkdir as
|
|
855
|
-
import { dirname as
|
|
1304
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1305
|
+
import { mkdir as mkdir6, writeFile as writeFile7 } from "fs/promises";
|
|
1306
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
856
1307
|
async function newFeature(name, summary = "") {
|
|
1308
|
+
if (!name) {
|
|
1309
|
+
console.error(` ${pc.red("Usage:")} rsp new <name> [summary]`);
|
|
1310
|
+
process.exit(1);
|
|
1311
|
+
}
|
|
1312
|
+
if (/^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(name) === false) {
|
|
1313
|
+
console.error(` ${pc.red("Error:")} feature name must be kebab-case with optional subdirectory (lowercase, digits, hyphens, slashes)`);
|
|
1314
|
+
process.exit(1);
|
|
1315
|
+
}
|
|
1316
|
+
const rspRulesPath = join12(RSP_DIR, "rules", "rsp-rules.md");
|
|
1317
|
+
const designPath = join12(RSP_DIR, "specs", "design.md");
|
|
1318
|
+
if (!existsSync12(rspRulesPath) || !existsSync12(designPath)) {
|
|
1319
|
+
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
1320
|
+
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
1321
|
+
process.exit(1);
|
|
1322
|
+
}
|
|
857
1323
|
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);
|
|
1324
|
+
const featurePath = join12(RSP_DIR, "features", `${name}.md`);
|
|
1325
|
+
await mkdir6(dirname6(featurePath), { recursive: true });
|
|
1326
|
+
const existed = existsSync12(featurePath);
|
|
869
1327
|
if (!existed) {
|
|
870
1328
|
const content = generateFeatureContent(name, summary);
|
|
871
|
-
await
|
|
1329
|
+
await writeFile7(featurePath, content);
|
|
872
1330
|
}
|
|
873
|
-
await
|
|
874
|
-
const activeEntry =
|
|
875
|
-
await
|
|
876
|
-
await
|
|
1331
|
+
await mkdir6(ACTIVE_DIR, { recursive: true });
|
|
1332
|
+
const activeEntry = join12(ACTIVE_DIR, name);
|
|
1333
|
+
await mkdir6(dirname6(activeEntry), { recursive: true });
|
|
1334
|
+
await writeFile7(activeEntry, "");
|
|
877
1335
|
if (!existed) {
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
if (existsSync8(archiveIndex))
|
|
1336
|
+
const archiveIndex = join12(ARCHIVES_DIR, "INDEX.md");
|
|
1337
|
+
if (existsSync12(archiveIndex))
|
|
881
1338
|
await buildArchiveIndex({ acquireLock: false });
|
|
882
1339
|
}
|
|
883
1340
|
const label = existed ? "Using" : pc.green("Created");
|
|
@@ -888,27 +1345,27 @@ async function newFeature(name, summary = "") {
|
|
|
888
1345
|
}
|
|
889
1346
|
|
|
890
1347
|
// src/commands/status.ts
|
|
891
|
-
import { existsSync as
|
|
892
|
-
import { readFile as
|
|
893
|
-
import { join as
|
|
1348
|
+
import { existsSync as existsSync13 } from "fs";
|
|
1349
|
+
import { readFile as readFile12, stat as stat2 } from "fs/promises";
|
|
1350
|
+
import { join as join13, relative as relative6 } from "path";
|
|
894
1351
|
var COL_FEATURE = 30;
|
|
895
1352
|
var COL_STATUS = 16;
|
|
896
1353
|
var COL_AGE = 7;
|
|
897
1354
|
var COL_PROGRESS = 10;
|
|
898
|
-
async function showStatus() {
|
|
899
|
-
const featuresDir =
|
|
1355
|
+
async function showStatus(options = {}) {
|
|
1356
|
+
const featuresDir = join13(RSP_DIR, "features");
|
|
900
1357
|
const activeSet = /* @__PURE__ */ new Set();
|
|
901
|
-
if (
|
|
1358
|
+
if (existsSync13(ACTIVE_DIR)) {
|
|
902
1359
|
try {
|
|
903
1360
|
const entries = await walkFiles(ACTIVE_DIR);
|
|
904
1361
|
for (const entryPath of entries) {
|
|
905
|
-
const name =
|
|
1362
|
+
const name = relative6(ACTIVE_DIR, entryPath);
|
|
906
1363
|
activeSet.add(name);
|
|
907
1364
|
}
|
|
908
1365
|
} catch {
|
|
909
1366
|
}
|
|
910
1367
|
}
|
|
911
|
-
const featureFiles =
|
|
1368
|
+
const featureFiles = existsSync13(featuresDir) ? await walkMarkdownFiles(featuresDir) : [];
|
|
912
1369
|
const featureMap = {};
|
|
913
1370
|
for (const fp of featureFiles) {
|
|
914
1371
|
const name = featureNameFromPath(featuresDir, fp);
|
|
@@ -941,7 +1398,7 @@ async function showStatus() {
|
|
|
941
1398
|
let isBlocked = false;
|
|
942
1399
|
if (fp) {
|
|
943
1400
|
try {
|
|
944
|
-
const content = await
|
|
1401
|
+
const content = await readFile12(fp, "utf-8");
|
|
945
1402
|
const fm = parseFrontmatter(content);
|
|
946
1403
|
status = fm?.status || "\u2014";
|
|
947
1404
|
isBlocked = status === "blocked";
|
|
@@ -960,11 +1417,28 @@ async function showStatus() {
|
|
|
960
1417
|
}
|
|
961
1418
|
records.push({ name, status, progress, age, depends, isActive, isBlocked });
|
|
962
1419
|
}
|
|
1420
|
+
const filteredRecords = records.filter((record) => {
|
|
1421
|
+
if (options.active && !record.isActive)
|
|
1422
|
+
return false;
|
|
1423
|
+
if (options.blocked && !record.isBlocked)
|
|
1424
|
+
return false;
|
|
1425
|
+
if (typeof options.stale === "number") {
|
|
1426
|
+
const ageDays = Number(record.age);
|
|
1427
|
+
if (!Number.isFinite(ageDays) || ageDays < options.stale)
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
return true;
|
|
1431
|
+
});
|
|
1432
|
+
if (filteredRecords.length === 0) {
|
|
1433
|
+
console.log(` ${pc.dim("No features match the current filters.")}`);
|
|
1434
|
+
console.log();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
963
1437
|
const pad = (s, w) => s.padEnd(w);
|
|
964
1438
|
console.log(` ${pad("Feature", COL_FEATURE)} ${pad("Status", COL_STATUS)} ${pad("Age(d)", COL_AGE)} ${pad("Progress", COL_PROGRESS)} Depends On`);
|
|
965
1439
|
console.log(` ${"\u2500".repeat(COL_FEATURE)} ${"\u2500".repeat(COL_STATUS)} ${"\u2500".repeat(COL_AGE)} ${"\u2500".repeat(COL_PROGRESS)} ${"\u2500".repeat(30)}`);
|
|
966
1440
|
let blockedCount = 0;
|
|
967
|
-
for (const r of
|
|
1441
|
+
for (const r of filteredRecords) {
|
|
968
1442
|
const marker = r.isActive ? pc.cyan("*") : " ";
|
|
969
1443
|
const namePadded = r.name.padEnd(COL_FEATURE - 1);
|
|
970
1444
|
const statusPadded = String(r.status).padEnd(COL_STATUS);
|
|
@@ -978,24 +1452,25 @@ async function showStatus() {
|
|
|
978
1452
|
blockedCount++;
|
|
979
1453
|
}
|
|
980
1454
|
console.log();
|
|
981
|
-
const total =
|
|
982
|
-
const doneCount =
|
|
983
|
-
|
|
1455
|
+
const total = filteredRecords.length;
|
|
1456
|
+
const doneCount = filteredRecords.filter((r) => r.status === "done").length;
|
|
1457
|
+
const activeCount = filteredRecords.filter((r) => r.isActive).length;
|
|
1458
|
+
console.log(` ${pc.bold("Summary:")} ${total} feature(s), ${activeCount} active, ${doneCount} done, ${pc.yellow(String(blockedCount))} blocked`);
|
|
984
1459
|
console.log();
|
|
985
1460
|
await showArchiveTrend();
|
|
986
1461
|
if (blockedCount > 0) {
|
|
987
|
-
const blocked =
|
|
1462
|
+
const blocked = filteredRecords.filter((r) => r.isBlocked);
|
|
988
1463
|
console.log(` ${pc.yellow("Blocked:")} ${blocked.map((r) => r.name).join(", ")}`);
|
|
989
1464
|
console.log();
|
|
990
1465
|
}
|
|
991
1466
|
}
|
|
992
1467
|
async function showArchiveTrend() {
|
|
993
|
-
const archiveDir =
|
|
994
|
-
const indexPath =
|
|
995
|
-
if (!
|
|
1468
|
+
const archiveDir = join13(RSP_DIR, "archives");
|
|
1469
|
+
const indexPath = join13(archiveDir, "INDEX.md");
|
|
1470
|
+
if (!existsSync13(indexPath))
|
|
996
1471
|
return;
|
|
997
1472
|
try {
|
|
998
|
-
const content = await
|
|
1473
|
+
const content = await readFile12(indexPath, "utf-8");
|
|
999
1474
|
const dateRe = /^\| (\d{4}-\d{2})-\d{2} \|/gm;
|
|
1000
1475
|
const counts = {};
|
|
1001
1476
|
let match;
|
|
@@ -1023,8 +1498,29 @@ var initCommand = defineCommand({
|
|
|
1023
1498
|
name: "init",
|
|
1024
1499
|
description: "Scaffold .rsp/ + AGENTS.md in current project"
|
|
1025
1500
|
},
|
|
1026
|
-
|
|
1027
|
-
|
|
1501
|
+
args: {
|
|
1502
|
+
"agents-mode": {
|
|
1503
|
+
type: "string",
|
|
1504
|
+
description: "How to update AGENTS.md: managed, skip, or print",
|
|
1505
|
+
default: "managed"
|
|
1506
|
+
},
|
|
1507
|
+
"with-project-rules": {
|
|
1508
|
+
type: "boolean",
|
|
1509
|
+
description: "Create .rsp/rules/project-rules.md",
|
|
1510
|
+
default: false
|
|
1511
|
+
},
|
|
1512
|
+
"with-project-setup": {
|
|
1513
|
+
type: "boolean",
|
|
1514
|
+
description: "Create .rsp/features/project-setup.md and mark it active",
|
|
1515
|
+
default: false
|
|
1516
|
+
}
|
|
1517
|
+
},
|
|
1518
|
+
async run({ args }) {
|
|
1519
|
+
await initProject({
|
|
1520
|
+
agentsMode: args["agents-mode"] === "skip" || args["agents-mode"] === "print" ? args["agents-mode"] : "managed",
|
|
1521
|
+
withProjectRules: Boolean(args["with-project-rules"]),
|
|
1522
|
+
withProjectSetup: Boolean(args["with-project-setup"])
|
|
1523
|
+
});
|
|
1028
1524
|
}
|
|
1029
1525
|
});
|
|
1030
1526
|
var newFeatureCommand = defineCommand({
|
|
@@ -1044,10 +1540,52 @@ var newFeatureCommand = defineCommand({
|
|
|
1044
1540
|
await newFeature(args.name, summary);
|
|
1045
1541
|
}
|
|
1046
1542
|
});
|
|
1543
|
+
var addRulesCommand = defineCommand({
|
|
1544
|
+
meta: {
|
|
1545
|
+
name: "rules",
|
|
1546
|
+
description: "Create .rsp/rules/<name>.md"
|
|
1547
|
+
},
|
|
1548
|
+
args: {
|
|
1549
|
+
name: {
|
|
1550
|
+
type: "positional",
|
|
1551
|
+
description: "Rules file name",
|
|
1552
|
+
required: true
|
|
1553
|
+
}
|
|
1554
|
+
},
|
|
1555
|
+
async run({ args }) {
|
|
1556
|
+
await addRules(args.name);
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
var addSpecCommand = defineCommand({
|
|
1560
|
+
meta: {
|
|
1561
|
+
name: "spec",
|
|
1562
|
+
description: "Create .rsp/specs/<name>.md and rebuild specs index"
|
|
1563
|
+
},
|
|
1564
|
+
args: {
|
|
1565
|
+
name: {
|
|
1566
|
+
type: "positional",
|
|
1567
|
+
description: "Spec file name",
|
|
1568
|
+
required: true
|
|
1569
|
+
}
|
|
1570
|
+
},
|
|
1571
|
+
async run({ args }) {
|
|
1572
|
+
await addSpec(args.name);
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
var addCommand = defineCommand({
|
|
1576
|
+
meta: {
|
|
1577
|
+
name: "add",
|
|
1578
|
+
description: "Add optional rules or spec files"
|
|
1579
|
+
},
|
|
1580
|
+
subCommands: {
|
|
1581
|
+
rules: addRulesCommand,
|
|
1582
|
+
spec: addSpecCommand
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1047
1585
|
var closeFeatureCommand = defineCommand({
|
|
1048
1586
|
meta: {
|
|
1049
1587
|
name: "close",
|
|
1050
|
-
description: "Archive to .rsp/
|
|
1588
|
+
description: "Archive to .rsp/archives/"
|
|
1051
1589
|
},
|
|
1052
1590
|
args: {
|
|
1053
1591
|
name: {
|
|
@@ -1065,8 +1603,33 @@ var statusCommand = defineCommand({
|
|
|
1065
1603
|
name: "status",
|
|
1066
1604
|
description: "Show project status summary"
|
|
1067
1605
|
},
|
|
1068
|
-
|
|
1069
|
-
|
|
1606
|
+
args: {
|
|
1607
|
+
active: {
|
|
1608
|
+
type: "boolean",
|
|
1609
|
+
description: "Show only active features",
|
|
1610
|
+
default: false
|
|
1611
|
+
},
|
|
1612
|
+
blocked: {
|
|
1613
|
+
type: "boolean",
|
|
1614
|
+
description: "Show only blocked features",
|
|
1615
|
+
default: false
|
|
1616
|
+
},
|
|
1617
|
+
stale: {
|
|
1618
|
+
type: "string",
|
|
1619
|
+
description: "Show only features with age >= days"
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1622
|
+
async run({ args }) {
|
|
1623
|
+
const stale = args.stale === void 0 ? void 0 : Number(args.stale);
|
|
1624
|
+
if (args.stale !== void 0 && (Number.isFinite(stale) === false || Number.isInteger(stale) === false || stale < 0)) {
|
|
1625
|
+
console.error(` Error: --stale must be a non-negative integer number of days`);
|
|
1626
|
+
process.exit(1);
|
|
1627
|
+
}
|
|
1628
|
+
await showStatus({
|
|
1629
|
+
active: Boolean(args.active),
|
|
1630
|
+
blocked: Boolean(args.blocked),
|
|
1631
|
+
stale
|
|
1632
|
+
});
|
|
1070
1633
|
}
|
|
1071
1634
|
});
|
|
1072
1635
|
var checkCommand = defineCommand({
|
|
@@ -1090,21 +1653,53 @@ var depsCommand = defineCommand({
|
|
|
1090
1653
|
type: "boolean",
|
|
1091
1654
|
description: "Output as Mermaid.js graph",
|
|
1092
1655
|
default: false
|
|
1656
|
+
},
|
|
1657
|
+
focus: {
|
|
1658
|
+
type: "string",
|
|
1659
|
+
description: "Show only a feature and its direct dependency neighborhood"
|
|
1660
|
+
},
|
|
1661
|
+
reverse: {
|
|
1662
|
+
type: "string",
|
|
1663
|
+
description: "Show only features that depend on the given feature"
|
|
1093
1664
|
}
|
|
1094
1665
|
},
|
|
1095
1666
|
async run({ args }) {
|
|
1096
|
-
await showDependencies(
|
|
1667
|
+
await showDependencies({
|
|
1668
|
+
mermaid: Boolean(args.mermaid),
|
|
1669
|
+
focus: typeof args.focus === "string" ? args.focus : void 0,
|
|
1670
|
+
reverse: typeof args.reverse === "string" ? args.reverse : void 0
|
|
1671
|
+
});
|
|
1097
1672
|
}
|
|
1098
1673
|
});
|
|
1099
1674
|
var archiveIndexCommand = defineCommand({
|
|
1100
1675
|
meta: {
|
|
1101
1676
|
name: "archive-index",
|
|
1102
|
-
description: "Regenerate
|
|
1677
|
+
description: "Regenerate archives INDEX.md"
|
|
1103
1678
|
},
|
|
1104
1679
|
async run() {
|
|
1105
1680
|
await buildArchiveIndex();
|
|
1106
1681
|
}
|
|
1107
1682
|
});
|
|
1683
|
+
var specsIndexCommand = defineCommand({
|
|
1684
|
+
meta: {
|
|
1685
|
+
name: "specs-index",
|
|
1686
|
+
description: "Regenerate specs INDEX.md"
|
|
1687
|
+
},
|
|
1688
|
+
async run() {
|
|
1689
|
+
await buildSpecsIndex();
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
var doctorCommand = defineCommand({
|
|
1693
|
+
meta: {
|
|
1694
|
+
name: "doctor",
|
|
1695
|
+
description: "Check RSP setup health and common integration issues"
|
|
1696
|
+
},
|
|
1697
|
+
async run() {
|
|
1698
|
+
const issues = await runDoctor();
|
|
1699
|
+
if (issues > 0)
|
|
1700
|
+
process.exit(1);
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1108
1703
|
async function runCli(rawArgs = process.argv.slice(2)) {
|
|
1109
1704
|
const version = await getVersion();
|
|
1110
1705
|
const main = defineCommand({
|
|
@@ -1115,12 +1710,15 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
1115
1710
|
},
|
|
1116
1711
|
subCommands: {
|
|
1117
1712
|
"init": initCommand,
|
|
1713
|
+
"add": addCommand,
|
|
1118
1714
|
"new": newFeatureCommand,
|
|
1119
1715
|
"close": closeFeatureCommand,
|
|
1120
1716
|
"status": statusCommand,
|
|
1121
1717
|
"check": checkCommand,
|
|
1122
1718
|
"deps": depsCommand,
|
|
1123
|
-
"archive-index": archiveIndexCommand
|
|
1719
|
+
"archive-index": archiveIndexCommand,
|
|
1720
|
+
"specs-index": specsIndexCommand,
|
|
1721
|
+
"doctor": doctorCommand
|
|
1124
1722
|
}
|
|
1125
1723
|
});
|
|
1126
1724
|
await runMain(main, { rawArgs });
|