@oevery/rsp 3.1.0-beta.2 → 3.1.0-beta.3
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 +44 -33
- package/README.zh-CN.md +37 -31
- package/dist/{chunk-EALHA3ZS.mjs → chunk-7KKAUBKN.mjs} +246 -253
- package/dist/{chunk-4K53353L.mjs → chunk-NX55NIC3.mjs} +40 -4
- package/dist/cli.mjs +443 -302
- package/dist/{entry-UWUX3HM7.mjs → entry-WZDVKDUM.mjs} +1 -1
- package/dist/{entry-N7WLWGTI.mjs → entry-X23GPCVS.mjs} +1 -1
- package/package.json +1 -1
- package/rules/rsp-rules.md +9 -5
- package/skills/rsp/SKILL.md +10 -4
- package/skills/rsp/references/durable-review.md +1 -1
- package/skills/rsp/references/managed-routing.md +11 -5
- package/skills/rsp/references/setup-repair.md +2 -2
- package/skills/rsp-design/SKILL.md +2 -2
- package/skills/rsp-manage/SKILL.md +27 -12
- package/skills/{rsp-address-review → rsp-resolve-findings}/SKILL.md +4 -4
- package/skills/rsp-shape/SKILL.md +2 -2
- package/skills/{rsp-codebase-audit → rsp-structural-audit}/SKILL.md +5 -3
- package/skills/{rsp-codebase-audit → rsp-structural-audit}/references/structural-lenses.md +6 -0
package/dist/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
installPackagedSkills,
|
|
5
5
|
printPackagedSkillInventory,
|
|
6
6
|
printSkillInstallResult
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-NX55NIC3.mjs";
|
|
8
8
|
import {
|
|
9
9
|
ArchiveHistoryError,
|
|
10
10
|
GROUP_BRIEF_FILENAME,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
resolveFocusMarkerPath,
|
|
30
30
|
resolveWorkRef,
|
|
31
31
|
selectArchiveHistoryRecord
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-7KKAUBKN.mjs";
|
|
33
33
|
import {
|
|
34
34
|
ARCHIVES_DIR,
|
|
35
35
|
CHANGES_DIR,
|
|
@@ -53,7 +53,6 @@ import {
|
|
|
53
53
|
emitJson,
|
|
54
54
|
ensureDecisionRecordsDirectory,
|
|
55
55
|
ensureManagedFile,
|
|
56
|
-
extractSection,
|
|
57
56
|
generateChangeContent,
|
|
58
57
|
generateDesignContent,
|
|
59
58
|
generateSpecContent,
|
|
@@ -92,7 +91,7 @@ import { fileURLToPath } from "url";
|
|
|
92
91
|
import { defineCommand, runMain } from "citty";
|
|
93
92
|
|
|
94
93
|
// src/commands/add-spec.ts
|
|
95
|
-
import { mkdir as mkdir2, unlink as
|
|
94
|
+
import { mkdir as mkdir2, unlink as unlink3, writeFile as writeFile2 } from "fs/promises";
|
|
96
95
|
import { join as join2 } from "path";
|
|
97
96
|
|
|
98
97
|
// src/core/lock.ts
|
|
@@ -157,12 +156,51 @@ ${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
|
157
156
|
}
|
|
158
157
|
|
|
159
158
|
// src/commands/specs-index.ts
|
|
160
|
-
import {
|
|
161
|
-
import {
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
import { readFile as readFile2, unlink as unlink2 } from "fs/promises";
|
|
160
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "path";
|
|
161
|
+
var SPECS_INDEX_FILENAME = "00-index.md";
|
|
162
|
+
var LEGACY_SPECS_INDEX_FILENAME = "INDEX.md";
|
|
163
|
+
async function buildSpecsIndex(options = {}) {
|
|
164
|
+
const { acquireLock = true, quiet = false, affectedDirectory } = options;
|
|
164
165
|
if (acquireLock)
|
|
165
|
-
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet }));
|
|
166
|
+
return withRspLock("specs-index", async () => buildSpecsIndex({ acquireLock: false, quiet, affectedDirectory }));
|
|
167
|
+
const plan = await planSpecsIndexes(affectedDirectory);
|
|
168
|
+
for (const write of plan.writes)
|
|
169
|
+
await writeManagedFile(write.path, write.content, "Specs index");
|
|
170
|
+
for (const path of plan.removals)
|
|
171
|
+
await unlink2(path);
|
|
172
|
+
const changed = plan.writes.length > 0 || plan.removals.length > 0;
|
|
173
|
+
if (!quiet) {
|
|
174
|
+
if (changed)
|
|
175
|
+
console.log(` ${pc.green("Specs indexes updated:")} ${plan.writes.length} written, ${plan.removals.length} removed.
|
|
176
|
+
`);
|
|
177
|
+
else
|
|
178
|
+
console.log(` ${pc.dim("Specs indexes already up to date.")}
|
|
179
|
+
`);
|
|
180
|
+
}
|
|
181
|
+
return changed;
|
|
182
|
+
}
|
|
183
|
+
async function inspectSpecsIndexHealth() {
|
|
184
|
+
try {
|
|
185
|
+
const plan = await planSpecsIndexes();
|
|
186
|
+
return {
|
|
187
|
+
ok: plan.writes.length === 0 && plan.removals.length === 0,
|
|
188
|
+
expectedPaths: plan.expectedPaths.map(toProjectPath),
|
|
189
|
+
stalePaths: plan.writes.map((write) => toProjectPath(write.path)),
|
|
190
|
+
obsoletePaths: plan.removals.map(toProjectPath),
|
|
191
|
+
issues: []
|
|
192
|
+
};
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
expectedPaths: [],
|
|
197
|
+
stalePaths: [],
|
|
198
|
+
obsoletePaths: [],
|
|
199
|
+
issues: [error instanceof Error ? error.message : String(error)]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function planSpecsIndexes(affectedDirectory) {
|
|
166
204
|
const specsDir = join(RSP_DIR, "specs");
|
|
167
205
|
const configInspection = await inspectRspConfig();
|
|
168
206
|
if (configInspection.issues.length > 0)
|
|
@@ -171,91 +209,154 @@ async function buildSpecsIndex({ acquireLock = true, quiet = false } = {}) {
|
|
|
171
209
|
const inspection = await inspectManagedFileTree(specsDir, "Specs", { allowMissing: true });
|
|
172
210
|
if (inspection.issues.length > 0)
|
|
173
211
|
throw inspection.issues[0];
|
|
174
|
-
if (!inspection.rootExists)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
212
|
+
if (!inspection.rootExists)
|
|
213
|
+
return { writes: [], removals: [], expectedPaths: [] };
|
|
214
|
+
const excludedDirectories = [DEFAULT_DECISION_RECORDS_PATH, decisionRecordsPath].filter((path) => isInside(specsDir, path));
|
|
215
|
+
const isExcluded = (path) => excludedDirectories.some((directory) => isInside(directory, path));
|
|
216
|
+
const managedIndexFiles = inspection.files.filter((path) => !isExcluded(path)).filter((path) => basename(path) === SPECS_INDEX_FILENAME || path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME));
|
|
217
|
+
const recognizedIndexes = /* @__PURE__ */ new Set();
|
|
218
|
+
for (const path of managedIndexFiles) {
|
|
219
|
+
let metadata;
|
|
220
|
+
try {
|
|
221
|
+
metadata = parseFrontmatter(await readFile2(path, "utf-8"));
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new Error(`unable to inspect reserved Specs index ${toProjectPath(path)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
224
|
+
}
|
|
225
|
+
const expectedSourceDirectory = path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME) ? toProjectPath(specsDir) : toProjectPath(dirname(path));
|
|
226
|
+
if (metadata?.kind !== "generated-index" || metadata.index_type !== "specs" || metadata.source_dir !== expectedSourceDirectory) {
|
|
227
|
+
throw new Error(`reserved Specs index is not recognized generated content: ${toProjectPath(path)}`);
|
|
228
|
+
}
|
|
229
|
+
recognizedIndexes.add(path);
|
|
230
|
+
}
|
|
231
|
+
const specFiles = inspection.files.filter((path) => path.endsWith(".md")).filter((path) => !isExcluded(path)).filter((path) => basename(path) !== SPECS_INDEX_FILENAME).filter((path) => path !== join(specsDir, LEGACY_SPECS_INDEX_FILENAME)).sort();
|
|
232
|
+
const directories = /* @__PURE__ */ new Set([specsDir]);
|
|
233
|
+
for (const file of specFiles) {
|
|
234
|
+
let current = dirname(file);
|
|
235
|
+
while (isInside(specsDir, current)) {
|
|
236
|
+
directories.add(current);
|
|
237
|
+
if (current === specsDir)
|
|
238
|
+
break;
|
|
239
|
+
current = dirname(current);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const orderedDirectories = [...directories].sort();
|
|
243
|
+
const expectedPaths = orderedDirectories.map((directory) => join(directory, SPECS_INDEX_FILENAME));
|
|
244
|
+
const expectedSet = new Set(expectedPaths);
|
|
245
|
+
const writableDirectories = affectedDirectory ? new Set(ancestorDirectories(specsDir, affectedDirectory)) : directories;
|
|
246
|
+
const writes = [];
|
|
247
|
+
for (const directory of orderedDirectories) {
|
|
248
|
+
if (!writableDirectories.has(directory))
|
|
249
|
+
continue;
|
|
250
|
+
const directFiles = specFiles.filter((path2) => dirname(path2) === directory);
|
|
251
|
+
const directDirectories = orderedDirectories.filter((path2) => path2 !== directory && dirname(path2) === directory);
|
|
252
|
+
const content = await renderSpecsIndex(specsDir, directory, directFiles, directDirectories);
|
|
253
|
+
const path = join(directory, SPECS_INDEX_FILENAME);
|
|
254
|
+
const existingContent = recognizedIndexes.has(path) ? await readFile2(path, "utf-8") : null;
|
|
255
|
+
if (existingContent !== content)
|
|
256
|
+
writes.push({ path, content });
|
|
257
|
+
}
|
|
258
|
+
const removals = [...recognizedIndexes].filter((path) => path === join(specsDir, LEGACY_SPECS_INDEX_FILENAME) || !expectedSet.has(path) && !affectedDirectory).sort();
|
|
259
|
+
return { writes, removals, expectedPaths };
|
|
260
|
+
}
|
|
261
|
+
async function renderSpecsIndex(specsDir, directory, files, directories) {
|
|
262
|
+
const sourceDir = toProjectPath(directory);
|
|
263
|
+
const scope = normalizeLogicalPath(relative(specsDir, directory));
|
|
264
|
+
const title = scope === "" ? "Specs Index" : `Specs: ${scope}`;
|
|
265
|
+
const entries = [];
|
|
266
|
+
for (const childDirectory of directories) {
|
|
267
|
+
const name = basename(childDirectory);
|
|
268
|
+
entries.push({
|
|
269
|
+
sortKey: `${name}/`,
|
|
270
|
+
line: `| [${escapeLabel(`${name}/`)}](${encodeLink(`${name}/${SPECS_INDEX_FILENAME}`)}) | directory | ${escapeCell(name)} | \u2014 |`
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
for (const path of files) {
|
|
274
|
+
const name = basename(path);
|
|
275
|
+
const content = await readFile2(path, "utf-8");
|
|
276
|
+
const metadata = parseFrontmatter(content);
|
|
277
|
+
const metadataTitle = metadata && typeof metadata.title === "string" ? metadata.title : "";
|
|
278
|
+
const entryTitle = metadataTitle || extractTitle(content) || name.replace(/\.md$/, "");
|
|
279
|
+
const summary = extractSummary(content, metadata?.summary);
|
|
280
|
+
entries.push({
|
|
281
|
+
sortKey: name,
|
|
282
|
+
line: `| [${escapeLabel(name)}](${encodeLink(name)}) | spec | ${escapeCell(entryTitle)} | ${escapeCell(summary || "\u2014")} |`
|
|
283
|
+
});
|
|
179
284
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (specFiles.length === 0) {
|
|
199
|
-
lines.push("_No additional project-level specs yet._");
|
|
285
|
+
entries.sort((left, right) => left.sortKey < right.sortKey ? -1 : left.sortKey > right.sortKey ? 1 : 0);
|
|
286
|
+
const lines = [
|
|
287
|
+
"---",
|
|
288
|
+
`title: ${JSON.stringify(title)}`,
|
|
289
|
+
`summary: ${JSON.stringify(`Direct Specs and child Spec directories under ${sourceDir}.`)}`,
|
|
290
|
+
"kind: generated-index",
|
|
291
|
+
"index_type: specs",
|
|
292
|
+
`source_dir: ${JSON.stringify(sourceDir)}`,
|
|
293
|
+
`entry_count: ${entries.length}`,
|
|
294
|
+
"---",
|
|
295
|
+
"",
|
|
296
|
+
`# ${title}`,
|
|
297
|
+
"",
|
|
298
|
+
"_Generated direct-child navigation. Edit the linked Specs, then run `rsp update`._",
|
|
299
|
+
""
|
|
300
|
+
];
|
|
301
|
+
if (entries.length === 0) {
|
|
302
|
+
lines.push("_No Specs or child Spec directories yet._");
|
|
200
303
|
} else {
|
|
201
|
-
lines.push("|
|
|
202
|
-
lines.push("
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
`);
|
|
222
|
-
return changed;
|
|
223
|
-
}
|
|
224
|
-
function isInside(directory, filePath) {
|
|
225
|
-
const rel = relative(directory, filePath);
|
|
304
|
+
lines.push("| Entry | Kind | Title | Summary |");
|
|
305
|
+
lines.push("|-------|------|-------|---------|");
|
|
306
|
+
lines.push(...entries.map((entry) => entry.line));
|
|
307
|
+
}
|
|
308
|
+
return lines.join("\n");
|
|
309
|
+
}
|
|
310
|
+
function ancestorDirectories(root, directory) {
|
|
311
|
+
if (!isInside(root, directory))
|
|
312
|
+
throw new Error(`affected Spec directory must stay inside ${toProjectPath(root)}: ${directory}`);
|
|
313
|
+
const result = [];
|
|
314
|
+
let current = directory;
|
|
315
|
+
while (true) {
|
|
316
|
+
result.push(current);
|
|
317
|
+
if (current === root)
|
|
318
|
+
return result;
|
|
319
|
+
current = dirname(current);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function isInside(directory, path) {
|
|
323
|
+
const rel = relative(directory, path);
|
|
226
324
|
return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
227
325
|
}
|
|
326
|
+
function toProjectPath(path) {
|
|
327
|
+
return normalizeLogicalPath(relative(".", path));
|
|
328
|
+
}
|
|
228
329
|
function extractTitle(content) {
|
|
229
330
|
for (const rawLine of content.split("\n")) {
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
return rawLine.slice(2).trim();
|
|
331
|
+
if (rawLine.startsWith("# "))
|
|
332
|
+
return rawLine.slice(2).trim();
|
|
233
333
|
}
|
|
234
334
|
return "";
|
|
235
335
|
}
|
|
236
336
|
function extractSummary(content, frontmatterSummary) {
|
|
237
337
|
if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
|
|
238
338
|
return frontmatterSummary.trim();
|
|
239
|
-
const lines = content.split("\n");
|
|
240
339
|
let inFrontmatter = false;
|
|
241
|
-
for (const rawLine of
|
|
340
|
+
for (const rawLine of content.split("\n")) {
|
|
242
341
|
const line = rawLine.trim();
|
|
243
342
|
if (line === "---" || line === "...") {
|
|
244
343
|
inFrontmatter = !inFrontmatter;
|
|
245
344
|
continue;
|
|
246
345
|
}
|
|
247
|
-
if (inFrontmatter || line === "")
|
|
248
|
-
continue;
|
|
249
|
-
if (line.startsWith("#"))
|
|
250
|
-
continue;
|
|
251
|
-
if (line.startsWith("##"))
|
|
346
|
+
if (inFrontmatter || line === "" || line.startsWith("#"))
|
|
252
347
|
continue;
|
|
253
348
|
return line.replace(/^[-*]\s*/, "").trim();
|
|
254
349
|
}
|
|
255
350
|
return "";
|
|
256
351
|
}
|
|
257
352
|
function escapeCell(value) {
|
|
258
|
-
return value.replace(/\|/g, "\\|");
|
|
353
|
+
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
354
|
+
}
|
|
355
|
+
function escapeLabel(value) {
|
|
356
|
+
return value.replace(/([\\[\]])/g, "\\$1");
|
|
357
|
+
}
|
|
358
|
+
function encodeLink(value) {
|
|
359
|
+
return encodeURI(value).replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
259
360
|
}
|
|
260
361
|
|
|
261
362
|
// src/commands/add-spec.ts
|
|
@@ -268,6 +369,10 @@ async function addSpec(name, projectName = "project") {
|
|
|
268
369
|
console.error(` ${pc.red("Error:")} .rsp/specs/decisions/ is reserved for Decision Records`);
|
|
269
370
|
process.exit(1);
|
|
270
371
|
}
|
|
372
|
+
if (["index", "00-index"].includes(name.split("/").at(-1) || "")) {
|
|
373
|
+
console.error(` ${pc.red("Error:")} spec name "index" is reserved for generated local navigation`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
271
376
|
guardRspInitialized();
|
|
272
377
|
try {
|
|
273
378
|
const configInspection = await inspectRspConfig();
|
|
@@ -294,11 +399,11 @@ async function addSpec(name, projectName = "project") {
|
|
|
294
399
|
try {
|
|
295
400
|
await writeFile2(path, content, { flag: "wx" });
|
|
296
401
|
created = true;
|
|
297
|
-
await buildSpecsIndex({ acquireLock: false });
|
|
402
|
+
await buildSpecsIndex({ acquireLock: false, affectedDirectory: parent });
|
|
298
403
|
} catch (error) {
|
|
299
404
|
if (created) {
|
|
300
405
|
try {
|
|
301
|
-
await
|
|
406
|
+
await unlink3(path);
|
|
302
407
|
await cleanupEmptyParentDirs(path, specsDir);
|
|
303
408
|
} catch {
|
|
304
409
|
}
|
|
@@ -313,126 +418,22 @@ async function addSpec(name, projectName = "project") {
|
|
|
313
418
|
}
|
|
314
419
|
|
|
315
420
|
// src/commands/archive.ts
|
|
316
|
-
import { existsSync
|
|
317
|
-
import { mkdir as mkdir3, readFile as
|
|
318
|
-
import { basename as
|
|
319
|
-
|
|
320
|
-
// src/commands/archive-index.ts
|
|
321
|
-
import { existsSync as existsSync2 } from "fs";
|
|
322
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
323
|
-
import { basename as basename2, dirname, join as join3, relative as relative2 } from "path";
|
|
324
|
-
async function buildArchiveIndex({ acquireLock = true, quiet = false } = {}) {
|
|
325
|
-
if (acquireLock)
|
|
326
|
-
return withRspLock("archive-index", async () => buildArchiveIndex({ acquireLock: false, quiet }));
|
|
327
|
-
const inspection = await inspectArchiveTree();
|
|
328
|
-
if (inspection.diagnostics.length > 0) {
|
|
329
|
-
const diagnostic = inspection.diagnostics[0];
|
|
330
|
-
throw new WorkRefError(diagnostic.code, diagnostic.message, diagnostic.input);
|
|
331
|
-
}
|
|
332
|
-
if (!inspection.rootExists) {
|
|
333
|
-
if (!quiet)
|
|
334
|
-
console.log(` ${pc.dim("No archives directory.")}
|
|
335
|
-
`);
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
const archiveFiles = inspection.files;
|
|
339
|
-
const lines = [];
|
|
340
|
-
lines.push("---");
|
|
341
|
-
lines.push("title: Archive Index");
|
|
342
|
-
lines.push("summary: Completed RSP changes.");
|
|
343
|
-
lines.push("kind: generated-index");
|
|
344
|
-
lines.push("index_type: archives");
|
|
345
|
-
lines.push("source_dir: .rsp/archives");
|
|
346
|
-
lines.push(`entry_count: ${archiveFiles.length}`);
|
|
347
|
-
lines.push("---");
|
|
348
|
-
lines.push("");
|
|
349
|
-
lines.push("# Archive Index");
|
|
350
|
-
lines.push("");
|
|
351
|
-
if (archiveFiles.length === 0) {
|
|
352
|
-
lines.push("_No archived changes yet._");
|
|
353
|
-
} else {
|
|
354
|
-
lines.push("| Date | Change | Kind | Summary |");
|
|
355
|
-
lines.push("|------|--------|------|---------|");
|
|
356
|
-
}
|
|
357
|
-
for (const fp of archiveFiles) {
|
|
358
|
-
const base = basename2(fp);
|
|
359
|
-
const archiveRel = normalizeLogicalPath(relative2(ARCHIVES_DIR, fp));
|
|
360
|
-
const archiveDirPart = dirname(archiveRel);
|
|
361
|
-
const dateMatch = base.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.md$/);
|
|
362
|
-
const date = dateMatch ? dateMatch[1] : "\u2014";
|
|
363
|
-
const baseName = dateMatch ? dateMatch[2] : base.replace(/\.md$/, "");
|
|
364
|
-
const changeName = archiveDirPart !== "." ? `${archiveDirPart}/${baseName}` : baseName;
|
|
365
|
-
let summary = "";
|
|
366
|
-
let kind = "";
|
|
367
|
-
try {
|
|
368
|
-
const content = await readFile3(fp, "utf-8");
|
|
369
|
-
const fm = parseFrontmatter(content);
|
|
370
|
-
if (typeof fm?.kind === "string")
|
|
371
|
-
kind = fm.kind;
|
|
372
|
-
if (typeof fm?.summary === "string")
|
|
373
|
-
summary = fm.summary.trim();
|
|
374
|
-
const proposal = extractSection(content, "Proposal");
|
|
375
|
-
const outcomeLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Outcome:"));
|
|
376
|
-
const legacySummaryLine = proposal.split("\n").map((line) => line.trim()).find((line) => line.startsWith("- Summary:"));
|
|
377
|
-
if (!summary && outcomeLine)
|
|
378
|
-
summary = outcomeLine.slice("- Outcome:".length).trim();
|
|
379
|
-
if (!summary && legacySummaryLine)
|
|
380
|
-
summary = legacySummaryLine.slice("- Summary:".length).trim();
|
|
381
|
-
} catch {
|
|
382
|
-
}
|
|
383
|
-
lines.push(`| ${date} | ${escapeCell2(changeName)} | ${escapeCell2(kind || "\u2014")} | ${escapeCell2(summary || "\u2014")} |`);
|
|
384
|
-
}
|
|
385
|
-
const indexPath = join3(ARCHIVES_DIR, "INDEX.md");
|
|
386
|
-
const nextContent = lines.join("\n");
|
|
387
|
-
const existingContent = existsSync2(indexPath) ? await readFile3(indexPath, "utf-8") : null;
|
|
388
|
-
const changed = existingContent !== nextContent;
|
|
389
|
-
if (changed)
|
|
390
|
-
await writeManagedFile(indexPath, nextContent, "archive index");
|
|
391
|
-
if (!quiet)
|
|
392
|
-
console.log(` ${pc.green("INDEX.md updated:")} ${archiveFiles.length} archived change(s).
|
|
393
|
-
`);
|
|
394
|
-
return changed;
|
|
395
|
-
}
|
|
396
|
-
function escapeCell2(value) {
|
|
397
|
-
return value.replace(/\|/g, "\\|");
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// src/commands/archive.ts
|
|
401
|
-
async function archiveChange(name, options = {}) {
|
|
421
|
+
import { existsSync } from "fs";
|
|
422
|
+
import { mkdir as mkdir3, readFile as readFile3, rename, unlink as unlink4 } from "fs/promises";
|
|
423
|
+
import { basename as basename2, join as join3 } from "path";
|
|
424
|
+
async function archiveChange(name) {
|
|
402
425
|
if (!name) {
|
|
403
426
|
console.error(` ${pc.red("Usage:")} rsp archive <name>`);
|
|
404
427
|
process.exit(1);
|
|
405
428
|
}
|
|
406
429
|
guardRspInitialized();
|
|
407
|
-
if (options.dryRun) {
|
|
408
|
-
const workRef = await resolveArchiveWorkRefOrExit(name);
|
|
409
|
-
const content = await readFile4(workRef.path, "utf-8");
|
|
410
|
-
const dependencyInspection = await inspectChangeDependencies();
|
|
411
|
-
const checklist = collectArchiveChecklist(content, {
|
|
412
|
-
activeBlockers: dependencyInspection.activeBlockers.get(name)
|
|
413
|
-
});
|
|
414
|
-
console.log();
|
|
415
|
-
console.log(` ${pc.bold("Archive dry-run for")} ${pc.cyan(name)}`);
|
|
416
|
-
console.log();
|
|
417
|
-
if (checklist.length === 0) {
|
|
418
|
-
console.log(` ${pc.green("\u2713")} Ready to archive. No deterministic warnings found.
|
|
419
|
-
`);
|
|
420
|
-
} else {
|
|
421
|
-
for (const line of checklist)
|
|
422
|
-
console.log(` ${pc.yellow("\u26A0")} ${line}`);
|
|
423
|
-
console.log();
|
|
424
|
-
console.log(` ${pc.dim("Review the warnings above before treating this work as fully closed.")}
|
|
425
|
-
`);
|
|
426
|
-
}
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
430
|
try {
|
|
430
431
|
return await withRspLock("archive-change", async () => {
|
|
431
432
|
const workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
432
433
|
const srcPath = workRef.path;
|
|
433
434
|
const archiveSubdir = resolveArchiveDirectory(workRef);
|
|
434
435
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
435
|
-
const content = await
|
|
436
|
+
const content = await readFile3(srcPath, "utf-8");
|
|
436
437
|
const dependencyInspection = await inspectChangeDependencies();
|
|
437
438
|
const checklist = collectArchiveChecklist(content, {
|
|
438
439
|
activeBlockers: dependencyInspection.activeBlockers.get(name)
|
|
@@ -443,15 +444,15 @@ async function archiveChange(name, options = {}) {
|
|
|
443
444
|
console.log(` ${pc.dim("Archive will continue. Review the warnings above before treating this work as fully closed.")}
|
|
444
445
|
`);
|
|
445
446
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
446
|
-
const base =
|
|
447
|
+
const base = basename2(workRef.name);
|
|
447
448
|
await mkdir3(archiveSubdir, { recursive: true });
|
|
448
449
|
const archiveName = resolveArchiveName(archiveSubdir, date, base);
|
|
449
|
-
await rename(srcPath,
|
|
450
|
+
await rename(srcPath, join3(archiveSubdir, archiveName));
|
|
450
451
|
const postArchiveWarnings = [];
|
|
451
452
|
let focusCleared = false;
|
|
452
|
-
if (
|
|
453
|
+
if (existsSync(focusEntry)) {
|
|
453
454
|
try {
|
|
454
|
-
await
|
|
455
|
+
await unlink4(focusEntry);
|
|
455
456
|
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
456
457
|
focusCleared = true;
|
|
457
458
|
} catch (error) {
|
|
@@ -467,17 +468,12 @@ async function archiveChange(name, options = {}) {
|
|
|
467
468
|
` : "";
|
|
468
469
|
console.log(` ${pc.green("Archived:")} ${archiveName}
|
|
469
470
|
${clearedMsg}`);
|
|
470
|
-
try {
|
|
471
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
472
|
-
} catch (error) {
|
|
473
|
-
postArchiveWarnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
|
|
474
|
-
}
|
|
475
471
|
for (const warning of postArchiveWarnings)
|
|
476
472
|
console.log(` ${pc.yellow("\u26A0")} ${warning}`);
|
|
477
473
|
if (postArchiveWarnings.length > 0)
|
|
478
474
|
console.log(` ${pc.dim("Archive completed, but follow-up cleanup was only partially successful.")}
|
|
479
475
|
`);
|
|
480
|
-
if (
|
|
476
|
+
if (existsSync(".git")) {
|
|
481
477
|
console.log(` ${pc.cyan("Git delivery:")}
|
|
482
478
|
`);
|
|
483
479
|
console.log(` git status --short`);
|
|
@@ -493,28 +489,14 @@ ${clearedMsg}`);
|
|
|
493
489
|
throw error;
|
|
494
490
|
}
|
|
495
491
|
}
|
|
496
|
-
async function resolveArchiveWorkRefOrExit(name) {
|
|
497
|
-
try {
|
|
498
|
-
const workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
499
|
-
resolveArchiveDirectory(workRef);
|
|
500
|
-
resolveFocusMarkerPath(workRef);
|
|
501
|
-
return workRef;
|
|
502
|
-
} catch (error) {
|
|
503
|
-
if (error instanceof WorkRefError) {
|
|
504
|
-
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
505
|
-
process.exit(1);
|
|
506
|
-
}
|
|
507
|
-
throw error;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
492
|
function resolveArchiveName(archiveSubdir, date, base) {
|
|
511
493
|
const initialName = `${date}_${base}.md`;
|
|
512
|
-
if (!
|
|
494
|
+
if (!existsSync(join3(archiveSubdir, initialName)))
|
|
513
495
|
return initialName;
|
|
514
496
|
const MAX_SUFFIX = 9999;
|
|
515
497
|
for (let index = 2; index <= MAX_SUFFIX; index++) {
|
|
516
498
|
const candidate = `${date}_${base}-${index}.md`;
|
|
517
|
-
if (!
|
|
499
|
+
if (!existsSync(join3(archiveSubdir, candidate)))
|
|
518
500
|
return candidate;
|
|
519
501
|
}
|
|
520
502
|
throw new WorkRefError(
|
|
@@ -525,7 +507,7 @@ function resolveArchiveName(archiveSubdir, date, base) {
|
|
|
525
507
|
}
|
|
526
508
|
|
|
527
509
|
// src/commands/check.ts
|
|
528
|
-
import { readFile as
|
|
510
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
529
511
|
async function runCheck(options = {}) {
|
|
530
512
|
const diagnostics = [];
|
|
531
513
|
const runtime = [];
|
|
@@ -568,7 +550,7 @@ async function runCheck(options = {}) {
|
|
|
568
550
|
const entryContent = marker.name;
|
|
569
551
|
let markerContent = "";
|
|
570
552
|
try {
|
|
571
|
-
markerContent = (await
|
|
553
|
+
markerContent = (await readFile4(entryPath, "utf-8")).trim();
|
|
572
554
|
} catch (error) {
|
|
573
555
|
addDiagnostic({
|
|
574
556
|
severity: "error",
|
|
@@ -679,7 +661,7 @@ async function runCheck(options = {}) {
|
|
|
679
661
|
const { name, path: fp } = ref;
|
|
680
662
|
let content;
|
|
681
663
|
try {
|
|
682
|
-
content = await
|
|
664
|
+
content = await readFile4(fp, "utf-8");
|
|
683
665
|
} catch (error) {
|
|
684
666
|
addDiagnostic({
|
|
685
667
|
severity: "error",
|
|
@@ -908,8 +890,8 @@ function collectMatchingLines(content, predicate) {
|
|
|
908
890
|
}
|
|
909
891
|
|
|
910
892
|
// src/commands/create.ts
|
|
911
|
-
import { existsSync as
|
|
912
|
-
import { mkdir as mkdir4, unlink as
|
|
893
|
+
import { existsSync as existsSync2 } from "fs";
|
|
894
|
+
import { mkdir as mkdir4, unlink as unlink5, writeFile as writeFile3 } from "fs/promises";
|
|
913
895
|
import { dirname as dirname2 } from "path";
|
|
914
896
|
async function createChange(name, summary = "", kind, options = {}) {
|
|
915
897
|
if (!name) {
|
|
@@ -921,7 +903,7 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
921
903
|
return await withRspLock("create-change", async () => {
|
|
922
904
|
const workRef = await resolveExecutableChange(name);
|
|
923
905
|
const changePath = workRef.path;
|
|
924
|
-
const existed =
|
|
906
|
+
const existed = existsSync2(changePath);
|
|
925
907
|
if (!existed) {
|
|
926
908
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
927
909
|
const content = generateChangeContent(name, summary, kind, { lite: Boolean(options.lite) });
|
|
@@ -936,7 +918,7 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
936
918
|
} catch (error) {
|
|
937
919
|
if (changeCreated) {
|
|
938
920
|
try {
|
|
939
|
-
await
|
|
921
|
+
await unlink5(changePath);
|
|
940
922
|
await cleanupEmptyParentDirs(changePath, CHANGES_DIR);
|
|
941
923
|
} catch {
|
|
942
924
|
}
|
|
@@ -963,17 +945,39 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
963
945
|
}
|
|
964
946
|
|
|
965
947
|
// src/commands/doctor.ts
|
|
966
|
-
import { existsSync as
|
|
948
|
+
import { existsSync as existsSync4 } from "fs";
|
|
967
949
|
import { readdir, readFile as readFile7, stat } from "fs/promises";
|
|
968
|
-
import { basename as
|
|
950
|
+
import { basename as basename3, join as join6, relative as relative2 } from "path";
|
|
969
951
|
|
|
970
952
|
// src/commands/update.ts
|
|
971
|
-
import { existsSync as
|
|
953
|
+
import { existsSync as existsSync3 } from "fs";
|
|
972
954
|
import { mkdir as mkdir5, readFile as readFile6, rm, rmdir } from "fs/promises";
|
|
973
955
|
import { join as join5 } from "path";
|
|
956
|
+
|
|
957
|
+
// src/commands/archive-index-migration.ts
|
|
958
|
+
import { readFile as readFile5, unlink as unlink6 } from "fs/promises";
|
|
959
|
+
import { join as join4 } from "path";
|
|
960
|
+
async function removeLegacyArchiveIndex() {
|
|
961
|
+
const path = join4(ARCHIVES_DIR, "INDEX.md");
|
|
962
|
+
const inspection = inspectManagedFile(path, "legacy archive index", { allowMissing: true });
|
|
963
|
+
if (!inspection.exists || inspection.issue)
|
|
964
|
+
return false;
|
|
965
|
+
let frontmatter;
|
|
966
|
+
try {
|
|
967
|
+
frontmatter = parseFrontmatter(await readFile5(path, "utf-8"));
|
|
968
|
+
} catch {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
if (frontmatter?.kind !== "generated-index" || frontmatter.index_type !== "archives")
|
|
972
|
+
return false;
|
|
973
|
+
await unlink6(path);
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/commands/update.ts
|
|
974
978
|
var SKILL_REFRESH_HINT = " Note: if you use package-bundled RSP Skills, refresh them too:\n rsp skills install --dry-run\n rsp skills install --force\n";
|
|
975
979
|
async function updateProject(options = {}) {
|
|
976
|
-
if (!
|
|
980
|
+
if (!existsSync3(RSP_DIR)) {
|
|
977
981
|
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
978
982
|
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
979
983
|
process.exit(1);
|
|
@@ -1004,6 +1008,9 @@ async function updateProject(options = {}) {
|
|
|
1004
1008
|
console.log(` ${pc.green("\u2713")} changes/ directory restored`);
|
|
1005
1009
|
updated = true;
|
|
1006
1010
|
}
|
|
1011
|
+
const archivesInspection = inspectManagedDirectory(join5(RSP_DIR, "archives"), "archive root", { allowMissing: true });
|
|
1012
|
+
if (archivesInspection.issue)
|
|
1013
|
+
throw archivesInspection.issue;
|
|
1007
1014
|
const bundledRules = await readFile6(join5(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
1008
1015
|
const rulesInspection = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
1009
1016
|
if (rulesInspection.issue)
|
|
@@ -1016,7 +1023,7 @@ async function updateProject(options = {}) {
|
|
|
1016
1023
|
console.log(` ${pc.green("\u2713")} rsp-rules.md updated`);
|
|
1017
1024
|
updated = true;
|
|
1018
1025
|
}
|
|
1019
|
-
if (
|
|
1026
|
+
if (existsSync3(OBSOLETE_RSP_RULES_PATH)) {
|
|
1020
1027
|
await rm(OBSOLETE_RSP_RULES_PATH);
|
|
1021
1028
|
actions.push("obsolete rules/rsp-rules.md removed");
|
|
1022
1029
|
if (!options.quiet)
|
|
@@ -1078,9 +1085,14 @@ async function updateProject(options = {}) {
|
|
|
1078
1085
|
updated = true;
|
|
1079
1086
|
}
|
|
1080
1087
|
const specsIndexChanged = await buildSpecsIndex({ acquireLock: false, quiet: options.quiet });
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1088
|
+
if (specsIndexChanged) {
|
|
1089
|
+
actions.push("generated Specs indexes reconciled");
|
|
1090
|
+
updated = true;
|
|
1091
|
+
}
|
|
1092
|
+
if (await removeLegacyArchiveIndex()) {
|
|
1093
|
+
actions.push("legacy Archive Index removed");
|
|
1094
|
+
if (!options.quiet)
|
|
1095
|
+
console.log(` ${pc.green("\u2713")} legacy Archive Index removed`);
|
|
1084
1096
|
updated = true;
|
|
1085
1097
|
}
|
|
1086
1098
|
if (!options.quiet && inspection.diagnostics.length > 0) {
|
|
@@ -1116,7 +1128,7 @@ async function updateProject(options = {}) {
|
|
|
1116
1128
|
async function runDoctor(options = {}) {
|
|
1117
1129
|
const fixed = [];
|
|
1118
1130
|
let repairIssue = null;
|
|
1119
|
-
if (options.fix &&
|
|
1131
|
+
if (options.fix && existsSync4(RSP_DIR)) {
|
|
1120
1132
|
try {
|
|
1121
1133
|
const update = await updateProject({ quiet: Boolean(options.json) });
|
|
1122
1134
|
fixed.push(...update.actions);
|
|
@@ -1136,17 +1148,12 @@ async function runDoctor(options = {}) {
|
|
|
1136
1148
|
const runtime = [];
|
|
1137
1149
|
const reportRuntime = (diagnostic) => recordRuntimeDiagnostic(runtime, diagnostic, Boolean(options.verbose) && !options.json);
|
|
1138
1150
|
const designPath = join6(RSP_DIR, "specs", "design.md");
|
|
1139
|
-
|
|
1140
|
-
const archivesIndexPath = join6(RSP_DIR, "archives", "INDEX.md");
|
|
1141
|
-
reportCheck(checks, ".rsp exists", existsSync6(RSP_DIR), "Run: rsp init");
|
|
1151
|
+
reportCheck(checks, ".rsp exists", existsSync4(RSP_DIR), "Run: rsp init");
|
|
1142
1152
|
reportFallbackProtocol(checks);
|
|
1143
1153
|
await reportUnsupportedRules(checks, reportRuntime);
|
|
1144
|
-
reportCheck(checks, "specs/design.md exists",
|
|
1145
|
-
reportCheck(checks, "specs/INDEX.md exists", existsSync6(specsIndexPath), "Run: rsp update");
|
|
1146
|
-
reportCheck(checks, "archives/INDEX.md exists", existsSync6(archivesIndexPath), "Run: rsp update");
|
|
1154
|
+
reportCheck(checks, "specs/design.md exists", existsSync4(designPath), "Run: rsp init");
|
|
1147
1155
|
await checkAgents(checks, reportRuntime);
|
|
1148
|
-
await
|
|
1149
|
-
await checkGeneratedIndex(checks, reportRuntime, archivesIndexPath, "archives", "archives/INDEX.md has generated-index metadata", "Run: rsp update");
|
|
1156
|
+
await checkSpecsIndexes(checks, reportRuntime);
|
|
1150
1157
|
await checkArchiveNaming(checks);
|
|
1151
1158
|
const decisionRecordsConfigValid = await checkConfigSemantics(checks, reportRuntime);
|
|
1152
1159
|
if (decisionRecordsConfigValid) {
|
|
@@ -1174,7 +1181,7 @@ async function runDoctor(options = {}) {
|
|
|
1174
1181
|
if (fixed.length > 0) {
|
|
1175
1182
|
console.log(` ${pc.green("Fixed:")} ${fixed.join(", ")}`);
|
|
1176
1183
|
console.log();
|
|
1177
|
-
} else if (options.fix && !repairIssue &&
|
|
1184
|
+
} else if (options.fix && !repairIssue && existsSync4(RSP_DIR)) {
|
|
1178
1185
|
console.log(` ${pc.dim("No safe fixes needed.")}`);
|
|
1179
1186
|
console.log();
|
|
1180
1187
|
}
|
|
@@ -1192,7 +1199,7 @@ async function runDoctor(options = {}) {
|
|
|
1192
1199
|
}
|
|
1193
1200
|
function reportFallbackProtocol(checks) {
|
|
1194
1201
|
const canonical = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
1195
|
-
const obsoleteExists =
|
|
1202
|
+
const obsoleteExists = existsSync4(OBSOLETE_RSP_RULES_PATH);
|
|
1196
1203
|
if (canonical.issue) {
|
|
1197
1204
|
checks.push({
|
|
1198
1205
|
status: "issue",
|
|
@@ -1206,7 +1213,7 @@ function reportFallbackProtocol(checks) {
|
|
|
1206
1213
|
checks.push({
|
|
1207
1214
|
status: "issue",
|
|
1208
1215
|
label: "rsp-rules.md exists",
|
|
1209
|
-
hint:
|
|
1216
|
+
hint: existsSync4(RSP_DIR) ? "Run: rsp update" : "Run: rsp init"
|
|
1210
1217
|
});
|
|
1211
1218
|
}
|
|
1212
1219
|
if (obsoleteExists) {
|
|
@@ -1297,44 +1304,31 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
1297
1304
|
}
|
|
1298
1305
|
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md missing managed RSP block", hint: "Run: rsp init" });
|
|
1299
1306
|
}
|
|
1300
|
-
async function
|
|
1301
|
-
const
|
|
1302
|
-
if (
|
|
1303
|
-
checks.push({ status: "
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
if (!indexFile.exists)
|
|
1307
|
-
return;
|
|
1308
|
-
let content;
|
|
1309
|
-
try {
|
|
1310
|
-
content = await readFile7(path, "utf-8");
|
|
1311
|
-
} catch (error) {
|
|
1312
|
-
reportRuntime({
|
|
1313
|
-
code: "generated_index_read_failed",
|
|
1314
|
-
operation: "readFile",
|
|
1315
|
-
path,
|
|
1316
|
-
message: toErrorMessage(error)
|
|
1317
|
-
});
|
|
1318
|
-
checks.push({ status: "issue", label, message: `${label} could not be read`, hint: fixHint });
|
|
1307
|
+
async function checkSpecsIndexes(checks, reportRuntime) {
|
|
1308
|
+
const health = await inspectSpecsIndexHealth();
|
|
1309
|
+
if (health.ok) {
|
|
1310
|
+
checks.push({ status: "ok", label: "hierarchical Specs indexes are current generated files" });
|
|
1319
1311
|
return;
|
|
1320
1312
|
}
|
|
1321
|
-
|
|
1322
|
-
const fm = parseFrontmatter(content);
|
|
1323
|
-
if (fm?.kind === "generated-index" && fm.index_type === expectedIndexType) {
|
|
1324
|
-
checks.push({ status: "ok", label });
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
} catch (error) {
|
|
1313
|
+
for (const message of health.issues) {
|
|
1328
1314
|
reportRuntime({
|
|
1329
|
-
code: "
|
|
1330
|
-
operation: "
|
|
1331
|
-
path,
|
|
1332
|
-
message
|
|
1315
|
+
code: "specs_index_inspection_failed",
|
|
1316
|
+
operation: "inspectSpecsIndexHealth",
|
|
1317
|
+
path: ".rsp/specs",
|
|
1318
|
+
message
|
|
1333
1319
|
});
|
|
1334
|
-
checks.push({ status: "issue", label, message: `${label} could not parse metadata`, hint: fixHint });
|
|
1335
|
-
return;
|
|
1336
1320
|
}
|
|
1337
|
-
|
|
1321
|
+
const details = [
|
|
1322
|
+
...health.issues,
|
|
1323
|
+
health.stalePaths.length > 0 ? `missing or stale: ${health.stalePaths.join(", ")}` : "",
|
|
1324
|
+
health.obsoletePaths.length > 0 ? `recognized obsolete indexes: ${health.obsoletePaths.join(", ")}` : ""
|
|
1325
|
+
].filter(Boolean);
|
|
1326
|
+
checks.push({
|
|
1327
|
+
status: "issue",
|
|
1328
|
+
label: "hierarchical Specs indexes are current generated files",
|
|
1329
|
+
message: details.join("; "),
|
|
1330
|
+
hint: "Run: rsp update"
|
|
1331
|
+
});
|
|
1338
1332
|
}
|
|
1339
1333
|
async function checkArchiveNaming(checks) {
|
|
1340
1334
|
const archivesDir = join6(RSP_DIR, "archives");
|
|
@@ -1349,7 +1343,7 @@ async function checkArchiveNaming(checks) {
|
|
|
1349
1343
|
return;
|
|
1350
1344
|
}
|
|
1351
1345
|
checks.push({ status: "ok", label: "archives use supported managed paths" });
|
|
1352
|
-
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(
|
|
1346
|
+
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(relative2(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename3(fp)) === false);
|
|
1353
1347
|
if (invalidFiles.length === 0) {
|
|
1354
1348
|
checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
|
|
1355
1349
|
return;
|
|
@@ -1437,7 +1431,7 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
|
|
|
1437
1431
|
}
|
|
1438
1432
|
async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
1439
1433
|
const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
|
|
1440
|
-
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !
|
|
1434
|
+
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync4(DEFAULT_DECISION_RECORDS_PATH))
|
|
1441
1435
|
return;
|
|
1442
1436
|
let inspectionFailed = false;
|
|
1443
1437
|
const inactiveRecords = (await walkMarkdownFiles(DEFAULT_DECISION_RECORDS_PATH, {
|
|
@@ -1445,7 +1439,7 @@ async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
|
1445
1439
|
inspectionFailed = true;
|
|
1446
1440
|
reportRuntime(diagnostic);
|
|
1447
1441
|
}
|
|
1448
|
-
})).map((path) => normalizeLogicalPath(
|
|
1442
|
+
})).map((path) => normalizeLogicalPath(relative2(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
|
|
1449
1443
|
if (inspectionFailed) {
|
|
1450
1444
|
checks.push({
|
|
1451
1445
|
status: "issue",
|
|
@@ -1555,8 +1549,8 @@ async function checkActiveChangeConsistency(checks) {
|
|
|
1555
1549
|
}
|
|
1556
1550
|
|
|
1557
1551
|
// src/commands/focus.ts
|
|
1558
|
-
import { existsSync as
|
|
1559
|
-
import { mkdir as mkdir6, unlink as
|
|
1552
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1553
|
+
import { mkdir as mkdir6, unlink as unlink7 } from "fs/promises";
|
|
1560
1554
|
import { dirname as dirname3 } from "path";
|
|
1561
1555
|
async function focusChange(name) {
|
|
1562
1556
|
if (!name) {
|
|
@@ -1589,9 +1583,9 @@ async function unfocusChange(name) {
|
|
|
1589
1583
|
return await withRspLock("unfocus-change", async () => {
|
|
1590
1584
|
const workRef = resolveWorkRef(name, { executable: false, mustExist: false });
|
|
1591
1585
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
1592
|
-
if (!
|
|
1586
|
+
if (!existsSync5(focusEntry))
|
|
1593
1587
|
throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
|
|
1594
|
-
await
|
|
1588
|
+
await unlink7(focusEntry);
|
|
1595
1589
|
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
1596
1590
|
console.log(` ${pc.green("Unfocused:")} ${name}`);
|
|
1597
1591
|
console.log(` ${pc.dim("focus.d cleared")} \u2192 ${name}`);
|
|
@@ -1682,11 +1676,6 @@ async function closeChangeGroup(name) {
|
|
|
1682
1676
|
} catch (error) {
|
|
1683
1677
|
warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
1684
1678
|
}
|
|
1685
|
-
try {
|
|
1686
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
1687
|
-
} catch (error) {
|
|
1688
|
-
warnings.push(`archive index rebuild failed: ${toErrorMessage(error)}`);
|
|
1689
|
-
}
|
|
1690
1679
|
console.log(` ${pc.green("Closed Change Group:")} ${name}`);
|
|
1691
1680
|
console.log(` ${pc.dim("Archived Group Brief:")} ${join7(archiveDirectory, archiveName)}
|
|
1692
1681
|
`);
|
|
@@ -1780,7 +1769,8 @@ async function showHistory(input = {}, options = {}) {
|
|
|
1780
1769
|
since: query.since ?? null,
|
|
1781
1770
|
until: query.until ?? null,
|
|
1782
1771
|
kind: query.kind ?? null,
|
|
1783
|
-
group: query.group ?? null
|
|
1772
|
+
group: query.group ?? null,
|
|
1773
|
+
search: query.search ?? null
|
|
1784
1774
|
},
|
|
1785
1775
|
records: listed.records,
|
|
1786
1776
|
summary: listed.summary,
|
|
@@ -1800,7 +1790,7 @@ function validateHistoryQuery(input) {
|
|
|
1800
1790
|
if (input.workRef) {
|
|
1801
1791
|
if (!WORK_REF_RE.test(input.workRef) || input.workRef.endsWith("/brief") || input.workRef.endsWith("/00-brief"))
|
|
1802
1792
|
return invalid("invalid_history_work_ref", "history WorkRef must be a flat or one-Group-level executable Change identity");
|
|
1803
|
-
if (input.limit !== void 0 || input.since !== void 0 || input.until !== void 0 || input.kind !== void 0 || input.group !== void 0)
|
|
1793
|
+
if (input.limit !== void 0 || input.since !== void 0 || input.until !== void 0 || input.kind !== void 0 || input.group !== void 0 || input.search !== void 0)
|
|
1804
1794
|
return invalid("history_detail_filters_unsupported", "list filters cannot be combined with an exact history detail lookup");
|
|
1805
1795
|
return { ok: true, query: {} };
|
|
1806
1796
|
}
|
|
@@ -1817,6 +1807,8 @@ function validateHistoryQuery(input) {
|
|
|
1817
1807
|
return invalid("invalid_history_kind", "--kind must be a non-empty exact historical kind");
|
|
1818
1808
|
if (input.group !== void 0 && !GROUP_RE.test(input.group))
|
|
1819
1809
|
return invalid("invalid_history_group", "--group must be one lowercase kebab-case Group name");
|
|
1810
|
+
if (input.search !== void 0 && input.search.trim() === "")
|
|
1811
|
+
return invalid("invalid_history_search", "--search must be non-empty");
|
|
1820
1812
|
return {
|
|
1821
1813
|
ok: true,
|
|
1822
1814
|
query: {
|
|
@@ -1824,7 +1816,8 @@ function validateHistoryQuery(input) {
|
|
|
1824
1816
|
since: input.since,
|
|
1825
1817
|
until: input.until,
|
|
1826
1818
|
kind: input.kind?.trim(),
|
|
1827
|
-
group: input.group
|
|
1819
|
+
group: input.group,
|
|
1820
|
+
search: input.search?.trim()
|
|
1828
1821
|
}
|
|
1829
1822
|
};
|
|
1830
1823
|
}
|
|
@@ -1916,7 +1909,7 @@ function printHistoryDetail(result) {
|
|
|
1916
1909
|
}
|
|
1917
1910
|
|
|
1918
1911
|
// src/commands/init.ts
|
|
1919
|
-
import { existsSync as
|
|
1912
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1920
1913
|
import { mkdir as mkdir8, readFile as readFile8 } from "fs/promises";
|
|
1921
1914
|
import { join as join8 } from "path";
|
|
1922
1915
|
function generateConfigTemplate() {
|
|
@@ -1934,7 +1927,7 @@ ${fmtList(VALID_KINDS, 0)}
|
|
|
1934
1927
|
|
|
1935
1928
|
# New projects default to automatic Manage routing with lifecycle-only closeout.
|
|
1936
1929
|
# Use activation: explicit to require a named managed request.
|
|
1937
|
-
# closeout accepts manual, lifecycle, or local; local
|
|
1930
|
+
# closeout accepts manual, lifecycle, or local; local routes a qualified clean terminal non-small boundary to one local commit but never push or publication.
|
|
1938
1931
|
manage:
|
|
1939
1932
|
activation: auto
|
|
1940
1933
|
closeout: lifecycle
|
|
@@ -1952,7 +1945,7 @@ function toTitle(projectName) {
|
|
|
1952
1945
|
return projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
1953
1946
|
}
|
|
1954
1947
|
async function initProject(args = {}) {
|
|
1955
|
-
const isNew = !
|
|
1948
|
+
const isNew = !existsSync6(RSP_DIR);
|
|
1956
1949
|
const projectName = await detectProjectName();
|
|
1957
1950
|
const mode = args.agentsMode ?? "managed";
|
|
1958
1951
|
if (mode !== "managed" && mode !== "print") {
|
|
@@ -1967,7 +1960,7 @@ async function initProject(args = {}) {
|
|
|
1967
1960
|
const agentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
|
|
1968
1961
|
if (agentsInspection.issue)
|
|
1969
1962
|
throw agentsInspection.issue;
|
|
1970
|
-
if (
|
|
1963
|
+
if (existsSync6(CONFIG_PATH)) {
|
|
1971
1964
|
clearConfigCache();
|
|
1972
1965
|
const existingConfig = await inspectRspConfig();
|
|
1973
1966
|
if (existingConfig.issues.length > 0)
|
|
@@ -1997,12 +1990,9 @@ async function initProject(args = {}) {
|
|
|
1997
1990
|
throw new Error(configInspection.issues.join("; "));
|
|
1998
1991
|
const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
|
|
1999
1992
|
created = await ensureDecisionRecordsDirectory(decisionRecordsPath) || created;
|
|
2000
|
-
const createdSpecsIndex = await ensureFile(join8(RSP_DIR, "specs", "INDEX.md"), "# Specs Index\n\n_Additional project-level specs beyond `design.md`._\n");
|
|
2001
|
-
created = createdSpecsIndex || created;
|
|
2002
1993
|
const createdDesign = await ensureFile(join8(RSP_DIR, "specs", "design.md"), generateDesignContent(projectName));
|
|
2003
1994
|
created = createdDesign || created;
|
|
2004
|
-
|
|
2005
|
-
created = createdArchivesIndex || created;
|
|
1995
|
+
created = await removeLegacyArchiveIndex() || created;
|
|
2006
1996
|
created = await ensureFile(join8(CHANGES_DIR, ".gitkeep"), "") || created;
|
|
2007
1997
|
created = await ensureFile(join8(FOCUS_DIR, ".gitkeep"), "") || created;
|
|
2008
1998
|
created = await ensureFile(join8(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n") || created;
|
|
@@ -2013,10 +2003,7 @@ async function initProject(args = {}) {
|
|
|
2013
2003
|
created = await ensureFile(changePath, generateChangeContent("project-setup")) || created;
|
|
2014
2004
|
created = await ensureFile(focusMarker, "") || created;
|
|
2015
2005
|
}
|
|
2016
|
-
|
|
2017
|
-
await buildSpecsIndex({ acquireLock: false });
|
|
2018
|
-
if (createdArchivesIndex)
|
|
2019
|
-
await buildArchiveIndex({ acquireLock: false });
|
|
2006
|
+
created = await buildSpecsIndex({ acquireLock: false }) || created;
|
|
2020
2007
|
const title = toTitle(projectName);
|
|
2021
2008
|
let agentsUpdated = false;
|
|
2022
2009
|
if (!agentsInspection.exists) {
|
|
@@ -2192,8 +2179,126 @@ function formatArchiveReady(value) {
|
|
|
2192
2179
|
}
|
|
2193
2180
|
}
|
|
2194
2181
|
|
|
2182
|
+
// src/commands/reopen.ts
|
|
2183
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2184
|
+
import { mkdir as mkdir9, readFile as readFile10, unlink as unlink8, writeFile as writeFile5 } from "fs/promises";
|
|
2185
|
+
import { dirname as dirname4 } from "path";
|
|
2186
|
+
async function reopenChange(name, options) {
|
|
2187
|
+
if (!name) {
|
|
2188
|
+
console.error(` ${pc.red("Usage:")} rsp reopen <name> --reason <text> [--from <archive-path>]`);
|
|
2189
|
+
process.exit(1);
|
|
2190
|
+
}
|
|
2191
|
+
const reason = options.reason?.trim();
|
|
2192
|
+
if (!reason || /[\r\n]/.test(reason)) {
|
|
2193
|
+
console.error(` ${pc.red("Error:")} --reason must be one non-empty line`);
|
|
2194
|
+
process.exit(1);
|
|
2195
|
+
}
|
|
2196
|
+
guardRspInitialized();
|
|
2197
|
+
try {
|
|
2198
|
+
await withRspLock("reopen-change", async () => {
|
|
2199
|
+
const inspection = await inspectArchiveHistory();
|
|
2200
|
+
if (!historyInspectionComplete(inspection)) {
|
|
2201
|
+
throw new ArchiveHistoryError(
|
|
2202
|
+
"archive_inspection_incomplete",
|
|
2203
|
+
"archive history inspection is incomplete; resolve the reported diagnostics before reopening work"
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
const record = selectArchiveHistoryRecord(
|
|
2207
|
+
inspection.records,
|
|
2208
|
+
options.from ? { path: options.from } : { workRef: name }
|
|
2209
|
+
);
|
|
2210
|
+
if (record.workRef !== name) {
|
|
2211
|
+
throw new ArchiveHistoryError(
|
|
2212
|
+
"archive_identity_mismatch",
|
|
2213
|
+
`selected archive belongs to ${record.workRef}, not ${name}`
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
let workRef;
|
|
2217
|
+
try {
|
|
2218
|
+
workRef = resolveWorkRef(name, { executable: true });
|
|
2219
|
+
} catch (error) {
|
|
2220
|
+
if (error instanceof WorkRefError && error.code === "group_brief_missing") {
|
|
2221
|
+
throw new ArchiveHistoryError(
|
|
2222
|
+
"archived_group_closed",
|
|
2223
|
+
`cannot reopen ${name} because its Change Group is closed; reopen the Group through a separately authorized lifecycle operation first`
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
throw error;
|
|
2227
|
+
}
|
|
2228
|
+
if (existsSync7(workRef.path))
|
|
2229
|
+
throw new ArchiveHistoryError("open_change_exists", `open Change already exists: ${name}`);
|
|
2230
|
+
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
2231
|
+
if (existsSync7(focusEntry))
|
|
2232
|
+
throw new ArchiveHistoryError("focus_marker_exists", `focus marker already exists for non-open Change: ${name}; run rsp doctor`);
|
|
2233
|
+
const archivedContent = await readFile10(record.sourcePath, "utf-8");
|
|
2234
|
+
const reopenedContent = appendChecklistItem(
|
|
2235
|
+
appendChecklistItem(archivedContent, "Tasks", `- [ ] Resolve reopened concern: ${reason}`),
|
|
2236
|
+
"Verify",
|
|
2237
|
+
`- [ ] Verify reopened concern: ${reason}`
|
|
2238
|
+
);
|
|
2239
|
+
await mkdir9(dirname4(workRef.path), { recursive: true });
|
|
2240
|
+
let changeCreated = false;
|
|
2241
|
+
try {
|
|
2242
|
+
await writeFile5(workRef.path, reopenedContent, { flag: "wx" });
|
|
2243
|
+
changeCreated = true;
|
|
2244
|
+
await mkdir9(FOCUS_DIR, { recursive: true });
|
|
2245
|
+
await mkdir9(dirname4(focusEntry), { recursive: true });
|
|
2246
|
+
await writeManagedFile(focusEntry, "", "focus marker");
|
|
2247
|
+
} catch (error) {
|
|
2248
|
+
if (changeCreated) {
|
|
2249
|
+
try {
|
|
2250
|
+
await unlink8(workRef.path);
|
|
2251
|
+
await cleanupEmptyParentDirs(workRef.path, CHANGES_DIR);
|
|
2252
|
+
} catch {
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
throw error;
|
|
2256
|
+
}
|
|
2257
|
+
console.log(` ${pc.green("Reopened:")} ${name}`);
|
|
2258
|
+
console.log(` ${pc.dim("retained archive")} \u2192 ${record.path}`);
|
|
2259
|
+
console.log(` ${pc.dim("focused via focus.d")} \u2192 ${name}`);
|
|
2260
|
+
console.log(` ${pc.cyan("Next:")} refine the reopened Task and Verify evidence, then resolve the concern
|
|
2261
|
+
`);
|
|
2262
|
+
if (existsSync7(".git")) {
|
|
2263
|
+
console.log(` ${pc.cyan("Git delivery:")}`);
|
|
2264
|
+
console.log(` git status --short`);
|
|
2265
|
+
console.log(` Inspect the complete reopen transition; stage and commit only with separate Git authority.`);
|
|
2266
|
+
console.log();
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
if (error instanceof ArchiveHistoryError || error instanceof WorkRefError) {
|
|
2271
|
+
console.error(` ${pc.red("Error:")} ${error.message}`);
|
|
2272
|
+
if (error instanceof ArchiveHistoryError) {
|
|
2273
|
+
for (const candidate of error.candidates)
|
|
2274
|
+
console.error(` ${candidate}`);
|
|
2275
|
+
if (error.candidatesTruncated)
|
|
2276
|
+
console.error(` ... ${error.candidateTotal - error.candidates.length} more`);
|
|
2277
|
+
}
|
|
2278
|
+
process.exit(1);
|
|
2279
|
+
}
|
|
2280
|
+
throw error;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
function appendChecklistItem(content, heading, item) {
|
|
2284
|
+
const headings = [...content.matchAll(new RegExp(`^## ${heading}[ \\t]*\\r?$`, "gm"))];
|
|
2285
|
+
if (headings.length !== 1) {
|
|
2286
|
+
throw new ArchiveHistoryError(
|
|
2287
|
+
"archive_section_invalid",
|
|
2288
|
+
`archived Change must contain exactly one required section: ${heading}`
|
|
2289
|
+
);
|
|
2290
|
+
}
|
|
2291
|
+
const sectionBodyStart = headings[0].index + headings[0][0].length;
|
|
2292
|
+
const nextHeading = content.indexOf("\n## ", sectionBodyStart);
|
|
2293
|
+
const insertAt = nextHeading < 0 ? content.length : nextHeading;
|
|
2294
|
+
const before = content.slice(0, insertAt);
|
|
2295
|
+
const separator = before.endsWith("\n\n") ? "" : before.endsWith("\n") ? "\n" : "\n\n";
|
|
2296
|
+
return `${before}${separator}${item}
|
|
2297
|
+
${content.slice(insertAt)}`;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2195
2300
|
// src/commands/show.ts
|
|
2196
|
-
import { readFile as
|
|
2301
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2197
2302
|
function exitShowError(error, options) {
|
|
2198
2303
|
const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
|
|
2199
2304
|
if (options.json) {
|
|
@@ -2251,7 +2356,7 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
2251
2356
|
const srcPath = workRef.path;
|
|
2252
2357
|
let content;
|
|
2253
2358
|
try {
|
|
2254
|
-
content = await
|
|
2359
|
+
content = await readFile11(srcPath, "utf-8");
|
|
2255
2360
|
} catch {
|
|
2256
2361
|
exitShowError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, options);
|
|
2257
2362
|
}
|
|
@@ -2788,7 +2893,7 @@ var groupCommand = defineCommand({
|
|
|
2788
2893
|
var addSpecCommand = defineCommand({
|
|
2789
2894
|
meta: {
|
|
2790
2895
|
name: "spec",
|
|
2791
|
-
description: "Create .rsp/specs/<name>.md and
|
|
2896
|
+
description: "Create .rsp/specs/<name>.md and refresh local Specs indexes"
|
|
2792
2897
|
},
|
|
2793
2898
|
args: {
|
|
2794
2899
|
name: {
|
|
@@ -2823,12 +2928,42 @@ var archiveCommand = defineCommand({
|
|
|
2823
2928
|
},
|
|
2824
2929
|
"dry-run": {
|
|
2825
2930
|
type: "boolean",
|
|
2826
|
-
description: "
|
|
2931
|
+
description: "Deprecated compatibility route to rsp ready; never moves the Change",
|
|
2827
2932
|
default: false
|
|
2828
2933
|
}
|
|
2829
2934
|
},
|
|
2830
2935
|
async run({ args }) {
|
|
2831
|
-
|
|
2936
|
+
if (args["dry-run"]) {
|
|
2937
|
+
console.error(" Deprecated: use `rsp ready <name>` for read-only archive readiness.");
|
|
2938
|
+
await showReady(args.name);
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
await archiveChange(args.name);
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
var reopenCommand = defineCommand({
|
|
2945
|
+
meta: {
|
|
2946
|
+
name: "reopen",
|
|
2947
|
+
description: "Restore one archived Change as focused open work while retaining history"
|
|
2948
|
+
},
|
|
2949
|
+
args: {
|
|
2950
|
+
name: {
|
|
2951
|
+
type: "positional",
|
|
2952
|
+
description: "Archived Change WorkRef",
|
|
2953
|
+
required: true
|
|
2954
|
+
},
|
|
2955
|
+
from: {
|
|
2956
|
+
type: "string",
|
|
2957
|
+
description: "Exact .rsp/archives/... path when the WorkRef has multiple archives"
|
|
2958
|
+
},
|
|
2959
|
+
reason: {
|
|
2960
|
+
type: "string",
|
|
2961
|
+
description: "One-line concern that makes the restored Change unfinished",
|
|
2962
|
+
required: true
|
|
2963
|
+
}
|
|
2964
|
+
},
|
|
2965
|
+
async run({ args }) {
|
|
2966
|
+
await reopenChange(args.name, { from: args.from, reason: args.reason });
|
|
2832
2967
|
}
|
|
2833
2968
|
});
|
|
2834
2969
|
var focusCommand = defineCommand({
|
|
@@ -2969,7 +3104,7 @@ var checkCommand = defineCommand({
|
|
|
2969
3104
|
var updateCommand = defineCommand({
|
|
2970
3105
|
meta: {
|
|
2971
3106
|
name: "update",
|
|
2972
|
-
description: "Refresh RSP project
|
|
3107
|
+
description: "Refresh RSP-managed project files after upgrade; does not update packaged Skills"
|
|
2973
3108
|
},
|
|
2974
3109
|
async run() {
|
|
2975
3110
|
const result = await updateProject();
|
|
@@ -3176,6 +3311,10 @@ var historyCommand = defineCommand({
|
|
|
3176
3311
|
type: "string",
|
|
3177
3312
|
description: "Match one exact Change Group"
|
|
3178
3313
|
},
|
|
3314
|
+
search: {
|
|
3315
|
+
type: "string",
|
|
3316
|
+
description: "Case-insensitive literal match over WorkRef and summary"
|
|
3317
|
+
},
|
|
3179
3318
|
json: {
|
|
3180
3319
|
type: "boolean",
|
|
3181
3320
|
description: "Print machine-readable JSON output",
|
|
@@ -3195,6 +3334,7 @@ var historyCommand = defineCommand({
|
|
|
3195
3334
|
until: args.until,
|
|
3196
3335
|
kind: args.kind,
|
|
3197
3336
|
group: args.group,
|
|
3337
|
+
search: args.search,
|
|
3198
3338
|
positionalCount: args._?.length ?? (args.name ? 1 : 0)
|
|
3199
3339
|
}, { json: Boolean(args.json), compact: Boolean(args.compact) });
|
|
3200
3340
|
if (!result.ok)
|
|
@@ -3212,7 +3352,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
3212
3352
|
if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
|
|
3213
3353
|
const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
|
|
3214
3354
|
const locale = resolveUiLocale("auto", process.env.RSP_UI_LANG, process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
3215
|
-
const { runSkillsTui } = await import("./entry-
|
|
3355
|
+
const { runSkillsTui } = await import("./entry-WZDVKDUM.mjs");
|
|
3216
3356
|
const selection = await runSkillsTui(locale);
|
|
3217
3357
|
if (selection.kind === "confirmed") {
|
|
3218
3358
|
const result = await installPackagedSkills({ names: selection.names, force: selection.force });
|
|
@@ -3230,7 +3370,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
3230
3370
|
} else {
|
|
3231
3371
|
const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
|
|
3232
3372
|
const locale = resolveUiLocale(lang, process.env.RSP_UI_LANG, process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
3233
|
-
const { runTui } = await import("./entry-
|
|
3373
|
+
const { runTui } = await import("./entry-X23GPCVS.mjs");
|
|
3234
3374
|
const exitCode = await runTui(locale);
|
|
3235
3375
|
if (exitCode !== 0)
|
|
3236
3376
|
process.exitCode = exitCode;
|
|
@@ -3253,6 +3393,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
3253
3393
|
focus: focusCommand,
|
|
3254
3394
|
unfocus: unfocusCommand,
|
|
3255
3395
|
archive: archiveCommand,
|
|
3396
|
+
reopen: reopenCommand,
|
|
3256
3397
|
ready: readyCommand,
|
|
3257
3398
|
show: showCommand,
|
|
3258
3399
|
history: historyCommand,
|