@oevery/rsp 3.3.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -17
- package/README.zh-CN.md +25 -18
- package/dist/chunk-JO6NDJM4.mjs +360 -0
- package/dist/chunk-LGDCIQYL.mjs +725 -0
- package/dist/{chunk-7HXQNBIG.mjs → chunk-P3KO3GZW.mjs} +420 -268
- package/dist/chunk-U56XMIOO.mjs +66 -0
- package/dist/{chunk-TSUDEX5Y.mjs → chunk-VDRXFKBP.mjs} +142 -867
- package/dist/cli.mjs +1111 -2347
- package/dist/client-EHRWH5Q7.mjs +108 -0
- package/dist/{entry-GIDNR7CJ.mjs → entry-NDASTAI5.mjs} +128 -38
- package/dist/viewer/NOTICE.txt +299 -0
- package/dist/viewer/app.css +2 -0
- package/dist/viewer/app.js +670 -0
- package/dist/viewer-build.json +1 -0
- package/dist/viewer-service.mjs +551 -0
- package/package.json +51 -25
- package/skills/rsp/SKILL.md +44 -38
- package/skills/rsp/references/architecture.md +39 -0
- package/skills/rsp/references/closeout.md +38 -0
- package/skills/rsp/references/context-migration.md +22 -0
- package/skills/rsp/references/control-outcome.md +3 -29
- package/skills/rsp/references/coordination-recovery.md +25 -0
- package/skills/rsp/references/coordination-review.md +15 -0
- package/skills/rsp/references/coordination.md +59 -0
- package/skills/rsp/references/durable-review.md +60 -12
- package/skills/rsp/references/focus-continuation.md +12 -4
- package/skills/rsp/references/groups-dependencies.md +6 -4
- package/skills/rsp/references/response-language.md +2 -0
- package/skills/rsp/references/setup-repair.md +42 -7
- package/skills/rsp/references/work-model.md +37 -0
- package/skills/rsp-commit/SKILL.md +76 -31
- package/skills/rsp-commit/references/message.md +34 -0
- package/skills/rsp-doc/SKILL.md +68 -0
- package/skills/rsp-doc/agents/openai.yaml +3 -0
- package/skills/rsp-doc/references/release-communication.md +39 -0
- package/skills/rsp-doc/references/skill-authoring.md +36 -0
- package/skills/rsp-implement/SKILL.md +60 -25
- package/skills/rsp-implement/references/diagnosis.md +10 -0
- package/skills/rsp-implement/references/finding-resolution.md +21 -0
- package/skills/rsp-implement/references/test-first.md +7 -0
- package/skills/rsp-review/SKILL.md +46 -45
- package/skills/rsp-review/references/code-review.md +30 -9
- package/skills/rsp-review/references/document-review.md +24 -9
- package/skills/rsp-review/references/release-communication.md +21 -0
- package/skills/rsp-shape/SKILL.md +26 -45
- package/skills/rsp-shape/references/complex-shaping.md +3 -1
- package/skills/rsp-shape/references/deep-clarification.md +4 -4
- package/skills/rsp-shape/references/domain-modeling.md +13 -0
- package/skills/rsp-shape/references/module-seams.md +13 -0
- package/skills/rsp-shape/references/planning.md +42 -0
- package/skills/{rsp-design → rsp-shape}/references/reversible-exploration.md +2 -2
- package/skills/rsp-verify/SKILL.md +39 -12
- package/dist/chunk-JOMFFQRG.mjs +0 -99
- package/dist/chunk-QM2UVPQK.mjs +0 -375
- package/dist/entry-YAW3PYFA.mjs +0 -174
- package/rules/rsp-rules.md +0 -48
- package/skills/rsp/references/implementation-evidence.md +0 -12
- package/skills/rsp/references/managed-routing.md +0 -45
- package/skills/rsp-design/SKILL.md +0 -53
- package/skills/rsp-design/references/domain-modeling.md +0 -11
- package/skills/rsp-design/references/module-seams.md +0 -11
- package/skills/rsp-diagnose/SKILL.md +0 -48
- package/skills/rsp-manage/SKILL.md +0 -101
- package/skills/rsp-manage/references/closeout.md +0 -13
- package/skills/rsp-manage/references/delegation.md +0 -47
- package/skills/rsp-manage/references/interruption-recovery.md +0 -19
- package/skills/rsp-manage/references/review-convergence.md +0 -9
- package/skills/rsp-release-docs/SKILL.md +0 -77
- package/skills/rsp-release-docs/references/convention-discovery.md +0 -65
- package/skills/rsp-release-docs/references/evidence-and-surfaces.md +0 -21
- package/skills/rsp-release-docs/references/output-contracts.md +0 -97
- package/skills/rsp-release-docs/references/publication-lifecycle.md +0 -28
- package/skills/rsp-resolve-findings/SKILL.md +0 -61
- package/skills/rsp-structural-audit/SKILL.md +0 -71
- package/skills/rsp-structural-audit/references/structural-lenses.md +0 -41
- package/skills/rsp-tdd/SKILL.md +0 -56
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
ARCHITECTURE_FILE,
|
|
4
|
+
PROJECT_VERSION_PATH,
|
|
5
|
+
SOURCE_INVOCATION,
|
|
6
|
+
compareVersions,
|
|
7
|
+
exactInvocation,
|
|
8
|
+
guardProjectVersion,
|
|
9
|
+
isExactVersion,
|
|
10
|
+
queryArchitecture,
|
|
11
|
+
readProjectRuntime,
|
|
12
|
+
readProjectVersion,
|
|
13
|
+
selectProjectRuntime
|
|
14
|
+
} from "./chunk-JO6NDJM4.mjs";
|
|
8
15
|
import {
|
|
9
16
|
ArchiveHistoryError,
|
|
10
17
|
GROUP_BRIEF_FILENAME,
|
|
@@ -12,21 +19,13 @@ import {
|
|
|
12
19
|
HISTORY_DEFAULT_LIMIT,
|
|
13
20
|
HISTORY_MAX_LIMIT,
|
|
14
21
|
IssueRelationshipError,
|
|
15
|
-
SPECS_DEFAULT_RESULT_LIMIT,
|
|
16
|
-
SPECS_DETAIL_CONTENT_CODE_POINTS,
|
|
17
|
-
SPECS_MAX_FILE_BYTES,
|
|
18
|
-
SPECS_MAX_RESULT_LIMIT,
|
|
19
|
-
SpecsQueryError,
|
|
20
22
|
WorkRefError,
|
|
21
23
|
buildDurableReviewGuidance,
|
|
22
|
-
cleanupEmptyParentDirs,
|
|
23
24
|
collectArchiveReadiness,
|
|
24
25
|
createIssueRelationship,
|
|
25
|
-
detectProjectName,
|
|
26
26
|
generateGroupBriefContent,
|
|
27
27
|
getDurableReviewCandidateTargets,
|
|
28
28
|
getGroupReopenEvidence,
|
|
29
|
-
guardRspInitialized,
|
|
30
29
|
hasArchivedGroupBrief,
|
|
31
30
|
historyInspectionComplete,
|
|
32
31
|
inspectArchiveHistory,
|
|
@@ -35,13 +34,8 @@ import {
|
|
|
35
34
|
inspectChangeGroups,
|
|
36
35
|
inspectFocusTree,
|
|
37
36
|
inspectProjectStatus,
|
|
38
|
-
inspectSpecs,
|
|
39
|
-
inspectUnsupportedRules,
|
|
40
37
|
inspectWorkTree,
|
|
41
|
-
isRecognizedGeneratedSpecsIndex,
|
|
42
|
-
isValidSpecName,
|
|
43
38
|
normalizeExecutableWorkRef,
|
|
44
|
-
normalizeLogicalPath,
|
|
45
39
|
normalizeWorkRefSegment,
|
|
46
40
|
parseIssueRelationships,
|
|
47
41
|
projectDependencyForest,
|
|
@@ -56,10 +50,27 @@ import {
|
|
|
56
50
|
resolveWorkRef,
|
|
57
51
|
selectArchiveHistoryRecord,
|
|
58
52
|
selectArchivedGroupBrief,
|
|
53
|
+
toArchiveReadinessOutput
|
|
54
|
+
} from "./chunk-VDRXFKBP.mjs";
|
|
55
|
+
import {
|
|
56
|
+
SPECS_DEFAULT_RESULT_LIMIT,
|
|
57
|
+
SPECS_DETAIL_CONTENT_CODE_POINTS,
|
|
58
|
+
SPECS_MAX_RESULT_LIMIT,
|
|
59
|
+
SpecsQueryError,
|
|
60
|
+
cleanupEmptyParentDirs,
|
|
61
|
+
detectProjectName,
|
|
62
|
+
emitJson,
|
|
63
|
+
guardRspInitialized,
|
|
64
|
+
inspectRspInitialization,
|
|
65
|
+
inspectSpecs,
|
|
66
|
+
inspectUnsupportedRules,
|
|
67
|
+
isRecognizedGeneratedSpecsIndex,
|
|
68
|
+
isValidSpecName,
|
|
69
|
+
normalizeLogicalPath,
|
|
59
70
|
specsInspectionComplete,
|
|
60
|
-
|
|
71
|
+
toErrorMessage,
|
|
61
72
|
walkMarkdownFiles
|
|
62
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-LGDCIQYL.mjs";
|
|
63
74
|
import {
|
|
64
75
|
ARCHIVES_DIR,
|
|
65
76
|
CHANGES_DIR,
|
|
@@ -80,46 +91,51 @@ import {
|
|
|
80
91
|
clearConfigCache,
|
|
81
92
|
countCheckboxes,
|
|
82
93
|
detectDeltaSections,
|
|
83
|
-
emitJson,
|
|
84
|
-
ensureDecisionRecordsDirectory,
|
|
85
94
|
ensureManagedFile,
|
|
86
95
|
generateConfigTemplate,
|
|
87
|
-
getDocumentSectionBody,
|
|
88
96
|
getDocumentSectionDefinitionByHeading,
|
|
97
|
+
getDocumentSectionMarkdown,
|
|
89
98
|
getDocumentSections,
|
|
90
99
|
getDocumentTitles,
|
|
91
100
|
getVersion,
|
|
101
|
+
getVisibleBodyLines,
|
|
92
102
|
hasMeaningfulBlockers,
|
|
93
103
|
inspectManagedDirectory,
|
|
94
104
|
inspectManagedFile,
|
|
95
105
|
inspectManagedFileTree,
|
|
96
106
|
inspectRspConfig,
|
|
97
107
|
loadRspConfig,
|
|
108
|
+
markdownProse,
|
|
109
|
+
markdownText,
|
|
110
|
+
markdownTopLevelComments,
|
|
98
111
|
parseFrontmatter,
|
|
112
|
+
parseMarkdown,
|
|
99
113
|
parseRspDocument,
|
|
100
114
|
parseScenarios,
|
|
101
115
|
pc,
|
|
102
|
-
reconcileRspConfigDefaults,
|
|
103
116
|
renderDocumentSectionHeading,
|
|
104
117
|
renderDocumentTitle,
|
|
105
118
|
requireManagedDirectory,
|
|
106
119
|
requireManagedFile,
|
|
107
120
|
resolveDecisionRecordsPath,
|
|
108
121
|
resolveKinds,
|
|
122
|
+
resolveLanguagePolicy,
|
|
109
123
|
resolveManagedDirectoryChain,
|
|
110
124
|
resolveRequiredSections,
|
|
111
|
-
|
|
125
|
+
resolveWorkflowPolicy,
|
|
112
126
|
validateDecisionRecordsFilesystemPath,
|
|
113
127
|
writeManagedFile
|
|
114
|
-
} from "./chunk-
|
|
128
|
+
} from "./chunk-P3KO3GZW.mjs";
|
|
115
129
|
|
|
116
130
|
// src/cli.ts
|
|
117
|
-
import { resolve as
|
|
131
|
+
import { resolve as resolve2 } from "path";
|
|
118
132
|
import { fileURLToPath } from "url";
|
|
119
133
|
import { runMain } from "citty";
|
|
120
134
|
|
|
121
135
|
// src/cli/capabilities.ts
|
|
122
136
|
var CLI_COMMAND_CAPABILITIES = {
|
|
137
|
+
"architecture": { json: true, compact: true },
|
|
138
|
+
"config": { json: true, compact: true },
|
|
123
139
|
"status": { json: true, compact: true },
|
|
124
140
|
"show": { json: true, compact: true },
|
|
125
141
|
"ready": { json: true, compact: true },
|
|
@@ -152,10 +168,49 @@ function supportsCompact(command) {
|
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
// src/cli/registry.ts
|
|
155
|
-
import { defineCommand as
|
|
171
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
156
172
|
|
|
157
|
-
// src/cli/commands/
|
|
173
|
+
// src/cli/commands/architecture.ts
|
|
158
174
|
import { defineCommand } from "citty";
|
|
175
|
+
var architectureCommand = defineCommand({
|
|
176
|
+
meta: { name: "architecture", description: "Read full or selected architecture structure without document bodies" },
|
|
177
|
+
args: { node: { type: "string", description: "Node and descendants" }, view: { type: "string", description: "Explicit view selection" }, ...compactJsonArgs },
|
|
178
|
+
async run({ args }) {
|
|
179
|
+
const unknown = Object.keys(args).filter((key) => !["node", "view", "json", "compact", "_"].includes(key));
|
|
180
|
+
if (unknown.length || args._.length)
|
|
181
|
+
throw new Error(`Unsupported architecture options/arguments: ${[...unknown, ...args._].join(", ")}`);
|
|
182
|
+
const result = await queryArchitecture({ node: args.node, view: args.view });
|
|
183
|
+
if (args.json) {
|
|
184
|
+
console.log(JSON.stringify(result, null, args.compact ? void 0 : 2));
|
|
185
|
+
} else {
|
|
186
|
+
console.log(`Architecture: ${result.configured ? result.source.root : "not configured"}
|
|
187
|
+
Selection: ${result.selection.kind}${result.selection.id ? ` ${result.selection.id}` : ""}`);
|
|
188
|
+
for (const node of result.nodes) {
|
|
189
|
+
console.log(`${node.boundary ? "[boundary] " : ""}${node.id}: ${node.title} \u2014 ${node.description}${node.parent ? ` (parent: ${node.parent})` : ""}`);
|
|
190
|
+
for (const binding of node.bindings) console.log(` ${binding.file}${binding.headings ? ` > ${binding.headings.join(" > ")}` : ""} (scope: ${binding.scope}; ${binding.inherited ? `inherited from ${binding.origin}` : `direct on ${binding.origin}`})`);
|
|
191
|
+
}
|
|
192
|
+
for (const edge of result.relations) console.log(`Relation ${edge.id}: ${edge.source} -> ${edge.target} [${edge.type}]: ${edge.description}`);
|
|
193
|
+
for (const view of result.views) console.log(`View ${view.id}: ${view.title} (owner: ${view.node}; include: ${view.include.join(", ")})`);
|
|
194
|
+
for (const diagnostic of result.diagnostics) console.log(`${diagnostic.code}: ${diagnostic.message}`);
|
|
195
|
+
if (result.configured)
|
|
196
|
+
console.log(`Source: ${result.source.file} @ ${result.source.revision}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
var viewCommand = defineCommand({
|
|
201
|
+
meta: { name: "view", description: "Open the shared local architecture viewer, inspect or stop its service" },
|
|
202
|
+
args: { action: { type: "positional", required: false, description: "status or stop; omit to open" }, open: { type: "boolean", default: true, description: "Open browser (use --no-open to print URL only)" } },
|
|
203
|
+
async run({ args }) {
|
|
204
|
+
const unknown = Object.keys(args).filter((key) => !["action", "open", "_"].includes(key));
|
|
205
|
+
if (unknown.length || args._.length > 1 || args.action && !args.open)
|
|
206
|
+
throw new Error("rsp view accepts only an optional status/stop action or --no-open");
|
|
207
|
+
const { runView } = await import("./client-EHRWH5Q7.mjs");
|
|
208
|
+
await runView(args.action, args.open);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// src/cli/commands/inspection.ts
|
|
213
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
159
214
|
|
|
160
215
|
// src/commands/check.ts
|
|
161
216
|
import { readFile } from "fs/promises";
|
|
@@ -164,9 +219,10 @@ import { readFile } from "fs/promises";
|
|
|
164
219
|
var choosePlaceholderRe = /^<choose:/i;
|
|
165
220
|
function inspectChangeDocument(content, options) {
|
|
166
221
|
const diagnostics = [];
|
|
222
|
+
const document = typeof content === "string" ? parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA) : content;
|
|
167
223
|
let frontmatter = null;
|
|
168
224
|
try {
|
|
169
|
-
frontmatter = parseFrontmatter(
|
|
225
|
+
frontmatter = parseFrontmatter(document.markdown);
|
|
170
226
|
} catch (error) {
|
|
171
227
|
const message = error instanceof Error ? error.message : String(error);
|
|
172
228
|
diagnostics.push({
|
|
@@ -182,7 +238,6 @@ function inspectChangeDocument(content, options) {
|
|
|
182
238
|
message: "missing YAML frontmatter"
|
|
183
239
|
});
|
|
184
240
|
}
|
|
185
|
-
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
186
241
|
for (const heading of options.requiredSections) {
|
|
187
242
|
const definition = getDocumentSectionDefinitionByHeading(CHANGE_DOCUMENT_SCHEMA, heading);
|
|
188
243
|
if (!definition)
|
|
@@ -257,7 +312,6 @@ function inspectChangeDocument(content, options) {
|
|
|
257
312
|
}
|
|
258
313
|
|
|
259
314
|
// src/core/focus-capsule.ts
|
|
260
|
-
import { Buffer } from "buffer";
|
|
261
315
|
import { TextDecoder } from "util";
|
|
262
316
|
var FOCUS_CAPSULE_VERSION = "<!-- rsp-focus:v1 -->";
|
|
263
317
|
var LEGACY_WARNING = "legacy focus capsule has no structured recovery projection";
|
|
@@ -275,15 +329,6 @@ function inspectFocusCapsuleBytes(bytes) {
|
|
|
275
329
|
} catch {
|
|
276
330
|
return { kind: "invalid", code: "focus_capsule_invalid_utf8", message: "focus capsule must contain valid UTF-8" };
|
|
277
331
|
}
|
|
278
|
-
if (Buffer.byteLength(content, "utf-8") > MAX_FOCUS_CAPSULE_BYTES) {
|
|
279
|
-
return {
|
|
280
|
-
kind: "invalid",
|
|
281
|
-
code: "focus_capsule_too_large",
|
|
282
|
-
message: `focus capsule exceeds ${MAX_FOCUS_CAPSULE_BYTES} UTF-8 bytes`
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
if (content.length === 0)
|
|
286
|
-
return { kind: "empty", content: "" };
|
|
287
332
|
return parseFocusCapsule(content);
|
|
288
333
|
}
|
|
289
334
|
function parseFocusCapsule(content) {
|
|
@@ -481,9 +526,10 @@ async function runCheck(options = {}) {
|
|
|
481
526
|
});
|
|
482
527
|
continue;
|
|
483
528
|
}
|
|
484
|
-
|
|
529
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
530
|
+
for (const diagnostic of inspectChangeDocument(document, { name, validKinds, requiredSections }))
|
|
485
531
|
addDiagnostic({ ...diagnostic, change: name, path: fp });
|
|
486
|
-
const deltas = detectDeltaSections(
|
|
532
|
+
const deltas = detectDeltaSections(document);
|
|
487
533
|
if (deltas.added || deltas.modified || deltas.removed) {
|
|
488
534
|
const parts = [];
|
|
489
535
|
if (deltas.added)
|
|
@@ -500,7 +546,7 @@ async function runCheck(options = {}) {
|
|
|
500
546
|
message: `delta markers found: ${parts.join(", ")}`
|
|
501
547
|
});
|
|
502
548
|
}
|
|
503
|
-
const placeholderLines = findUnfinishedTemplateLines(
|
|
549
|
+
const placeholderLines = findUnfinishedTemplateLines(document.markdown);
|
|
504
550
|
if (placeholderLines.length > 0) {
|
|
505
551
|
addDiagnostic({
|
|
506
552
|
severity: "warning",
|
|
@@ -512,7 +558,7 @@ async function runCheck(options = {}) {
|
|
|
512
558
|
hint: "Replace angle-bracket template text with concrete change details before archiving."
|
|
513
559
|
});
|
|
514
560
|
}
|
|
515
|
-
const clarificationLines = findUnresolvedClarificationLines(
|
|
561
|
+
const clarificationLines = findUnresolvedClarificationLines(document.markdown);
|
|
516
562
|
if (clarificationLines.length > 0) {
|
|
517
563
|
addDiagnostic({
|
|
518
564
|
severity: "warning",
|
|
@@ -524,7 +570,7 @@ async function runCheck(options = {}) {
|
|
|
524
570
|
hint: "Resolve or remove clarification markers once the open question is answered."
|
|
525
571
|
});
|
|
526
572
|
}
|
|
527
|
-
const scenarios = parseScenarios(
|
|
573
|
+
const scenarios = parseScenarios(document.markdown);
|
|
528
574
|
if (scenarios.length > 0) {
|
|
529
575
|
const scenarioIssues = [];
|
|
530
576
|
for (const s of scenarios) {
|
|
@@ -569,32 +615,77 @@ async function runCheck(options = {}) {
|
|
|
569
615
|
}
|
|
570
616
|
function findUnfinishedTemplateLines(content) {
|
|
571
617
|
const placeholderRe = /<(?:…|[^>\n]*(?:choose:|one-line|what |why |which |who |how |new |updated |expected |actual |specific |concrete |exact |verifiable |user |capability |behavior |context |action|path|directory|module|subsystem|command|scenario|constraint|requirement|question|uncertainty|implementation)[^>\n]*)>/i;
|
|
572
|
-
return collectMatchingLines(content, (
|
|
573
|
-
const trimmed = stripInlineCodeSpans(line).trim();
|
|
574
|
-
return trimmed.startsWith("<!--") === false && placeholderRe.test(trimmed);
|
|
575
|
-
});
|
|
618
|
+
return collectMatchingLines(content, (value) => placeholderRe.test(value));
|
|
576
619
|
}
|
|
577
620
|
function findUnresolvedClarificationLines(content) {
|
|
578
|
-
return collectMatchingLines(content, (
|
|
579
|
-
}
|
|
580
|
-
function stripInlineCodeSpans(line) {
|
|
581
|
-
return line.replace(/`[^`]*`/g, "");
|
|
621
|
+
return collectMatchingLines(content, (value) => /\[(?:NEEDS|TODO|TBD)\s+CLARIFICATION(?::[^\]]*)?\]/i.test(value));
|
|
582
622
|
}
|
|
583
623
|
function collectMatchingLines(content, predicate) {
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
624
|
+
const document = parseMarkdown(content);
|
|
625
|
+
const sourceLines = document.source.split(/\r?\n/);
|
|
626
|
+
const matched = /* @__PURE__ */ new Set();
|
|
627
|
+
for (const fragment of markdownProse(document)) {
|
|
628
|
+
fragment.value.split(/\r?\n/).forEach((value, index) => {
|
|
629
|
+
if (predicate(value))
|
|
630
|
+
matched.add(fragment.line + index);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
return [...matched].sort((a, b) => a - b).slice(0, 10).map((line) => `line ${line}: ${sourceLines[line - 1].trim()}`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/commands/config.ts
|
|
637
|
+
async function showConfig() {
|
|
638
|
+
try {
|
|
639
|
+
const inspection = await inspectRspConfig();
|
|
640
|
+
if (inspection.issues.length > 0) {
|
|
641
|
+
return {
|
|
642
|
+
command: "config",
|
|
643
|
+
ok: false,
|
|
644
|
+
path: CONFIG_PATH,
|
|
645
|
+
summary: null,
|
|
646
|
+
diagnostics: [{
|
|
647
|
+
severity: "error",
|
|
648
|
+
code: "invalid_config",
|
|
649
|
+
path: CONFIG_PATH,
|
|
650
|
+
message: inspection.issues.join("; ")
|
|
651
|
+
}],
|
|
652
|
+
runtime: []
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
command: "config",
|
|
657
|
+
ok: true,
|
|
658
|
+
path: CONFIG_PATH,
|
|
659
|
+
summary: {
|
|
660
|
+
kinds: resolveKinds(inspection.config),
|
|
661
|
+
decisions: { path: resolveDecisionRecordsPath(inspection.config) },
|
|
662
|
+
workflow: resolveWorkflowPolicy(inspection.config, { configValid: true }),
|
|
663
|
+
language: resolveLanguagePolicy(inspection.config, { configValid: true })
|
|
664
|
+
},
|
|
665
|
+
diagnostics: [],
|
|
666
|
+
runtime: []
|
|
667
|
+
};
|
|
668
|
+
} catch (error) {
|
|
669
|
+
return {
|
|
670
|
+
command: "config",
|
|
671
|
+
ok: false,
|
|
672
|
+
path: CONFIG_PATH,
|
|
673
|
+
summary: null,
|
|
674
|
+
diagnostics: [{
|
|
675
|
+
severity: "error",
|
|
676
|
+
code: "invalid_config",
|
|
677
|
+
path: CONFIG_PATH,
|
|
678
|
+
message: toErrorMessage(error)
|
|
679
|
+
}],
|
|
680
|
+
runtime: []
|
|
681
|
+
};
|
|
590
682
|
}
|
|
591
|
-
return matches.slice(0, 10);
|
|
592
683
|
}
|
|
593
684
|
|
|
594
685
|
// src/commands/doctor.ts
|
|
595
|
-
import { existsSync
|
|
596
|
-
import { readdir as readdir2, readFile as
|
|
597
|
-
import { basename, join as
|
|
686
|
+
import { existsSync } from "fs";
|
|
687
|
+
import { readdir as readdir2, readFile as readFile3, stat } from "fs/promises";
|
|
688
|
+
import { basename, join as join2, relative as relative2 } from "path";
|
|
598
689
|
|
|
599
690
|
// src/core/artifacts.ts
|
|
600
691
|
var RSP_AGENTS_BEGIN = "<!-- rsp:begin -->";
|
|
@@ -747,62 +838,88 @@ function getChangeTemplateByKind(kind) {
|
|
|
747
838
|
};
|
|
748
839
|
}
|
|
749
840
|
function changeSectionHeading(sectionId) {
|
|
750
|
-
|
|
841
|
+
const guidance = {
|
|
842
|
+
proposal: "State the intended outcome, why it matters, scope, and non-goals; do not narrate execution.",
|
|
843
|
+
spec: "Describe the contract delta and acceptance conditions, referencing affected existing Specs; do not copy the full baseline or implementation steps.",
|
|
844
|
+
design: "Explain the current approach, boundaries, and necessary tradeoffs; replace obsolete plans while preserving decision reasons and useful outcome comparisons.",
|
|
845
|
+
tasks: "Keep current checkable work and actual completion; revise or reopen invalidated tasks within authorized scope. Do not append routine attempts, fixes, retries, or check runs.",
|
|
846
|
+
verify: "Keep required/optional checks and only details needed to judge acceptance, evidence applicability, or locate necessary evidence. Do not repeat check results or retain commands, counts, or review steps merely because they occurred. Preserve applicable coverage, gaps, and distinct review/human acceptance; no run history.",
|
|
847
|
+
blockers: "Name unresolved decisions or dependencies that prevent progress; use none when there are no blockers."
|
|
848
|
+
};
|
|
849
|
+
return `${renderDocumentSectionHeading(CHANGE_DOCUMENT_SCHEMA, sectionId)}
|
|
850
|
+
<!-- ${guidance[sectionId]} -->`;
|
|
751
851
|
}
|
|
752
|
-
function renderRspAgentsBlock() {
|
|
852
|
+
function renderRspAgentsBlock(runtime) {
|
|
853
|
+
const { version } = runtime;
|
|
854
|
+
const selection = runtime.mode === "source" ? `This RSP development checkout explicitly selects source mode through \`.rsp/source\`. Build the current source with \`mise exec -- pnpm run build\`, then use \`${SOURCE_INVOCATION} <command>\` from this checkout for every CLI operation. Skill examples use \`rsp\` as shorthand for this local invocation. Do not substitute a registry package, global CLI or another checkout, even when its version matches. Package version metadata is not source identity; retain the commit, uncommitted diff and build inputs for candidate evidence. Authorized \`update\` reconciles this checkout's managed environment from its local source and keeps source mode; it does not select a published version.` : `This project pins RSP \`${version}\` in \`.rsp/version\`. For every CLI operation use \`${exactInvocation(version)} <command>\` (or a verified executable of that exact version). Skill examples use \`rsp\` as shorthand for this selected invocation. Never substitute \`@latest\`, a version range, or an unverified global CLI. Package fetching requires authority; if unavailable, report the gap. Only an authorized upgrade uses a deliberately selected target CLI with \`update\`; it updates the complete managed environment. Notices grant no upgrade authority.`;
|
|
753
855
|
return `${RSP_AGENTS_BEGIN}
|
|
754
856
|
## RSP Entry
|
|
755
857
|
|
|
756
858
|
RSP tracks current work, stable specs, and archives under \`.rsp/\`.
|
|
757
859
|
|
|
758
|
-
|
|
860
|
+
Use supplied or confirmed paths; discover only missing paths or applicable ancestor instructions, without overlapping whole-tree scans:
|
|
759
861
|
1. Nearest \`AGENTS.md\` for project or module instructions.
|
|
760
|
-
2. Root \`CONTEXT
|
|
761
|
-
3.
|
|
762
|
-
4. \`.rsp/focus.d
|
|
763
|
-
5. Only the relevant Specs and Decision Records under the configured authoritative path.
|
|
862
|
+
2. Root \`CONTEXT.md\` if present, then the relevant nearest \`CONTEXT.md\` for domain language, relationships, and navigation. If a legacy \`CONTEXT-MAP.md\` remains, retain its relevant context and use Core's context-migration branch; discovery alone does not authorize migration.
|
|
863
|
+
3. Establish the request's scope, authority and existing owner. A clear bounded request selecting one capability with sufficient owner facts loads that project Skill directly from \`.agents/skills/\`, without a Core or Focus preflight. For tracked work read the selected Change; for grouped work read its sibling Group Brief first. Read only relevant Specs and Decision Records under the configured authoritative path.
|
|
864
|
+
4. Load \`rsp\` at \`.agents/skills/rsp/SKILL.md\` for unresolved routing or ownership, actual cross-capability coordination, or lifecycle operations. Read \`.rsp/focus.d/\` only when needed to resolve or recover the owner; marker paths are open-work candidates and optional bounded Markdown is recovery guidance, not authority. An explicit WorkRef or the user's mentioned Change selects first; otherwise use current status and dependency evidence.
|
|
764
865
|
|
|
765
|
-
|
|
866
|
+
Direct capability invocation does not make tracked work untracked. Preserve existing Changes, required tracking prerequisites, Group dependencies, independent review and permission boundaries; missing prerequisites stop dependent work. Skills are the only workflow: hosts may discover them or read them directly, but a missing or unusable required Skill stops dependent work and must be reported as an incomplete installation. Do not use retired Rules or install implicitly.
|
|
766
867
|
Do not treat \`.rsp/specs/\` or \`.rsp/changes/\` as replacements for nearest \`AGENTS.md\` or \`CONTEXT.md\`.
|
|
868
|
+
|
|
869
|
+
${selection}
|
|
767
870
|
${RSP_AGENTS_END}`;
|
|
768
871
|
}
|
|
769
|
-
function upsertRspAgentsBlock(content) {
|
|
770
|
-
const block = renderRspAgentsBlock();
|
|
771
|
-
const
|
|
772
|
-
if (
|
|
773
|
-
const next2 = content.
|
|
872
|
+
function upsertRspAgentsBlock(content, runtime) {
|
|
873
|
+
const block = renderRspAgentsBlock(runtime);
|
|
874
|
+
const span = rspAgentsBlockSpan(content);
|
|
875
|
+
if (span) {
|
|
876
|
+
const next2 = content.slice(0, span.start) + block + content.slice(span.end);
|
|
774
877
|
return { content: next2, changed: next2 !== content };
|
|
775
878
|
}
|
|
776
|
-
const
|
|
777
|
-
const next = trimmed ? `${block}
|
|
879
|
+
const next = content ? `${block}
|
|
778
880
|
|
|
779
|
-
${
|
|
881
|
+
${content}` : `${block}
|
|
780
882
|
`;
|
|
781
883
|
return { content: next, changed: true };
|
|
782
884
|
}
|
|
783
885
|
function hasRspAgentsBlock(content) {
|
|
784
|
-
return content
|
|
886
|
+
return rspAgentsBlockSpan(content) !== null;
|
|
887
|
+
}
|
|
888
|
+
function rspAgentsBlockSpan(content) {
|
|
889
|
+
let start;
|
|
890
|
+
for (const comment of markdownTopLevelComments(content)) {
|
|
891
|
+
if (comment.source === RSP_AGENTS_BEGIN && start === void 0)
|
|
892
|
+
start = comment.start;
|
|
893
|
+
if (comment.source === RSP_AGENTS_END && start !== void 0)
|
|
894
|
+
return { start, end: comment.end };
|
|
895
|
+
}
|
|
896
|
+
return null;
|
|
785
897
|
}
|
|
786
898
|
function generateDesignContent(projectName) {
|
|
787
899
|
const placeholder = "<\u2026>";
|
|
788
900
|
return `# Project Design: ${projectName}
|
|
789
901
|
|
|
790
902
|
## Purpose
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
## Stable Facts
|
|
903
|
+
<!-- Define the system's purpose and consumers, not the current change. -->
|
|
794
904
|
- ${placeholder}
|
|
795
905
|
|
|
796
906
|
## Boundaries
|
|
907
|
+
<!-- Define system responsibilities, exclusions, and responsibility handoffs. -->
|
|
797
908
|
- In scope:
|
|
798
909
|
- ${placeholder}
|
|
799
910
|
- Out of scope:
|
|
800
911
|
- ${placeholder}
|
|
801
912
|
|
|
913
|
+
## Contracts
|
|
914
|
+
<!-- State current architectural obligations and invariants that must survive implementation changes. -->
|
|
915
|
+
- ${placeholder}
|
|
916
|
+
|
|
802
917
|
## Structure
|
|
918
|
+
<!-- Describe stable ownership and dependency direction; leave repository navigation to CONTEXT.md. -->
|
|
803
919
|
- ${placeholder}
|
|
804
920
|
|
|
805
921
|
## Constraints
|
|
922
|
+
<!-- Record necessary technical, security, protocol, or compatibility limits; link lasting rationale rather than duplicating it. -->
|
|
806
923
|
- ${placeholder}
|
|
807
924
|
`;
|
|
808
925
|
}
|
|
@@ -812,1589 +929,310 @@ function generateSpecContent(name) {
|
|
|
812
929
|
return `# ${title}
|
|
813
930
|
|
|
814
931
|
## Purpose
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
## Stable Facts
|
|
932
|
+
<!-- Define the capability, its consumers, and its value; do not describe this change. -->
|
|
818
933
|
- ${placeholder}
|
|
819
934
|
|
|
820
935
|
## Boundaries
|
|
936
|
+
<!-- Define responsibilities, exclusions, and handoffs to other capabilities. -->
|
|
821
937
|
- In scope:
|
|
822
938
|
- ${placeholder}
|
|
823
939
|
- Out of scope:
|
|
824
940
|
- ${placeholder}
|
|
825
941
|
|
|
826
|
-
##
|
|
942
|
+
## Contracts
|
|
943
|
+
<!-- State current behavior, inputs and outputs, invariants, and failure semantics by capability, not internal implementation steps. -->
|
|
827
944
|
- ${placeholder}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
import { mkdir, readFile as readFile2, unlink, writeFile } from "fs/promises";
|
|
838
|
-
async function withRspLock(operation, fn) {
|
|
839
|
-
requireManagedDirectory(RSP_DIR, "RSP root", { allowMissing: true });
|
|
840
|
-
await mkdir(RSP_DIR, { recursive: true });
|
|
841
|
-
try {
|
|
842
|
-
await writeFile(LOCK_PATH, `${process.pid}
|
|
843
|
-
${operation}
|
|
844
|
-
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
845
|
-
} catch (err) {
|
|
846
|
-
if (err.code === "EEXIST")
|
|
847
|
-
await handleStaleLock(operation);
|
|
848
|
-
else
|
|
849
|
-
throw err;
|
|
850
|
-
}
|
|
851
|
-
try {
|
|
852
|
-
return await fn();
|
|
853
|
-
} finally {
|
|
854
|
-
try {
|
|
855
|
-
await unlink(LOCK_PATH);
|
|
856
|
-
} catch {
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
async function handleStaleLock(operation) {
|
|
861
|
-
try {
|
|
862
|
-
const content = await readFile2(LOCK_PATH, "utf-8");
|
|
863
|
-
const lines = content.trim().split("\n");
|
|
864
|
-
const pid = Number(lines[0]);
|
|
865
|
-
if (pid && Number.isFinite(pid)) {
|
|
866
|
-
try {
|
|
867
|
-
process.kill(pid, 0);
|
|
868
|
-
const op = lines[1] || "unknown";
|
|
869
|
-
const ts = lines[2] || "unknown";
|
|
870
|
-
throw new Error(`RSP locked by pid ${pid} (${op}) since ${ts}`);
|
|
871
|
-
} catch (e) {
|
|
872
|
-
if (e instanceof Error && e.message.startsWith("RSP locked"))
|
|
873
|
-
throw e;
|
|
874
|
-
const code = e.code;
|
|
875
|
-
if (code === "ESRCH") {
|
|
876
|
-
await unlink(LOCK_PATH);
|
|
877
|
-
await writeFile(LOCK_PATH, `${process.pid}
|
|
878
|
-
${operation}
|
|
879
|
-
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
throw e;
|
|
883
|
-
}
|
|
884
|
-
} else {
|
|
885
|
-
await unlink(LOCK_PATH);
|
|
886
|
-
await writeFile(LOCK_PATH, `${process.pid}
|
|
887
|
-
${operation}
|
|
888
|
-
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
889
|
-
}
|
|
890
|
-
} catch (e) {
|
|
891
|
-
if (e instanceof Error && e.message.startsWith("RSP locked"))
|
|
892
|
-
throw e;
|
|
893
|
-
throw new Error(`RSP is locked. Remove ${LOCK_PATH} manually if no other process is running.`);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// src/commands/archive-index-migration.ts
|
|
898
|
-
import { readFile as readFile3, unlink as unlink2 } from "fs/promises";
|
|
899
|
-
import { join } from "path";
|
|
900
|
-
async function removeLegacyArchiveIndex() {
|
|
901
|
-
const path = join(ARCHIVES_DIR, "INDEX.md");
|
|
902
|
-
const inspection = inspectManagedFile(path, "legacy archive index", { allowMissing: true });
|
|
903
|
-
if (!inspection.exists || inspection.issue)
|
|
904
|
-
return false;
|
|
905
|
-
let frontmatter;
|
|
906
|
-
try {
|
|
907
|
-
frontmatter = parseFrontmatter(await readFile3(path, "utf-8"));
|
|
908
|
-
} catch {
|
|
909
|
-
return false;
|
|
910
|
-
}
|
|
911
|
-
if (frontmatter?.kind !== "generated-index" || frontmatter.index_type !== "archives")
|
|
912
|
-
return false;
|
|
913
|
-
await unlink2(path);
|
|
914
|
-
return true;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// src/commands/specs-index-migration.ts
|
|
918
|
-
import { Buffer as Buffer2 } from "buffer";
|
|
919
|
-
import { createHash, randomUUID } from "crypto";
|
|
920
|
-
import { constants } from "fs";
|
|
921
|
-
import { link, lstat as lstat2, open, readdir, realpath as realpath2, rename, unlink as unlink3 } from "fs/promises";
|
|
922
|
-
import { dirname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
923
|
-
|
|
924
|
-
// src/core/path-identity.ts
|
|
925
|
-
import { lstat, realpath } from "fs/promises";
|
|
926
|
-
import { isAbsolute, relative, resolve, sep } from "path";
|
|
927
|
-
var StablePathIdentityError = class extends Error {
|
|
928
|
-
constructor(code, message) {
|
|
929
|
-
super(message);
|
|
930
|
-
this.code = code;
|
|
931
|
-
this.name = "StablePathIdentityError";
|
|
932
|
-
}
|
|
933
|
-
code;
|
|
934
|
-
};
|
|
935
|
-
async function captureStableDirectoryChain(options) {
|
|
936
|
-
const rootPath = resolve(options.rootPath);
|
|
937
|
-
const targetPath = resolve(options.targetPath);
|
|
938
|
-
const requiredPath = options.requiredPath === void 0 ? null : resolve(options.requiredPath);
|
|
939
|
-
const targetRelative = containedRelative(rootPath, targetPath);
|
|
940
|
-
if (targetRelative === null) {
|
|
941
|
-
throw new StablePathIdentityError(
|
|
942
|
-
"stable_path_outside_root",
|
|
943
|
-
`${options.label} target must stay inside ${rootPath}: ${targetPath}`
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
if (requiredPath !== null) {
|
|
947
|
-
if (containedRelative(rootPath, requiredPath) === null || containedRelative(requiredPath, targetPath) === null) {
|
|
948
|
-
throw new StablePathIdentityError(
|
|
949
|
-
"stable_path_outside_required_root",
|
|
950
|
-
`${options.label} target must stay inside ${requiredPath}: ${targetPath}`
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
const logicalPaths = [rootPath];
|
|
955
|
-
if (targetRelative !== "") {
|
|
956
|
-
let current = rootPath;
|
|
957
|
-
for (const segment of targetRelative.split(sep)) {
|
|
958
|
-
current = resolve(current, segment);
|
|
959
|
-
logicalPaths.push(current);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
const entries = [];
|
|
963
|
-
let rootRealPath = null;
|
|
964
|
-
let requiredRealPath = null;
|
|
965
|
-
for (const path of logicalPaths) {
|
|
966
|
-
let value;
|
|
967
|
-
let resolvedPath;
|
|
968
|
-
try {
|
|
969
|
-
value = await lstat(path, { bigint: true });
|
|
970
|
-
resolvedPath = await realpath(path);
|
|
971
|
-
} catch (error) {
|
|
972
|
-
throw new StablePathIdentityError(
|
|
973
|
-
"stable_path_inspection_failed",
|
|
974
|
-
`${options.label} parent chain could not be inspected at ${path}: ${errorMessage(error)}`
|
|
975
|
-
);
|
|
976
|
-
}
|
|
977
|
-
if (!value.isDirectory() || value.isSymbolicLink()) {
|
|
978
|
-
throw new StablePathIdentityError(
|
|
979
|
-
"stable_path_not_real_directory",
|
|
980
|
-
`${options.label} parent chain must contain only real directories: ${path}`
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
rootRealPath ??= resolvedPath;
|
|
984
|
-
if (containedRelative(rootRealPath, resolvedPath) === null) {
|
|
985
|
-
throw new StablePathIdentityError(
|
|
986
|
-
"stable_path_real_containment_failed",
|
|
987
|
-
`${options.label} parent chain resolves outside ${rootRealPath}: ${path}`
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
if (requiredPath === path)
|
|
991
|
-
requiredRealPath = resolvedPath;
|
|
992
|
-
entries.push({
|
|
993
|
-
path,
|
|
994
|
-
realPath: resolvedPath,
|
|
995
|
-
device: value.dev,
|
|
996
|
-
inode: value.ino
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
if (requiredPath !== null) {
|
|
1000
|
-
const target = entries.at(-1);
|
|
1001
|
-
if (requiredRealPath === null || containedRelative(requiredRealPath, target.realPath) === null) {
|
|
1002
|
-
throw new StablePathIdentityError(
|
|
1003
|
-
"stable_path_required_containment_failed",
|
|
1004
|
-
`${options.label} parent chain no longer resolves inside ${requiredPath}`
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
return {
|
|
1009
|
-
rootPath,
|
|
1010
|
-
targetPath,
|
|
1011
|
-
requiredPath,
|
|
1012
|
-
entries
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
async function assertStableDirectoryChain(expected, label) {
|
|
1016
|
-
const current = await captureStableDirectoryChain({
|
|
1017
|
-
rootPath: expected.rootPath,
|
|
1018
|
-
targetPath: expected.targetPath,
|
|
1019
|
-
requiredPath: expected.requiredPath ?? void 0,
|
|
1020
|
-
label
|
|
1021
|
-
});
|
|
1022
|
-
if (current.entries.length !== expected.entries.length)
|
|
1023
|
-
throw changed(label, expected.targetPath);
|
|
1024
|
-
for (let index = 0; index < expected.entries.length; index += 1) {
|
|
1025
|
-
const before = expected.entries[index];
|
|
1026
|
-
const after = current.entries[index];
|
|
1027
|
-
if (before.path !== after.path || before.realPath !== after.realPath || before.device !== after.device || before.inode !== after.inode) {
|
|
1028
|
-
throw changed(label, before.path);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
function containedRelative(rootPath, candidatePath) {
|
|
1033
|
-
const candidate = relative(rootPath, candidatePath);
|
|
1034
|
-
if (candidate === "" || candidate !== ".." && !candidate.startsWith(`..${sep}`) && !isAbsolute(candidate)) {
|
|
1035
|
-
return candidate;
|
|
1036
|
-
}
|
|
1037
|
-
return null;
|
|
1038
|
-
}
|
|
1039
|
-
function changed(label, path) {
|
|
1040
|
-
return new StablePathIdentityError(
|
|
1041
|
-
"stable_path_identity_changed",
|
|
1042
|
-
`${label} parent identity changed before the filesystem operation: ${path}`
|
|
1043
|
-
);
|
|
1044
|
-
}
|
|
1045
|
-
function errorMessage(error) {
|
|
1046
|
-
return error instanceof Error ? error.message : String(error);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// src/commands/specs-index-migration.ts
|
|
1050
|
-
var READ_NO_FOLLOW_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
|
|
1051
|
-
var WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
1052
|
-
var DEFAULT_FILE_ADAPTER = { link };
|
|
1053
|
-
var GeneratedSpecsIndexMigrationError = class extends Error {
|
|
1054
|
-
constructor(code, message, recoveryPaths = [], retainedMutations = []) {
|
|
1055
|
-
super(message);
|
|
1056
|
-
this.code = code;
|
|
1057
|
-
this.recoveryPaths = recoveryPaths;
|
|
1058
|
-
this.retainedMutations = retainedMutations;
|
|
1059
|
-
this.name = "GeneratedSpecsIndexMigrationError";
|
|
1060
|
-
}
|
|
1061
|
-
code;
|
|
1062
|
-
recoveryPaths;
|
|
1063
|
-
retainedMutations;
|
|
1064
|
-
};
|
|
1065
|
-
async function inspectGeneratedSpecsIndexMigration(cwd = process.cwd()) {
|
|
1066
|
-
const inspection = await inspectSpecs({
|
|
1067
|
-
cwd,
|
|
1068
|
-
allowMissingDecisionRecords: true
|
|
1069
|
-
});
|
|
1070
|
-
const removable = inspection.generatedIndexes.filter((index) => index.classification === "safe-removal").map((index) => index.path).sort();
|
|
1071
|
-
const ownerControlled = inspection.generatedIndexes.filter((index) => index.classification === "owner-controlled").map((index) => index.path).sort();
|
|
1072
|
-
return {
|
|
1073
|
-
inspectionComplete: specsInspectionComplete(inspection),
|
|
1074
|
-
removable,
|
|
1075
|
-
ownerControlled,
|
|
1076
|
-
diagnostics: inspection.diagnostics.map((diagnostic) => `${diagnostic.path ?? ".rsp/specs"}: ${diagnostic.message}`)
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
async function removeRecognizedGeneratedSpecsIndexes(options = {}) {
|
|
1080
|
-
const cwd = await realpath2(resolve2(options.cwd ?? process.cwd()));
|
|
1081
|
-
const fileAdapter = options.fileAdapter ?? DEFAULT_FILE_ADAPTER;
|
|
1082
|
-
const plan = await inspectGeneratedSpecsIndexMigration(cwd);
|
|
1083
|
-
if (!plan.inspectionComplete || plan.ownerControlled.length > 0) {
|
|
1084
|
-
const ownerMessage = plan.ownerControlled.length > 0 ? ` Owner-controlled reserved path(s): ${plan.ownerControlled.join(", ")}.` : "";
|
|
1085
|
-
const diagnosticMessage = plan.diagnostics.length > 0 ? ` ${plan.diagnostics.join("; ")}` : "";
|
|
1086
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1087
|
-
"generated_specs_index_owner_review_required",
|
|
1088
|
-
`Generated Specs-index migration stopped before mutation.${ownerMessage}${diagnosticMessage}`
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
if (plan.removable.length === 0)
|
|
1092
|
-
return [];
|
|
1093
|
-
const snapshots = [];
|
|
1094
|
-
for (const projectPath of plan.removable)
|
|
1095
|
-
snapshots.push(await inspectRecognizedGeneratedIndex(cwd, projectPath));
|
|
1096
|
-
const quarantined = [];
|
|
1097
|
-
let removalCommitted = false;
|
|
1098
|
-
try {
|
|
1099
|
-
for (const snapshot2 of snapshots) {
|
|
1100
|
-
const quarantinePath = `${snapshot2.absolutePath}.${process.pid}.${randomUUID()}.rsp-migration`;
|
|
1101
|
-
await assertMigrationParent(snapshot2, "quarantine");
|
|
1102
|
-
await rename(snapshot2.absolutePath, quarantinePath);
|
|
1103
|
-
await assertMigrationParent(snapshot2, "quarantine");
|
|
1104
|
-
const quarantinedItem = {
|
|
1105
|
-
...snapshot2,
|
|
1106
|
-
quarantinePath,
|
|
1107
|
-
recoveryPath: null
|
|
1108
|
-
};
|
|
1109
|
-
quarantined.push(quarantinedItem);
|
|
1110
|
-
const quarantinedSnapshot = await inspectRecognizedGeneratedIndexAt(
|
|
1111
|
-
cwd,
|
|
1112
|
-
snapshot2.projectPath,
|
|
1113
|
-
quarantinePath,
|
|
1114
|
-
snapshot2.parentChain
|
|
1115
|
-
);
|
|
1116
|
-
if (!sameRenamedSnapshot(snapshot2.identity, quarantinedSnapshot.identity) || snapshot2.contentHash !== quarantinedSnapshot.contentHash) {
|
|
1117
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1118
|
-
"generated_specs_index_changed",
|
|
1119
|
-
`Generated Specs index changed before migration: ${snapshot2.projectPath}`
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
quarantinedItem.identity = quarantinedSnapshot.identity;
|
|
1123
|
-
await options.afterQuarantine?.(snapshot2.projectPath);
|
|
1124
|
-
await assertMigrationParent(quarantinedItem, "post-quarantine hook");
|
|
1125
|
-
}
|
|
1126
|
-
for (const item of quarantined)
|
|
1127
|
-
await assertMigrationParent(item, "direct Specs postcheck");
|
|
1128
|
-
const after = await inspectSpecs({
|
|
1129
|
-
cwd,
|
|
1130
|
-
allowMissingDecisionRecords: true
|
|
1131
|
-
});
|
|
1132
|
-
if (!specsInspectionComplete(after) || after.generatedIndexes.length > 0) {
|
|
1133
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1134
|
-
"generated_specs_index_postcheck_failed",
|
|
1135
|
-
"Specs tree changed while generated indexes were being migrated; no quarantined index was deleted."
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
for (const item of quarantined)
|
|
1139
|
-
await assertMigrationParent(item, "direct Specs postcheck");
|
|
1140
|
-
for (const item of quarantined) {
|
|
1141
|
-
item.recoveryPath = `${item.quarantinePath}.recovery`;
|
|
1142
|
-
await materializeVerifiedSnapshot(
|
|
1143
|
-
item,
|
|
1144
|
-
item.quarantinePath,
|
|
1145
|
-
item.recoveryPath,
|
|
1146
|
-
fileAdapter,
|
|
1147
|
-
options.testing
|
|
1148
|
-
);
|
|
1149
|
-
}
|
|
1150
|
-
for (const item of quarantined) {
|
|
1151
|
-
await assertMigrationParent(item, "quarantine removal");
|
|
1152
|
-
await unlink3(item.quarantinePath);
|
|
1153
|
-
await assertMigrationParent(item, "quarantine removal");
|
|
1154
|
-
}
|
|
1155
|
-
removalCommitted = true;
|
|
1156
|
-
const cleanupPaths = [];
|
|
1157
|
-
for (const item of quarantined) {
|
|
1158
|
-
try {
|
|
1159
|
-
await assertMigrationParent(item, "recovery cleanup");
|
|
1160
|
-
await unlink3(item.recoveryPath);
|
|
1161
|
-
await assertMigrationParent(item, "recovery cleanup");
|
|
1162
|
-
item.recoveryPath = null;
|
|
1163
|
-
} catch {
|
|
1164
|
-
cleanupPaths.push(item.recoveryPath);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
if (cleanupPaths.length > 0) {
|
|
1168
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1169
|
-
"generated_specs_index_cleanup_required",
|
|
1170
|
-
`Generated Specs indexes were removed, but recovery-link cleanup requires owner action: ${cleanupPaths.join(", ")}`,
|
|
1171
|
-
cleanupPaths
|
|
1172
|
-
);
|
|
1173
|
-
}
|
|
1174
|
-
return plan.removable;
|
|
1175
|
-
} catch (error) {
|
|
1176
|
-
if (removalCommitted)
|
|
1177
|
-
throw error;
|
|
1178
|
-
const recovery = await restoreQuarantinedIndexes(
|
|
1179
|
-
quarantined,
|
|
1180
|
-
fileAdapter,
|
|
1181
|
-
options.testing
|
|
1182
|
-
);
|
|
1183
|
-
if (recovery.recoveryPaths.length > 0 || recovery.retainedMutations.length > 0) {
|
|
1184
|
-
const retainedMessage = recovery.retainedMutations.length > 0 ? ` Retained mutation evidence: ${recovery.retainedMutations.map((item) => `${item.projectPath} (${item.quarantineName}, sha256 ${item.contentHash})`).join(", ")}.` : "";
|
|
1185
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1186
|
-
"generated_specs_index_recovery_required",
|
|
1187
|
-
`Generated Specs-index migration stopped and preserved recovery evidence.${recovery.recoveryPaths.length > 0 ? ` Accessible recovery copies: ${recovery.recoveryPaths.join(", ")}.` : ""}${retainedMessage}`,
|
|
1188
|
-
recovery.recoveryPaths,
|
|
1189
|
-
recovery.retainedMutations
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
throw error;
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
async function inspectRecognizedGeneratedIndex(projectRoot, projectPath) {
|
|
1196
|
-
const absolutePath = resolveProjectPath(projectRoot, projectPath);
|
|
1197
|
-
const parentChain = await captureStableDirectoryChain({
|
|
1198
|
-
rootPath: projectRoot,
|
|
1199
|
-
requiredPath: resolve2(projectRoot, ".rsp", "specs"),
|
|
1200
|
-
targetPath: dirname(absolutePath),
|
|
1201
|
-
label: `generated Specs index ${projectPath}`
|
|
1202
|
-
});
|
|
1203
|
-
return inspectRecognizedGeneratedIndexAt(
|
|
1204
|
-
projectRoot,
|
|
1205
|
-
projectPath,
|
|
1206
|
-
absolutePath,
|
|
1207
|
-
parentChain
|
|
1208
|
-
);
|
|
1209
|
-
}
|
|
1210
|
-
async function inspectRecognizedGeneratedIndexAt(projectRoot, projectPath, absolutePath, parentChain) {
|
|
1211
|
-
await assertStableDirectoryChain(parentChain, `generated Specs index ${projectPath}`);
|
|
1212
|
-
const before = await lstat2(absolutePath, { bigint: true });
|
|
1213
|
-
if (!before.isFile() || before.isSymbolicLink())
|
|
1214
|
-
throw changed2(projectPath);
|
|
1215
|
-
const handle = await open(absolutePath, READ_NO_FOLLOW_FLAGS);
|
|
1216
|
-
try {
|
|
1217
|
-
const opened = await handle.stat({ bigint: true });
|
|
1218
|
-
if (!opened.isFile() || !sameSnapshot(before, opened))
|
|
1219
|
-
throw changed2(projectPath);
|
|
1220
|
-
const content = await readBounded(handle, projectPath);
|
|
1221
|
-
const after = await handle.stat({ bigint: true });
|
|
1222
|
-
if (!sameSnapshot(opened, after))
|
|
1223
|
-
throw changed2(projectPath);
|
|
1224
|
-
const finalPath = await lstat2(absolutePath, { bigint: true });
|
|
1225
|
-
if (!sameSnapshot(after, finalPath))
|
|
1226
|
-
throw changed2(projectPath);
|
|
1227
|
-
await assertStableDirectoryChain(parentChain, `generated Specs index ${projectPath}`);
|
|
1228
|
-
if (!isRecognizedGeneratedSpecsIndex(content, projectPath)) {
|
|
1229
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1230
|
-
"generated_specs_index_owner_review_required",
|
|
1231
|
-
`Reserved Specs index is not recognized generated content: ${projectPath}`
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
return {
|
|
1235
|
-
projectPath,
|
|
1236
|
-
absolutePath,
|
|
1237
|
-
identity: snapshot(after),
|
|
1238
|
-
contentHash: createHash("sha256").update(content).digest("hex"),
|
|
1239
|
-
parentChain
|
|
1240
|
-
};
|
|
1241
|
-
} finally {
|
|
1242
|
-
await handle.close();
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
async function readBounded(handle, projectPath) {
|
|
1246
|
-
const buffer = Buffer2.allocUnsafe(SPECS_MAX_FILE_BYTES + 1);
|
|
1247
|
-
let bytes = 0;
|
|
1248
|
-
while (bytes < buffer.length) {
|
|
1249
|
-
const result = await handle.read(buffer, bytes, buffer.length - bytes, bytes);
|
|
1250
|
-
if (result.bytesRead === 0)
|
|
1251
|
-
break;
|
|
1252
|
-
bytes += result.bytesRead;
|
|
1253
|
-
}
|
|
1254
|
-
if (bytes > SPECS_MAX_FILE_BYTES) {
|
|
1255
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1256
|
-
"generated_specs_index_too_large",
|
|
1257
|
-
`Reserved Specs index exceeds the ${SPECS_MAX_FILE_BYTES}-byte migration limit: ${projectPath}`
|
|
1258
|
-
);
|
|
1259
|
-
}
|
|
1260
|
-
return buffer.subarray(0, bytes).toString("utf8");
|
|
1261
|
-
}
|
|
1262
|
-
async function restoreQuarantinedIndexes(quarantined, fileAdapter, testing) {
|
|
1263
|
-
const recoveryPaths = [];
|
|
1264
|
-
const retainedMutations = [];
|
|
1265
|
-
for (const item of [...quarantined].reverse()) {
|
|
1266
|
-
try {
|
|
1267
|
-
await assertMigrationParent(item, "rollback source inspection");
|
|
1268
|
-
const sourcePath = await firstExistingPath(item);
|
|
1269
|
-
if (!sourcePath) {
|
|
1270
|
-
recoveryPaths.push(item.recoveryPath ?? item.quarantinePath);
|
|
1271
|
-
continue;
|
|
1272
|
-
}
|
|
1273
|
-
await materializeVerifiedSnapshot(
|
|
1274
|
-
item,
|
|
1275
|
-
sourcePath,
|
|
1276
|
-
item.absolutePath,
|
|
1277
|
-
fileAdapter,
|
|
1278
|
-
testing
|
|
1279
|
-
);
|
|
1280
|
-
await assertMigrationParent(item, "rollback cleanup");
|
|
1281
|
-
await unlink3(item.quarantinePath).catch(ignoreMissing);
|
|
1282
|
-
if (item.recoveryPath) {
|
|
1283
|
-
await assertMigrationParent(item, "rollback cleanup");
|
|
1284
|
-
await unlink3(item.recoveryPath).catch(ignoreMissing);
|
|
1285
|
-
}
|
|
1286
|
-
} catch {
|
|
1287
|
-
try {
|
|
1288
|
-
await assertMigrationParent(item, "rollback recovery reporting");
|
|
1289
|
-
const recoveryPath = await firstExistingPath(item);
|
|
1290
|
-
if (recoveryPath)
|
|
1291
|
-
recoveryPaths.push(recoveryPath);
|
|
1292
|
-
else
|
|
1293
|
-
retainedMutations.push(retainedMutationEvidence(item));
|
|
1294
|
-
} catch {
|
|
1295
|
-
const relocated = await findRelocatedRetainedMutation(item).catch(() => null);
|
|
1296
|
-
if (relocated)
|
|
1297
|
-
recoveryPaths.push(relocated);
|
|
1298
|
-
else
|
|
1299
|
-
retainedMutations.push(retainedMutationEvidence(item));
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
return { recoveryPaths, retainedMutations };
|
|
1304
|
-
}
|
|
1305
|
-
async function findRelocatedRetainedMutation(item) {
|
|
1306
|
-
const targetName = item.quarantinePath.slice(dirname(item.quarantinePath).length + 1);
|
|
1307
|
-
const pending = [item.parentChain.rootPath];
|
|
1308
|
-
let inspected = 0;
|
|
1309
|
-
while (pending.length > 0 && inspected < 4096) {
|
|
1310
|
-
const directory = pending.shift();
|
|
1311
|
-
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
1312
|
-
inspected += 1;
|
|
1313
|
-
if (inspected > 4096)
|
|
1314
|
-
return null;
|
|
1315
|
-
const path = resolve2(directory, entry.name);
|
|
1316
|
-
if (entry.isSymbolicLink())
|
|
1317
|
-
continue;
|
|
1318
|
-
if (entry.isDirectory()) {
|
|
1319
|
-
pending.push(path);
|
|
1320
|
-
continue;
|
|
1321
|
-
}
|
|
1322
|
-
if (!entry.isFile() || entry.name !== targetName)
|
|
1323
|
-
continue;
|
|
1324
|
-
const value = await lstat2(path, { bigint: true });
|
|
1325
|
-
if (value.isFile() && !value.isSymbolicLink() && value.dev === item.identity.dev && value.ino === item.identity.ino) {
|
|
1326
|
-
return path;
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
return null;
|
|
1331
|
-
}
|
|
1332
|
-
async function firstExistingPath(item) {
|
|
1333
|
-
for (const path of [item.quarantinePath, item.recoveryPath]) {
|
|
1334
|
-
if (!path) {
|
|
1335
|
-
continue;
|
|
1336
|
-
}
|
|
1337
|
-
try {
|
|
1338
|
-
await assertMigrationParent(item, "recovery source inspection");
|
|
1339
|
-
await lstat2(path);
|
|
1340
|
-
return path;
|
|
1341
|
-
} catch (error) {
|
|
1342
|
-
if (error.code !== "ENOENT")
|
|
1343
|
-
throw error;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
return null;
|
|
1347
|
-
}
|
|
1348
|
-
function ignoreMissing(error) {
|
|
1349
|
-
if (error.code !== "ENOENT")
|
|
1350
|
-
throw error;
|
|
1351
|
-
}
|
|
1352
|
-
async function materializeVerifiedSnapshot(item, sourcePath, destinationPath, fileAdapter, testing) {
|
|
1353
|
-
await assertMigrationParent(item, "snapshot materialization");
|
|
1354
|
-
const source = await inspectRecognizedGeneratedIndexAt(
|
|
1355
|
-
item.parentChain.rootPath,
|
|
1356
|
-
item.projectPath,
|
|
1357
|
-
sourcePath,
|
|
1358
|
-
item.parentChain
|
|
1359
|
-
);
|
|
1360
|
-
if (source.contentHash !== item.contentHash || permissionMode(source.identity.mode) !== permissionMode(item.identity.mode)) {
|
|
1361
|
-
throw changed2(item.projectPath);
|
|
1362
|
-
}
|
|
1363
|
-
try {
|
|
1364
|
-
await fileAdapter.link(sourcePath, destinationPath);
|
|
1365
|
-
} catch (error) {
|
|
1366
|
-
if (!hardLinkUnsupported(error))
|
|
1367
|
-
throw error;
|
|
1368
|
-
await copySnapshotExclusive(
|
|
1369
|
-
source,
|
|
1370
|
-
destinationPath,
|
|
1371
|
-
fileAdapter,
|
|
1372
|
-
testing
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
const destination = await inspectRecognizedGeneratedIndexAt(
|
|
1376
|
-
item.parentChain.rootPath,
|
|
1377
|
-
item.projectPath,
|
|
1378
|
-
destinationPath,
|
|
1379
|
-
item.parentChain
|
|
1380
|
-
);
|
|
1381
|
-
const sourceAfter = await inspectRecognizedGeneratedIndexAt(
|
|
1382
|
-
item.parentChain.rootPath,
|
|
1383
|
-
item.projectPath,
|
|
1384
|
-
sourcePath,
|
|
1385
|
-
item.parentChain
|
|
1386
|
-
);
|
|
1387
|
-
if (destination.contentHash !== item.contentHash || sourceAfter.contentHash !== item.contentHash || permissionMode(destination.identity.mode) !== permissionMode(item.identity.mode) || permissionMode(sourceAfter.identity.mode) !== permissionMode(item.identity.mode) || !sameRenamedSnapshot(source.identity, sourceAfter.identity)) {
|
|
1388
|
-
throw changed2(item.projectPath);
|
|
1389
|
-
}
|
|
1390
|
-
await assertMigrationParent(item, "snapshot materialization");
|
|
1391
|
-
}
|
|
1392
|
-
async function copySnapshotExclusive(source, destinationPath, fileAdapter, testing) {
|
|
1393
|
-
const sourceHandle = await open(source.absolutePath, READ_NO_FOLLOW_FLAGS);
|
|
1394
|
-
let destinationHandle = null;
|
|
1395
|
-
let destinationIdentity = null;
|
|
1396
|
-
try {
|
|
1397
|
-
const sourceOpened = await sourceHandle.stat({ bigint: true });
|
|
1398
|
-
if (!sameSnapshot(source.identity, sourceOpened))
|
|
1399
|
-
throw changed2(source.projectPath);
|
|
1400
|
-
destinationHandle = await open(
|
|
1401
|
-
destinationPath,
|
|
1402
|
-
WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS,
|
|
1403
|
-
Number(permissionMode(source.identity.mode))
|
|
1404
|
-
);
|
|
1405
|
-
const destinationOpened = await destinationHandle.stat({ bigint: true });
|
|
1406
|
-
if (!destinationOpened.isFile() || destinationOpened.isSymbolicLink())
|
|
1407
|
-
throw changed2(source.projectPath);
|
|
1408
|
-
destinationIdentity = snapshot(destinationOpened);
|
|
1409
|
-
await testing?.afterCopyDestinationOpen?.(destinationPath);
|
|
1410
|
-
const hash = createHash("sha256");
|
|
1411
|
-
const buffer = Buffer2.allocUnsafe(64 * 1024);
|
|
1412
|
-
let bytes = 0;
|
|
1413
|
-
while (bytes <= SPECS_MAX_FILE_BYTES) {
|
|
1414
|
-
const read = await sourceHandle.read(buffer, 0, buffer.length, null);
|
|
1415
|
-
if (read.bytesRead === 0)
|
|
1416
|
-
break;
|
|
1417
|
-
bytes += read.bytesRead;
|
|
1418
|
-
if (bytes > SPECS_MAX_FILE_BYTES)
|
|
1419
|
-
throw changed2(source.projectPath);
|
|
1420
|
-
const chunk = buffer.subarray(0, read.bytesRead);
|
|
1421
|
-
hash.update(chunk);
|
|
1422
|
-
let written = 0;
|
|
1423
|
-
while (written < chunk.length) {
|
|
1424
|
-
const result = await destinationHandle.write(
|
|
1425
|
-
chunk,
|
|
1426
|
-
written,
|
|
1427
|
-
chunk.length - written,
|
|
1428
|
-
null
|
|
1429
|
-
);
|
|
1430
|
-
written += result.bytesWritten;
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
if (BigInt(bytes) !== source.identity.size || hash.digest("hex") !== source.contentHash) {
|
|
1434
|
-
throw changed2(source.projectPath);
|
|
1435
|
-
}
|
|
1436
|
-
await destinationHandle.chmod(Number(permissionMode(source.identity.mode)));
|
|
1437
|
-
await destinationHandle.sync();
|
|
1438
|
-
const destinationAfter = await destinationHandle.stat({ bigint: true });
|
|
1439
|
-
const sourceAfter = await sourceHandle.stat({ bigint: true });
|
|
1440
|
-
if (!sameSnapshot(sourceOpened, sourceAfter) || !destinationAfter.isFile() || destinationAfter.isSymbolicLink() || destinationAfter.size !== source.identity.size || permissionMode(destinationAfter.mode) !== permissionMode(source.identity.mode)) {
|
|
1441
|
-
throw changed2(source.projectPath);
|
|
1442
|
-
}
|
|
1443
|
-
} catch (error) {
|
|
1444
|
-
await destinationHandle?.close().catch(() => void 0);
|
|
1445
|
-
destinationHandle = null;
|
|
1446
|
-
if (destinationIdentity) {
|
|
1447
|
-
await cleanupExclusiveDestination(
|
|
1448
|
-
source,
|
|
1449
|
-
destinationPath,
|
|
1450
|
-
destinationIdentity,
|
|
1451
|
-
fileAdapter
|
|
1452
|
-
);
|
|
1453
|
-
}
|
|
1454
|
-
throw error;
|
|
1455
|
-
} finally {
|
|
1456
|
-
await destinationHandle?.close();
|
|
1457
|
-
await sourceHandle.close();
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
async function cleanupExclusiveDestination(source, destinationPath, expected, fileAdapter) {
|
|
1461
|
-
const quarantinePath = `${destinationPath}.${process.pid}.${randomUUID()}.rsp-copy-cleanup`;
|
|
1462
|
-
await assertMigrationParent(source, "exclusive-copy cleanup");
|
|
1463
|
-
try {
|
|
1464
|
-
await rename(destinationPath, quarantinePath);
|
|
1465
|
-
} catch (error) {
|
|
1466
|
-
if (error.code === "ENOENT")
|
|
1467
|
-
return;
|
|
1468
|
-
throw error;
|
|
1469
|
-
}
|
|
1470
|
-
await assertMigrationParent(source, "exclusive-copy cleanup");
|
|
1471
|
-
const quarantined = await lstat2(quarantinePath, { bigint: true });
|
|
1472
|
-
if (quarantined.isFile() && !quarantined.isSymbolicLink() && quarantined.dev === expected.dev && quarantined.ino === expected.ino) {
|
|
1473
|
-
const finalIdentity = await lstat2(quarantinePath, { bigint: true });
|
|
1474
|
-
if (!finalIdentity.isFile() || finalIdentity.isSymbolicLink() || finalIdentity.dev !== expected.dev || finalIdentity.ino !== expected.ino) {
|
|
1475
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1476
|
-
"generated_specs_index_copy_cleanup_required",
|
|
1477
|
-
`Exclusive-copy cleanup retained a changed file: ${quarantinePath}`,
|
|
1478
|
-
[quarantinePath]
|
|
1479
|
-
);
|
|
1480
|
-
}
|
|
1481
|
-
await unlink3(quarantinePath);
|
|
1482
|
-
return;
|
|
1483
|
-
}
|
|
1484
|
-
try {
|
|
1485
|
-
await fileAdapter.link(quarantinePath, destinationPath);
|
|
1486
|
-
await unlink3(quarantinePath);
|
|
1487
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1488
|
-
"generated_specs_index_copy_replaced",
|
|
1489
|
-
`Exclusive-copy cleanup preserved a concurrent replacement at ${destinationPath}`
|
|
1490
|
-
);
|
|
1491
|
-
} catch (error) {
|
|
1492
|
-
if (error instanceof GeneratedSpecsIndexMigrationError)
|
|
1493
|
-
throw error;
|
|
1494
|
-
if (!hardLinkUnsupported(error) && error.code !== "EEXIST") {
|
|
1495
|
-
throw error;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
if (await lstat2(destinationPath).catch(() => null) === null)
|
|
1499
|
-
await copyConcurrentReplacementExclusive(quarantinePath, destinationPath);
|
|
1500
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1501
|
-
"generated_specs_index_copy_cleanup_required",
|
|
1502
|
-
`Exclusive-copy cleanup preserved a concurrent replacement and retained its original inode at ${quarantinePath}`,
|
|
1503
|
-
[quarantinePath]
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
async function copyConcurrentReplacementExclusive(sourcePath, destinationPath) {
|
|
1507
|
-
const sourceHandle = await open(sourcePath, READ_NO_FOLLOW_FLAGS);
|
|
1508
|
-
let destinationHandle = null;
|
|
1509
|
-
try {
|
|
1510
|
-
const sourceBefore = await sourceHandle.stat({ bigint: true });
|
|
1511
|
-
if (!sourceBefore.isFile() || sourceBefore.isSymbolicLink() || sourceBefore.size > BigInt(SPECS_MAX_FILE_BYTES)) {
|
|
1512
|
-
return;
|
|
1513
|
-
}
|
|
1514
|
-
destinationHandle = await open(
|
|
1515
|
-
destinationPath,
|
|
1516
|
-
WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS,
|
|
1517
|
-
Number(permissionMode(sourceBefore.mode))
|
|
1518
|
-
);
|
|
1519
|
-
const buffer = Buffer2.allocUnsafe(64 * 1024);
|
|
1520
|
-
let bytes = 0;
|
|
1521
|
-
while (bytes <= SPECS_MAX_FILE_BYTES) {
|
|
1522
|
-
const read = await sourceHandle.read(buffer, 0, buffer.length, null);
|
|
1523
|
-
if (read.bytesRead === 0)
|
|
1524
|
-
break;
|
|
1525
|
-
bytes += read.bytesRead;
|
|
1526
|
-
if (bytes > SPECS_MAX_FILE_BYTES)
|
|
1527
|
-
return;
|
|
1528
|
-
let written = 0;
|
|
1529
|
-
while (written < read.bytesRead) {
|
|
1530
|
-
const result = await destinationHandle.write(
|
|
1531
|
-
buffer,
|
|
1532
|
-
written,
|
|
1533
|
-
read.bytesRead - written,
|
|
1534
|
-
null
|
|
1535
|
-
);
|
|
1536
|
-
written += result.bytesWritten;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
await destinationHandle.chmod(Number(permissionMode(sourceBefore.mode)));
|
|
1540
|
-
await destinationHandle.sync();
|
|
1541
|
-
const sourceAfter = await sourceHandle.stat({ bigint: true });
|
|
1542
|
-
const destinationAfter = await destinationHandle.stat({ bigint: true });
|
|
1543
|
-
if (!sameSnapshot(sourceBefore, sourceAfter) || destinationAfter.size !== sourceBefore.size || permissionMode(destinationAfter.mode) !== permissionMode(sourceBefore.mode)) {
|
|
1544
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1545
|
-
"generated_specs_index_copy_cleanup_required",
|
|
1546
|
-
`Concurrent replacement could not be restored byte-for-byte: ${sourcePath}`,
|
|
1547
|
-
[sourcePath]
|
|
1548
|
-
);
|
|
1549
|
-
}
|
|
1550
|
-
} finally {
|
|
1551
|
-
await destinationHandle?.close();
|
|
1552
|
-
await sourceHandle.close();
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
function retainedMutationEvidence(item) {
|
|
1556
|
-
return {
|
|
1557
|
-
projectPath: item.projectPath,
|
|
1558
|
-
quarantineName: item.quarantinePath.slice(dirname(item.quarantinePath).length + 1),
|
|
1559
|
-
contentHash: item.contentHash,
|
|
1560
|
-
device: String(item.identity.dev),
|
|
1561
|
-
inode: String(item.identity.ino),
|
|
1562
|
-
size: String(item.identity.size),
|
|
1563
|
-
mode: permissionMode(item.identity.mode).toString(8),
|
|
1564
|
-
lastKnownParentRealPath: item.parentChain.entries.at(-1).realPath
|
|
1565
|
-
};
|
|
1566
|
-
}
|
|
1567
|
-
async function assertMigrationParent(item, operation) {
|
|
1568
|
-
try {
|
|
1569
|
-
await assertStableDirectoryChain(
|
|
1570
|
-
item.parentChain,
|
|
1571
|
-
`generated Specs index ${item.projectPath} ${operation}`
|
|
1572
|
-
);
|
|
1573
|
-
} catch (error) {
|
|
1574
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1575
|
-
"generated_specs_index_parent_changed",
|
|
1576
|
-
`Generated Specs-index migration stopped because a managed parent changed before ${operation}: ${item.projectPath}. ${error instanceof Error ? error.message : String(error)}`
|
|
1577
|
-
);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
function hardLinkUnsupported(error) {
|
|
1581
|
-
return ["EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM"].includes(error.code ?? "");
|
|
1582
|
-
}
|
|
1583
|
-
function permissionMode(mode) {
|
|
1584
|
-
return mode & 0o777n;
|
|
1585
|
-
}
|
|
1586
|
-
function resolveProjectPath(projectRoot, projectPath) {
|
|
1587
|
-
const normalized = normalizeLogicalPath(projectPath);
|
|
1588
|
-
const absolutePath = resolve2(projectRoot, ...normalized.split("/"));
|
|
1589
|
-
const candidate = relative2(projectRoot, absolutePath);
|
|
1590
|
-
if (normalized === "" || isAbsolute2(projectPath) || candidate === "" || candidate === ".." || candidate.startsWith(`..${sep2}`) || isAbsolute2(candidate) || dirname(normalized) === ".") {
|
|
1591
|
-
throw new GeneratedSpecsIndexMigrationError(
|
|
1592
|
-
"generated_specs_index_path_invalid",
|
|
1593
|
-
`Generated Specs-index path is outside the managed Specs tree: ${projectPath}`
|
|
1594
|
-
);
|
|
1595
|
-
}
|
|
1596
|
-
return absolutePath;
|
|
1597
|
-
}
|
|
1598
|
-
function changed2(projectPath) {
|
|
1599
|
-
return new GeneratedSpecsIndexMigrationError(
|
|
1600
|
-
"generated_specs_index_changed",
|
|
1601
|
-
`Generated Specs index changed before migration: ${projectPath}`
|
|
1602
|
-
);
|
|
1603
|
-
}
|
|
1604
|
-
function snapshot(value) {
|
|
1605
|
-
return {
|
|
1606
|
-
dev: value.dev,
|
|
1607
|
-
ino: value.ino,
|
|
1608
|
-
size: value.size,
|
|
1609
|
-
mode: value.mode,
|
|
1610
|
-
mtimeNs: value.mtimeNs,
|
|
1611
|
-
ctimeNs: value.ctimeNs
|
|
1612
|
-
};
|
|
1613
|
-
}
|
|
1614
|
-
function sameSnapshot(left, right) {
|
|
1615
|
-
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mode === right.mode && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
1616
|
-
}
|
|
1617
|
-
function sameRenamedSnapshot(left, right) {
|
|
1618
|
-
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mode === right.mode && left.mtimeNs === right.mtimeNs;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
// src/commands/update-transaction.ts
|
|
1622
|
-
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
1623
|
-
import { constants as constants2 } from "fs";
|
|
1624
|
-
import {
|
|
1625
|
-
chmod,
|
|
1626
|
-
link as link2,
|
|
1627
|
-
lstat as lstat3,
|
|
1628
|
-
mkdir as mkdir2,
|
|
1629
|
-
mkdtemp,
|
|
1630
|
-
open as open2,
|
|
1631
|
-
rename as rename2,
|
|
1632
|
-
rmdir,
|
|
1633
|
-
unlink as unlink4
|
|
1634
|
-
} from "fs/promises";
|
|
1635
|
-
import { tmpdir } from "os";
|
|
1636
|
-
import { dirname as dirname2, join as join2, relative as relative3, resolve as resolve3, sep as sep3 } from "path";
|
|
1637
|
-
var READ_NO_FOLLOW_FLAGS2 = constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0);
|
|
1638
|
-
var WRITE_EXCLUSIVE_NO_FOLLOW_FLAGS2 = constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0);
|
|
1639
|
-
var UpdateRollbackMutationError = class extends Error {
|
|
1640
|
-
constructor(message, recoveryPaths = []) {
|
|
1641
|
-
super(message);
|
|
1642
|
-
this.recoveryPaths = recoveryPaths;
|
|
1643
|
-
this.name = "UpdateRollbackMutationError";
|
|
1644
|
-
}
|
|
1645
|
-
recoveryPaths;
|
|
1646
|
-
};
|
|
1647
|
-
var UpdateRollbackJournal = class {
|
|
1648
|
-
constructor(projectRoot = resolve3("."), testing = {}) {
|
|
1649
|
-
this.projectRoot = projectRoot;
|
|
1650
|
-
this.testing = testing;
|
|
1651
|
-
}
|
|
1652
|
-
projectRoot;
|
|
1653
|
-
testing;
|
|
1654
|
-
entries = /* @__PURE__ */ new Map();
|
|
1655
|
-
mutationOrder = [];
|
|
1656
|
-
async capture(path) {
|
|
1657
|
-
const absolutePath = this.resolveProjectPath(path);
|
|
1658
|
-
if (this.entries.has(absolutePath))
|
|
1659
|
-
return;
|
|
1660
|
-
this.entries.set(absolutePath, {
|
|
1661
|
-
path: absolutePath,
|
|
1662
|
-
original: await inspectPathSnapshot(absolutePath),
|
|
1663
|
-
published: null,
|
|
1664
|
-
parentChain: await captureExistingParentChain(
|
|
1665
|
-
this.projectRoot,
|
|
1666
|
-
absolutePath
|
|
1667
|
-
)
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
async captureDirectoryChain(path) {
|
|
1671
|
-
const absolutePath = this.resolveProjectPath(path);
|
|
1672
|
-
const rel = relative3(this.projectRoot, absolutePath);
|
|
1673
|
-
const captured = [];
|
|
1674
|
-
let current = this.projectRoot;
|
|
1675
|
-
for (const segment of rel === "" ? [] : rel.split(sep3)) {
|
|
1676
|
-
current = resolve3(current, segment);
|
|
1677
|
-
await this.capture(current);
|
|
1678
|
-
captured.push(current);
|
|
1679
|
-
}
|
|
1680
|
-
return captured;
|
|
1681
|
-
}
|
|
1682
|
-
async mark(path) {
|
|
1683
|
-
const absolutePath = this.resolveProjectPath(path);
|
|
1684
|
-
const entry = this.entries.get(absolutePath);
|
|
1685
|
-
if (!entry)
|
|
1686
|
-
throw new Error(`Update rollback journal did not capture ${absolutePath}`);
|
|
1687
|
-
entry.published = await inspectPathSnapshot(absolutePath);
|
|
1688
|
-
if (!samePathState(entry.original, entry.published) && !this.mutationOrder.includes(absolutePath)) {
|
|
1689
|
-
this.mutationOrder.push(absolutePath);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
async markAll(paths) {
|
|
1693
|
-
for (const path of paths)
|
|
1694
|
-
await this.mark(path);
|
|
1695
|
-
}
|
|
1696
|
-
async rollback() {
|
|
1697
|
-
const restored = [];
|
|
1698
|
-
const retainedMutations = [];
|
|
1699
|
-
const recoveryPaths = [];
|
|
1700
|
-
for (const [path, entry] of this.entries) {
|
|
1701
|
-
if (entry.published)
|
|
1702
|
-
continue;
|
|
1703
|
-
const current = await inspectPathSnapshot(path).catch(() => null);
|
|
1704
|
-
if (current && !samePathState(entry.original, current)) {
|
|
1705
|
-
entry.published = current;
|
|
1706
|
-
if (!this.mutationOrder.includes(path))
|
|
1707
|
-
this.mutationOrder.push(path);
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
const ordered = [...this.mutationOrder].sort((left, right) => compareRollbackOrder(
|
|
1711
|
-
this.entries.get(left),
|
|
1712
|
-
this.entries.get(right)
|
|
1713
|
-
));
|
|
1714
|
-
for (const path of ordered) {
|
|
1715
|
-
const entry = this.entries.get(path);
|
|
1716
|
-
const published = entry.published;
|
|
1717
|
-
if (!published)
|
|
1718
|
-
continue;
|
|
1719
|
-
let current;
|
|
1720
|
-
try {
|
|
1721
|
-
current = await inspectPathSnapshot(path);
|
|
1722
|
-
} catch {
|
|
1723
|
-
await recordIncompleteRollback(
|
|
1724
|
-
this.projectRoot,
|
|
1725
|
-
entry,
|
|
1726
|
-
retainedMutations,
|
|
1727
|
-
recoveryPaths
|
|
1728
|
-
);
|
|
1729
|
-
continue;
|
|
1730
|
-
}
|
|
1731
|
-
if (!samePathState(current, published)) {
|
|
1732
|
-
await recordIncompleteRollback(
|
|
1733
|
-
this.projectRoot,
|
|
1734
|
-
entry,
|
|
1735
|
-
retainedMutations,
|
|
1736
|
-
recoveryPaths
|
|
1737
|
-
);
|
|
1738
|
-
continue;
|
|
1739
|
-
}
|
|
1740
|
-
try {
|
|
1741
|
-
recoveryPaths.push(...await restorePath(entry, this.testing));
|
|
1742
|
-
restored.push(toProjectPath(this.projectRoot, path));
|
|
1743
|
-
if (entry.original.kind === "directory" && published.kind === "missing") {
|
|
1744
|
-
await this.refreshDescendantParentChains(path);
|
|
1745
|
-
}
|
|
1746
|
-
} catch (error) {
|
|
1747
|
-
if (error instanceof UpdateRollbackMutationError)
|
|
1748
|
-
recoveryPaths.push(...error.recoveryPaths);
|
|
1749
|
-
await recordIncompleteRollback(
|
|
1750
|
-
this.projectRoot,
|
|
1751
|
-
entry,
|
|
1752
|
-
retainedMutations,
|
|
1753
|
-
recoveryPaths
|
|
1754
|
-
);
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
return {
|
|
1758
|
-
restored,
|
|
1759
|
-
retainedMutations: [...new Set(retainedMutations)].sort(),
|
|
1760
|
-
recoveryPaths: [...new Set(recoveryPaths)].sort()
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
resolveProjectPath(path) {
|
|
1764
|
-
const absolutePath = resolve3(path);
|
|
1765
|
-
const rel = relative3(this.projectRoot, absolutePath);
|
|
1766
|
-
if (rel === ".." || rel.startsWith(`..${sep3}`))
|
|
1767
|
-
throw new Error(`Update transaction path escapes the project root: ${path}`);
|
|
1768
|
-
return absolutePath;
|
|
1769
|
-
}
|
|
1770
|
-
async refreshDescendantParentChains(restoredDirectory) {
|
|
1771
|
-
for (const entry of this.entries.values()) {
|
|
1772
|
-
const rel = relative3(restoredDirectory, entry.path);
|
|
1773
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${sep3}`))
|
|
1774
|
-
continue;
|
|
1775
|
-
entry.parentChain = await captureExistingParentChain(
|
|
1776
|
-
this.projectRoot,
|
|
1777
|
-
entry.path
|
|
1778
|
-
);
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
};
|
|
1782
|
-
async function recordIncompleteRollback(projectRoot, entry, retainedMutations, recoveryPaths) {
|
|
1783
|
-
retainedMutations.push(toProjectPath(projectRoot, entry.path));
|
|
1784
|
-
const recovery = await persistOriginalRecovery(entry.original).catch(() => null);
|
|
1785
|
-
if (recovery)
|
|
1786
|
-
recoveryPaths.push(recovery);
|
|
1787
|
-
}
|
|
1788
|
-
async function inspectPathSnapshot(path) {
|
|
1789
|
-
let before;
|
|
1790
|
-
try {
|
|
1791
|
-
before = await lstat3(path, { bigint: true });
|
|
1792
|
-
} catch (error) {
|
|
1793
|
-
if (error.code === "ENOENT")
|
|
1794
|
-
return { kind: "missing" };
|
|
1795
|
-
throw error;
|
|
1796
|
-
}
|
|
1797
|
-
if (before.isSymbolicLink())
|
|
1798
|
-
throw new Error(`Update transaction refuses a symlinked path: ${path}`);
|
|
1799
|
-
if (before.isDirectory()) {
|
|
1800
|
-
return {
|
|
1801
|
-
kind: "directory",
|
|
1802
|
-
device: before.dev,
|
|
1803
|
-
inode: before.ino,
|
|
1804
|
-
mode: before.mode
|
|
1805
|
-
};
|
|
1806
|
-
}
|
|
1807
|
-
if (!before.isFile())
|
|
1808
|
-
throw new Error(`Update transaction requires a regular file or directory: ${path}`);
|
|
1809
|
-
const handle = await open2(path, READ_NO_FOLLOW_FLAGS2);
|
|
1810
|
-
try {
|
|
1811
|
-
const opened = await handle.stat({ bigint: true });
|
|
1812
|
-
if (!sameFileIdentity(before, opened))
|
|
1813
|
-
throw new Error(`Update transaction path changed while it was captured: ${path}`);
|
|
1814
|
-
const content = await handle.readFile();
|
|
1815
|
-
const after = await handle.stat({ bigint: true });
|
|
1816
|
-
const finalPath = await lstat3(path, { bigint: true });
|
|
1817
|
-
if (!sameFileIdentity(opened, after) || !sameFileIdentity(after, finalPath) || BigInt(content.length) !== after.size) {
|
|
1818
|
-
throw new Error(`Update transaction path changed while it was captured: ${path}`);
|
|
1819
|
-
}
|
|
1820
|
-
return {
|
|
1821
|
-
kind: "file",
|
|
1822
|
-
content,
|
|
1823
|
-
device: after.dev,
|
|
1824
|
-
inode: after.ino,
|
|
1825
|
-
mode: after.mode,
|
|
1826
|
-
size: after.size,
|
|
1827
|
-
mtimeNs: after.mtimeNs,
|
|
1828
|
-
ctimeNs: after.ctimeNs,
|
|
1829
|
-
contentHash: createHash2("sha256").update(content).digest("hex")
|
|
1830
|
-
};
|
|
1831
|
-
} finally {
|
|
1832
|
-
await handle.close();
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
async function restorePath(entry, testing) {
|
|
1836
|
-
const { path, original, published, parentChain } = entry;
|
|
1837
|
-
if (!published)
|
|
1838
|
-
return [];
|
|
1839
|
-
if (original.kind === "missing") {
|
|
1840
|
-
if (published.kind === "file") {
|
|
1841
|
-
const quarantine2 = await quarantineExpectedFile(entry, published, testing);
|
|
1842
|
-
await unlinkVerifiedQuarantine(quarantine2, published, parentChain);
|
|
1843
|
-
return [];
|
|
1844
|
-
}
|
|
1845
|
-
if (published.kind === "directory") {
|
|
1846
|
-
const quarantine2 = await quarantineExpectedDirectory(entry, published, testing);
|
|
1847
|
-
await assertStableDirectoryChain(parentChain, "update rollback directory removal");
|
|
1848
|
-
await rmdir(quarantine2);
|
|
1849
|
-
await assertStableDirectoryChain(parentChain, "update rollback directory removal");
|
|
1850
|
-
return [];
|
|
1851
|
-
}
|
|
1852
|
-
return [];
|
|
1853
|
-
}
|
|
1854
|
-
if (original.kind === "directory") {
|
|
1855
|
-
if (published.kind === "missing") {
|
|
1856
|
-
await assertStableDirectoryChain(parentChain, "update rollback directory restore");
|
|
1857
|
-
await mkdir2(path, { mode: Number(permissionMode2(original.mode)) });
|
|
1858
|
-
if (process.platform !== "win32")
|
|
1859
|
-
await chmod(path, Number(permissionMode2(original.mode)));
|
|
1860
|
-
return [];
|
|
1861
|
-
}
|
|
1862
|
-
if (published.kind !== "directory")
|
|
1863
|
-
throw new Error(`Update rollback path type changed: ${path}`);
|
|
1864
|
-
const current = await inspectPathSnapshot(path);
|
|
1865
|
-
if (!samePathState(current, published))
|
|
1866
|
-
throw new UpdateRollbackMutationError(`Update rollback directory changed: ${path}`);
|
|
1867
|
-
if (process.platform !== "win32" && permissionMode2(published.mode) !== permissionMode2(original.mode)) {
|
|
1868
|
-
await assertStableDirectoryChain(parentChain, "update rollback directory mode restore");
|
|
1869
|
-
await chmod(path, Number(permissionMode2(original.mode)));
|
|
1870
|
-
}
|
|
1871
|
-
return [];
|
|
1872
|
-
}
|
|
1873
|
-
if (published.kind === "missing") {
|
|
1874
|
-
await writeSnapshotExclusive(path, original, parentChain, testing);
|
|
1875
|
-
return [];
|
|
1876
|
-
}
|
|
1877
|
-
if (published.kind !== "file")
|
|
1878
|
-
throw new Error(`Update rollback path type changed: ${path}`);
|
|
1879
|
-
const quarantine = await quarantineExpectedFile(entry, published, testing);
|
|
1880
|
-
try {
|
|
1881
|
-
await writeSnapshotExclusive(path, original, parentChain, testing);
|
|
1882
|
-
} catch (error) {
|
|
1883
|
-
const recovery = await restoreQuarantinedFile(
|
|
1884
|
-
quarantine,
|
|
1885
|
-
path,
|
|
1886
|
-
published,
|
|
1887
|
-
parentChain
|
|
1888
|
-
);
|
|
1889
|
-
if (recovery.length > 0) {
|
|
1890
|
-
throw new UpdateRollbackMutationError(
|
|
1891
|
-
`Update rollback preserved the current file after restore failure: ${path}`,
|
|
1892
|
-
recovery
|
|
1893
|
-
);
|
|
1894
|
-
}
|
|
1895
|
-
throw error;
|
|
1896
|
-
}
|
|
1897
|
-
await unlinkVerifiedQuarantine(quarantine, published, parentChain);
|
|
1898
|
-
return [];
|
|
1899
|
-
}
|
|
1900
|
-
async function quarantineExpectedFile(entry, expected, testing) {
|
|
1901
|
-
await testing.afterPublishedValidation?.(entry.path);
|
|
1902
|
-
await assertStableDirectoryChain(entry.parentChain, "update rollback file quarantine");
|
|
1903
|
-
const quarantine = `${entry.path}.${process.pid}.${randomUUID2()}.rsp-update-current`;
|
|
1904
|
-
await rename2(entry.path, quarantine);
|
|
1905
|
-
await assertStableDirectoryChain(entry.parentChain, "update rollback file quarantine");
|
|
1906
|
-
const quarantined = await inspectPathSnapshot(quarantine);
|
|
1907
|
-
if (!sameClaimedPathState(quarantined, expected)) {
|
|
1908
|
-
const recovery = await restoreQuarantinedPath(
|
|
1909
|
-
quarantine,
|
|
1910
|
-
entry.path,
|
|
1911
|
-
quarantined,
|
|
1912
|
-
entry.parentChain
|
|
1913
|
-
);
|
|
1914
|
-
throw new UpdateRollbackMutationError(
|
|
1915
|
-
`Update rollback refused a concurrently replaced file: ${entry.path}`,
|
|
1916
|
-
recovery
|
|
1917
|
-
);
|
|
1918
|
-
}
|
|
1919
|
-
return quarantine;
|
|
1920
|
-
}
|
|
1921
|
-
async function quarantineExpectedDirectory(entry, expected, testing) {
|
|
1922
|
-
await testing.afterPublishedValidation?.(entry.path);
|
|
1923
|
-
await assertStableDirectoryChain(entry.parentChain, "update rollback directory quarantine");
|
|
1924
|
-
const quarantine = `${entry.path}.${process.pid}.${randomUUID2()}.rsp-update-current`;
|
|
1925
|
-
await rename2(entry.path, quarantine);
|
|
1926
|
-
await assertStableDirectoryChain(entry.parentChain, "update rollback directory quarantine");
|
|
1927
|
-
const quarantined = await inspectPathSnapshot(quarantine);
|
|
1928
|
-
if (!sameClaimedPathState(quarantined, expected)) {
|
|
1929
|
-
const recovery = await restoreQuarantinedPath(
|
|
1930
|
-
quarantine,
|
|
1931
|
-
entry.path,
|
|
1932
|
-
quarantined,
|
|
1933
|
-
entry.parentChain
|
|
1934
|
-
);
|
|
1935
|
-
throw new UpdateRollbackMutationError(
|
|
1936
|
-
`Update rollback refused a concurrently replaced directory: ${entry.path}`,
|
|
1937
|
-
recovery
|
|
1938
|
-
);
|
|
1939
|
-
}
|
|
1940
|
-
return quarantine;
|
|
1941
|
-
}
|
|
1942
|
-
async function restoreQuarantinedPath(quarantine, destination, snapshot2, parentChain) {
|
|
1943
|
-
if (snapshot2.kind === "file")
|
|
1944
|
-
return restoreQuarantinedFile(quarantine, destination, snapshot2, parentChain);
|
|
1945
|
-
return [quarantine];
|
|
945
|
+
|
|
946
|
+
## Scenarios
|
|
947
|
+
<!-- Clarify key cross-boundary, failure, or ambiguous behavior with a few examples; omit when unnecessary and keep test execution results in the Change. -->
|
|
948
|
+
- ${placeholder}
|
|
949
|
+
|
|
950
|
+
## Constraints
|
|
951
|
+
<!-- Record necessary technical, protocol, security, or compatibility limits; do not repeat behavior requirements or decision rationale. -->
|
|
952
|
+
- ${placeholder}
|
|
953
|
+
`;
|
|
1946
954
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
await link2(quarantine, destination);
|
|
1951
|
-
const restored = await inspectPathSnapshot(destination);
|
|
1952
|
-
const retained = await inspectPathSnapshot(quarantine);
|
|
1953
|
-
if (!sameClaimedPathState(restored, snapshot2) || !sameClaimedPathState(retained, snapshot2)) {
|
|
1954
|
-
return [quarantine];
|
|
1955
|
-
}
|
|
1956
|
-
await unlinkVerifiedQuarantine(quarantine, snapshot2, parentChain);
|
|
1957
|
-
return [];
|
|
1958
|
-
} catch (error) {
|
|
1959
|
-
if (error.code === "EEXIST")
|
|
1960
|
-
return [quarantine];
|
|
1961
|
-
if (!hardLinkUnsupported2(error))
|
|
1962
|
-
throw error;
|
|
1963
|
-
}
|
|
955
|
+
|
|
956
|
+
// src/core/update-check.ts
|
|
957
|
+
async function checkForUpdates(current) {
|
|
1964
958
|
try {
|
|
1965
|
-
await
|
|
959
|
+
const response = await fetch("https://registry.npmjs.org/-/package/@oevery%2Frsp/dist-tags", {
|
|
960
|
+
signal: AbortSignal.timeout(2e3),
|
|
961
|
+
redirect: "error"
|
|
962
|
+
});
|
|
963
|
+
if (!response.ok)
|
|
964
|
+
throw new Error("registry unavailable");
|
|
965
|
+
const body = await response.text();
|
|
966
|
+
if (body.length > 16384)
|
|
967
|
+
throw new Error("registry metadata exceeded bound");
|
|
968
|
+
const latest = JSON.parse(body)?.latest;
|
|
969
|
+
if (typeof latest !== "string" || !isExactVersion(latest) || !/^\d+\.\d+\.\d+$/.test(latest))
|
|
970
|
+
throw new Error("invalid stable version");
|
|
971
|
+
return { status: compareVersions(latest, current) > 0 ? "available" : "current", current, latest };
|
|
1966
972
|
} catch {
|
|
1967
|
-
return
|
|
973
|
+
return { status: "unavailable", current };
|
|
1968
974
|
}
|
|
1969
|
-
return [quarantine];
|
|
1970
975
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
976
|
+
|
|
977
|
+
// src/commands/skills.ts
|
|
978
|
+
import { lstatSync, realpathSync } from "fs";
|
|
979
|
+
import { chmod, copyFile, mkdir, mkdtemp, readdir, readFile as readFile2, rename, rm } from "fs/promises";
|
|
980
|
+
import { dirname, join, relative } from "path";
|
|
981
|
+
var DEFAULT_PACKAGED_SKILL_NAMES = [
|
|
982
|
+
"rsp",
|
|
983
|
+
"rsp-shape",
|
|
984
|
+
"rsp-implement",
|
|
985
|
+
"rsp-doc",
|
|
986
|
+
"rsp-verify",
|
|
987
|
+
"rsp-review",
|
|
988
|
+
"rsp-commit"
|
|
989
|
+
];
|
|
990
|
+
var RETIRED_PACKAGED_SKILL_NAMES = [
|
|
991
|
+
"rsp-address-review",
|
|
992
|
+
"rsp-codebase-audit",
|
|
993
|
+
"rsp-structural-audit",
|
|
994
|
+
"rsp-design",
|
|
995
|
+
"rsp-diagnose",
|
|
996
|
+
"rsp-manage",
|
|
997
|
+
"rsp-release-docs",
|
|
998
|
+
"rsp-resolve-findings",
|
|
999
|
+
"rsp-tdd"
|
|
1000
|
+
];
|
|
1001
|
+
function unsupportedEntry(path, label) {
|
|
1002
|
+
return new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`);
|
|
1003
|
+
}
|
|
1004
|
+
async function inspectPackagedSkills(packageRoot) {
|
|
1005
|
+
const skillsRoot = join(packageRoot, "skills");
|
|
1006
|
+
const rootInspection = inspectManagedDirectory(skillsRoot, "packaged Skills root");
|
|
1007
|
+
if (rootInspection.issue)
|
|
1008
|
+
throw rootInspection.issue;
|
|
1009
|
+
const entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
1010
|
+
const skills = [];
|
|
1011
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1012
|
+
if (entry.name.startsWith("."))
|
|
1013
|
+
continue;
|
|
1014
|
+
const root = join(skillsRoot, entry.name);
|
|
1015
|
+
if (!entry.isDirectory())
|
|
1016
|
+
throw unsupportedEntry(root, "packaged Skills");
|
|
1017
|
+
const inspection = await inspectManagedFileTree(root, `packaged Skill ${entry.name}`, { includeHidden: true });
|
|
1018
|
+
if (inspection.issues.length > 0)
|
|
1019
|
+
throw inspection.issues[0];
|
|
1020
|
+
skills.push({
|
|
1021
|
+
name: entry.name,
|
|
1022
|
+
root,
|
|
1023
|
+
files: inspection.files.map((path) => relative(root, path))
|
|
1024
|
+
});
|
|
1979
1025
|
}
|
|
1980
|
-
|
|
1981
|
-
await assertStableDirectoryChain(parentChain, "update rollback quarantine cleanup");
|
|
1026
|
+
return skills;
|
|
1982
1027
|
}
|
|
1983
|
-
|
|
1984
|
-
let handle = null;
|
|
1985
|
-
let createdIdentity = null;
|
|
1028
|
+
function inspectTarget(path, name) {
|
|
1986
1029
|
try {
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
Number(permissionMode2(snapshot2.mode))
|
|
1992
|
-
);
|
|
1993
|
-
const opened = await handle.stat({ bigint: true });
|
|
1994
|
-
if (!opened.isFile() || opened.isSymbolicLink())
|
|
1995
|
-
throw new Error(`Update rollback could not open a regular restore file: ${path}`);
|
|
1996
|
-
createdIdentity = { device: opened.dev, inode: opened.ino };
|
|
1997
|
-
await testing.afterRestoreDestinationOpen?.(path);
|
|
1998
|
-
await handle.writeFile(snapshot2.content);
|
|
1999
|
-
await handle.chmod(Number(permissionMode2(snapshot2.mode)));
|
|
2000
|
-
await handle.sync();
|
|
2001
|
-
const value = await handle.stat({ bigint: true });
|
|
2002
|
-
if (!value.isFile() || value.isSymbolicLink() || value.dev !== createdIdentity.device || value.ino !== createdIdentity.inode || value.size !== snapshot2.size || permissionMode2(value.mode) !== permissionMode2(snapshot2.mode)) {
|
|
2003
|
-
throw new Error(`Update rollback could not validate restored file: ${path}`);
|
|
2004
|
-
}
|
|
1030
|
+
const stats = lstatSync(path);
|
|
1031
|
+
if (stats.isDirectory())
|
|
1032
|
+
return "directory";
|
|
1033
|
+
throw unsupportedEntry(path, `installed Skill ${name}`);
|
|
2005
1034
|
} catch (error) {
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
if (createdIdentity) {
|
|
2009
|
-
const recovery = await cleanupCreatedFile(
|
|
2010
|
-
path,
|
|
2011
|
-
createdIdentity,
|
|
2012
|
-
parentChain
|
|
2013
|
-
);
|
|
2014
|
-
if (recovery.length > 0) {
|
|
2015
|
-
throw new UpdateRollbackMutationError(
|
|
2016
|
-
`Update rollback exclusive restore preserved a concurrent replacement: ${path}`,
|
|
2017
|
-
recovery
|
|
2018
|
-
);
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
1035
|
+
if (error.code === "ENOENT")
|
|
1036
|
+
return "missing";
|
|
2021
1037
|
throw error;
|
|
2022
|
-
} finally {
|
|
2023
|
-
await handle?.close();
|
|
2024
|
-
}
|
|
2025
|
-
await assertStableDirectoryChain(parentChain, "update rollback exclusive restore");
|
|
2026
|
-
const restored = await inspectPathSnapshot(path);
|
|
2027
|
-
if (restored.kind !== "file" || restored.contentHash !== snapshot2.contentHash || permissionMode2(restored.mode) !== permissionMode2(snapshot2.mode)) {
|
|
2028
|
-
throw new Error(`Update rollback restored different bytes or mode: ${path}`);
|
|
2029
1038
|
}
|
|
2030
1039
|
}
|
|
2031
|
-
|
|
2032
|
-
const quarantine = `${path}.${process.pid}.${randomUUID2()}.rsp-update-created`;
|
|
2033
|
-
await assertStableDirectoryChain(parentChain, "update rollback exclusive cleanup");
|
|
1040
|
+
function inspectInventoryTarget(path) {
|
|
2034
1041
|
try {
|
|
2035
|
-
|
|
1042
|
+
return lstatSync(path).isDirectory() ? "directory" : "divergent";
|
|
2036
1043
|
} catch (error) {
|
|
2037
1044
|
if (error.code === "ENOENT")
|
|
2038
|
-
return
|
|
1045
|
+
return "missing";
|
|
2039
1046
|
throw error;
|
|
2040
1047
|
}
|
|
2041
|
-
await assertStableDirectoryChain(parentChain, "update rollback exclusive cleanup");
|
|
2042
|
-
const current = await lstat3(quarantine, { bigint: true });
|
|
2043
|
-
if (current.isFile() && !current.isSymbolicLink() && current.dev === expected.device && current.ino === expected.inode) {
|
|
2044
|
-
const finalIdentity = await lstat3(quarantine, { bigint: true });
|
|
2045
|
-
if (!finalIdentity.isFile() || finalIdentity.isSymbolicLink() || finalIdentity.dev !== expected.device || finalIdentity.ino !== expected.inode) {
|
|
2046
|
-
return [quarantine];
|
|
2047
|
-
}
|
|
2048
|
-
await unlink4(quarantine);
|
|
2049
|
-
return [];
|
|
2050
|
-
}
|
|
2051
|
-
const snapshot2 = await inspectPathSnapshot(quarantine);
|
|
2052
|
-
return restoreQuarantinedPath(quarantine, path, snapshot2, parentChain);
|
|
2053
|
-
}
|
|
2054
|
-
async function persistOriginalRecovery(original) {
|
|
2055
|
-
if (original.kind !== "file")
|
|
2056
|
-
return null;
|
|
2057
|
-
const recoveryRoot = await mkdtemp(join2(tmpdir(), "rsp-update-recovery-"));
|
|
2058
|
-
if (process.platform !== "win32")
|
|
2059
|
-
await chmod(recoveryRoot, 448);
|
|
2060
|
-
const recovery = join2(recoveryRoot, "snapshot");
|
|
2061
|
-
const parentChain = await captureStableDirectoryChain({
|
|
2062
|
-
rootPath: recoveryRoot,
|
|
2063
|
-
targetPath: recoveryRoot,
|
|
2064
|
-
label: "update rollback recovery root"
|
|
2065
|
-
});
|
|
2066
|
-
await writeSnapshotExclusive(recovery, original, parentChain, {});
|
|
2067
|
-
return recovery;
|
|
2068
|
-
}
|
|
2069
|
-
async function captureExistingParentChain(projectRoot, path) {
|
|
2070
|
-
let parent = dirname2(path);
|
|
2071
|
-
while (true) {
|
|
2072
|
-
const rel = relative3(projectRoot, parent);
|
|
2073
|
-
if (rel === ".." || rel.startsWith(`..${sep3}`))
|
|
2074
|
-
throw new Error(`Update transaction parent escapes the project root: ${path}`);
|
|
2075
|
-
try {
|
|
2076
|
-
const value = await lstat3(parent);
|
|
2077
|
-
if (!value.isDirectory() || value.isSymbolicLink())
|
|
2078
|
-
throw new Error(`Update transaction parent must be a real directory: ${parent}`);
|
|
2079
|
-
break;
|
|
2080
|
-
} catch (error) {
|
|
2081
|
-
if (error.code !== "ENOENT")
|
|
2082
|
-
throw error;
|
|
2083
|
-
if (parent === projectRoot)
|
|
2084
|
-
throw error;
|
|
2085
|
-
parent = dirname2(parent);
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
return captureStableDirectoryChain({
|
|
2089
|
-
rootPath: projectRoot,
|
|
2090
|
-
targetPath: parent,
|
|
2091
|
-
label: `update transaction ${path}`
|
|
2092
|
-
});
|
|
2093
1048
|
}
|
|
2094
|
-
function
|
|
2095
|
-
|
|
1049
|
+
function isCanonicalSourceProjection(sourceRoot, targetRoot) {
|
|
1050
|
+
try {
|
|
1051
|
+
if (!lstatSync(targetRoot).isSymbolicLink())
|
|
1052
|
+
return false;
|
|
1053
|
+
return realpathSync(targetRoot) === realpathSync(sourceRoot);
|
|
1054
|
+
} catch {
|
|
2096
1055
|
return false;
|
|
2097
|
-
if (left.kind === "missing")
|
|
2098
|
-
return true;
|
|
2099
|
-
if (left.kind === "directory" && right.kind === "directory") {
|
|
2100
|
-
return left.device === right.device && left.inode === right.inode && permissionMode2(left.mode) === permissionMode2(right.mode);
|
|
2101
|
-
}
|
|
2102
|
-
if (left.kind === "file" && right.kind === "file") {
|
|
2103
|
-
return left.device === right.device && left.inode === right.inode && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs && permissionMode2(left.mode) === permissionMode2(right.mode) && left.contentHash === right.contentHash;
|
|
2104
1056
|
}
|
|
2105
|
-
return false;
|
|
2106
1057
|
}
|
|
2107
|
-
function
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
if (
|
|
2113
|
-
return pathDepth(left.path) - pathDepth(right.path);
|
|
2114
|
-
return pathDepth(right.path) - pathDepth(left.path);
|
|
2115
|
-
}
|
|
2116
|
-
function sameClaimedPathState(left, right) {
|
|
2117
|
-
if (left.kind !== right.kind)
|
|
1058
|
+
async function treesEqual(source, targetRoot) {
|
|
1059
|
+
const target = await inspectManagedFileTree(targetRoot, `installed Skill ${source.name}`, { includeHidden: true });
|
|
1060
|
+
if (target.issues.length > 0)
|
|
1061
|
+
throw target.issues[0];
|
|
1062
|
+
const targetFiles = target.files.map((path) => relative(targetRoot, path));
|
|
1063
|
+
if (source.files.length !== targetFiles.length || source.files.some((path, index) => path !== targetFiles[index]))
|
|
2118
1064
|
return false;
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
1065
|
+
for (const path of source.files) {
|
|
1066
|
+
const sourcePath = join(source.root, path);
|
|
1067
|
+
const targetPath = join(targetRoot, path);
|
|
1068
|
+
const [sourceStats, targetStats] = [lstatSync(sourcePath), lstatSync(targetPath)];
|
|
1069
|
+
if (!sourceStats.isFile() || !targetStats.isFile())
|
|
1070
|
+
throw unsupportedEntry(!sourceStats.isFile() ? sourcePath : targetPath, `Skill ${source.name}`);
|
|
1071
|
+
if ((sourceStats.mode & 511) !== (targetStats.mode & 511))
|
|
1072
|
+
return false;
|
|
1073
|
+
const [sourceContent, targetContent] = await Promise.all([readFile2(sourcePath), readFile2(targetPath)]);
|
|
1074
|
+
if (!sourceContent.equals(targetContent))
|
|
1075
|
+
return false;
|
|
2126
1076
|
}
|
|
2127
|
-
return
|
|
2128
|
-
}
|
|
2129
|
-
function sameFileIdentity(left, right) {
|
|
2130
|
-
return left.isFile() && right.isFile() && !left.isSymbolicLink() && !right.isSymbolicLink() && left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
2131
|
-
}
|
|
2132
|
-
function hardLinkUnsupported2(error) {
|
|
2133
|
-
return ["EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM"].includes(error.code ?? "");
|
|
2134
|
-
}
|
|
2135
|
-
function permissionMode2(mode) {
|
|
2136
|
-
return mode & 0o777n;
|
|
2137
|
-
}
|
|
2138
|
-
function pathDepth(path) {
|
|
2139
|
-
return path.split(sep3).length;
|
|
1077
|
+
return true;
|
|
2140
1078
|
}
|
|
2141
|
-
function
|
|
2142
|
-
|
|
1079
|
+
async function inspectPackagedSkillInventory(options = {}) {
|
|
1080
|
+
const packageRoot = options.packageRoot ?? PKG_ROOT;
|
|
1081
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
1082
|
+
const packagedSkills = await inspectPackagedSkills(packageRoot);
|
|
1083
|
+
const packagedNames = new Set(packagedSkills.map((skill) => skill.name));
|
|
1084
|
+
const missingDefaults = DEFAULT_PACKAGED_SKILL_NAMES.filter((name) => !packagedNames.has(name));
|
|
1085
|
+
if (missingDefaults.length > 0)
|
|
1086
|
+
throw new Error(`packaged Skills missing default Skills: ${missingDefaults.join(", ")}`);
|
|
1087
|
+
const manifest = JSON.parse(await readFile2(join(packageRoot, "package.json"), "utf8"));
|
|
1088
|
+
if (typeof manifest.name !== "string" || typeof manifest.version !== "string")
|
|
1089
|
+
throw new Error("package.json must contain string name and version fields");
|
|
1090
|
+
const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
1091
|
+
const defaults = new Set(DEFAULT_PACKAGED_SKILL_NAMES);
|
|
1092
|
+
const skills = [];
|
|
1093
|
+
for (const skill of packagedSkills) {
|
|
1094
|
+
const target = join(targetRoot, skill.name);
|
|
1095
|
+
const targetStatus = inspectInventoryTarget(target);
|
|
1096
|
+
skills.push({
|
|
1097
|
+
name: skill.name,
|
|
1098
|
+
kind: defaults.has(skill.name) ? "default" : "optional",
|
|
1099
|
+
status: targetStatus === "missing" ? "missing" : targetStatus === "divergent" ? isCanonicalSourceProjection(skill.root, target) ? "unchanged" : "divergent" : await treesEqual(skill, target) ? "unchanged" : "divergent"
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
const retired = RETIRED_PACKAGED_SKILL_NAMES.filter((name) => inspectInventoryTarget(join(targetRoot, name)) !== "missing");
|
|
1103
|
+
return {
|
|
1104
|
+
retired,
|
|
1105
|
+
package: { name: manifest.name, version: manifest.version },
|
|
1106
|
+
target: relative(projectRoot, targetRoot) || ".",
|
|
1107
|
+
skills
|
|
1108
|
+
};
|
|
2143
1109
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
1110
|
+
async function copySkillTree(source, targetRoot) {
|
|
1111
|
+
await mkdir(targetRoot, { recursive: true });
|
|
1112
|
+
for (const path of source.files) {
|
|
1113
|
+
const sourcePath = join(source.root, path);
|
|
1114
|
+
const targetPath = join(targetRoot, path);
|
|
1115
|
+
const stats = lstatSync(sourcePath);
|
|
1116
|
+
if (!stats.isFile())
|
|
1117
|
+
throw unsupportedEntry(sourcePath, `packaged Skill ${source.name}`);
|
|
1118
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
1119
|
+
await copyFile(sourcePath, targetPath);
|
|
1120
|
+
await chmod(targetPath, stats.mode & 511);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
async function updatePackagedSkills(options = {}) {
|
|
1124
|
+
const packageRoot = options.packageRoot ?? PKG_ROOT;
|
|
1125
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
1126
|
+
const renamePath = options.renamePath ?? rename;
|
|
1127
|
+
const packaged = await inspectPackagedSkills(packageRoot);
|
|
1128
|
+
const skills = DEFAULT_PACKAGED_SKILL_NAMES.map((name) => {
|
|
1129
|
+
const skill = packaged.find((candidate) => candidate.name === name);
|
|
1130
|
+
if (!skill)
|
|
1131
|
+
throw new Error(`Packaged Skill missing: ${name}`);
|
|
1132
|
+
return skill;
|
|
1133
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
1134
|
+
const targetRoot = resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
1135
|
+
const result = { installed: [], removed: [], unchanged: [], replaced: [] };
|
|
1136
|
+
for (const skill of skills) {
|
|
1137
|
+
const target = join(targetRoot, skill.name);
|
|
1138
|
+
if (isCanonicalSourceProjection(skill.root, target))
|
|
1139
|
+
result.unchanged.push(skill.name);
|
|
1140
|
+
else if (inspectTarget(target, skill.name) === "missing")
|
|
1141
|
+
result.installed.push(skill.name);
|
|
1142
|
+
else if (await treesEqual(skill, target))
|
|
1143
|
+
result.unchanged.push(skill.name);
|
|
1144
|
+
else
|
|
1145
|
+
result.replaced.push(skill.name);
|
|
2165
1146
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
throw new Error(decisionRecordsFilesystemIssue);
|
|
2182
|
-
const specsMigrationPlan = await inspectGeneratedSpecsIndexMigration();
|
|
2183
|
-
if (!specsMigrationPlan.inspectionComplete || specsMigrationPlan.ownerControlled.length > 0) {
|
|
2184
|
-
const ownerMessage = specsMigrationPlan.ownerControlled.length > 0 ? ` Owner-controlled reserved path(s): ${specsMigrationPlan.ownerControlled.join(", ")}.` : "";
|
|
2185
|
-
const diagnosticMessage = specsMigrationPlan.diagnostics.length > 0 ? ` ${specsMigrationPlan.diagnostics.join("; ")}` : "";
|
|
2186
|
-
throw new Error(`Generated Specs-index migration requires owner review before update.${ownerMessage}${diagnosticMessage}`);
|
|
1147
|
+
for (const name of RETIRED_PACKAGED_SKILL_NAMES) {
|
|
1148
|
+
if (inspectTarget(join(targetRoot, name), name) !== "missing")
|
|
1149
|
+
result.removed.push(name);
|
|
1150
|
+
}
|
|
1151
|
+
if (options.dryRun || result.installed.length + result.replaced.length + result.removed.length === 0)
|
|
1152
|
+
return result;
|
|
1153
|
+
await mkdir(targetRoot, { recursive: true });
|
|
1154
|
+
resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
1155
|
+
const staging = await mkdtemp(join(targetRoot, ".rsp-skills-"));
|
|
1156
|
+
const completed = [];
|
|
1157
|
+
let failure2;
|
|
1158
|
+
try {
|
|
1159
|
+
for (const skill of skills) {
|
|
1160
|
+
if (!result.unchanged.includes(skill.name))
|
|
1161
|
+
await copySkillTree(skill, join(staging, skill.name));
|
|
2187
1162
|
}
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
const configFileInspection = inspectManagedFile(CONFIG_PATH, "config file", { allowMissing: true });
|
|
2194
|
-
if (configFileInspection.issue)
|
|
2195
|
-
throw configFileInspection.issue;
|
|
2196
|
-
const configContent = configFileInspection.exists ? await readFile4(CONFIG_PATH, "utf-8") : null;
|
|
2197
|
-
const reconciledConfig = configContent === null ? { content: generateConfigTemplate(), added: ["all defaults"], changed: true } : reconcileRspConfigDefaults(configContent);
|
|
2198
|
-
if (reconciledConfig.changed) {
|
|
2199
|
-
await rollbackJournal.capture(CONFIG_PATH);
|
|
2200
|
-
await writeManagedFile(CONFIG_PATH, reconciledConfig.content, "config file");
|
|
2201
|
-
await rollbackJournal.mark(CONFIG_PATH);
|
|
2202
|
-
const detail = reconciledConfig.added.length > 0 ? `defaults added: ${reconciledConfig.added.join(", ")}` : "generated layout normalized";
|
|
2203
|
-
actions.push(`config.yaml ${detail}`);
|
|
2204
|
-
events.push({ type: "updated", message: `config.yaml ${detail}` });
|
|
2205
|
-
updated = true;
|
|
2206
|
-
}
|
|
2207
|
-
const changesInspection = inspectManagedDirectory(CHANGES_DIR, "open work root", { allowMissing: true });
|
|
2208
|
-
if (changesInspection.issue)
|
|
2209
|
-
throw changesInspection.issue;
|
|
2210
|
-
if (!changesInspection.exists) {
|
|
2211
|
-
const changesDirectoryChain = await rollbackJournal.captureDirectoryChain(CHANGES_DIR);
|
|
2212
|
-
const changesPlaceholder = join3(CHANGES_DIR, ".gitkeep");
|
|
2213
|
-
await rollbackJournal.capture(changesPlaceholder);
|
|
2214
|
-
await mkdir3(CHANGES_DIR, { recursive: true });
|
|
2215
|
-
await ensureManagedFile(changesPlaceholder, "", "changes placeholder");
|
|
2216
|
-
await rollbackJournal.mark(changesPlaceholder);
|
|
2217
|
-
await rollbackJournal.markAll(changesDirectoryChain);
|
|
2218
|
-
actions.push("changes/ directory restored");
|
|
2219
|
-
events.push({ type: "updated", message: "changes/ directory restored" });
|
|
2220
|
-
updated = true;
|
|
2221
|
-
}
|
|
2222
|
-
const archivesInspection = inspectManagedDirectory(join3(RSP_DIR, "archives"), "archive root", { allowMissing: true });
|
|
2223
|
-
if (archivesInspection.issue)
|
|
2224
|
-
throw archivesInspection.issue;
|
|
2225
|
-
const bundledRules = await readFile4(join3(options.testing?.packageRoot ?? PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
2226
|
-
const rulesInspection = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
2227
|
-
if (rulesInspection.issue)
|
|
2228
|
-
throw rulesInspection.issue;
|
|
2229
|
-
const existingRules = rulesInspection.exists ? await readFile4(RSP_RULES_PATH, "utf-8") : null;
|
|
2230
|
-
if (existingRules !== bundledRules) {
|
|
2231
|
-
await rollbackJournal.capture(RSP_RULES_PATH);
|
|
2232
|
-
await writeManagedFile(RSP_RULES_PATH, bundledRules, "fallback protocol");
|
|
2233
|
-
await rollbackJournal.mark(RSP_RULES_PATH);
|
|
2234
|
-
actions.push("rsp-rules.md updated");
|
|
2235
|
-
events.push({ type: "updated", message: "rsp-rules.md updated" });
|
|
2236
|
-
updated = true;
|
|
2237
|
-
}
|
|
2238
|
-
if (existsSync(OBSOLETE_RSP_RULES_PATH)) {
|
|
2239
|
-
await rollbackJournal.capture(OBSOLETE_RSP_RULES_PATH);
|
|
2240
|
-
await rm(OBSOLETE_RSP_RULES_PATH);
|
|
2241
|
-
await rollbackJournal.mark(OBSOLETE_RSP_RULES_PATH);
|
|
2242
|
-
actions.push("obsolete rules/rsp-rules.md removed");
|
|
2243
|
-
events.push({ type: "updated", message: "obsolete rules/rsp-rules.md removed" });
|
|
2244
|
-
updated = true;
|
|
2245
|
-
}
|
|
2246
|
-
try {
|
|
2247
|
-
const rulesRoot = join3(RSP_DIR, "rules");
|
|
2248
|
-
await rollbackJournal.capture(rulesRoot);
|
|
2249
|
-
await rmdir2(rulesRoot);
|
|
2250
|
-
await rollbackJournal.mark(rulesRoot);
|
|
2251
|
-
actions.push("empty rules directory removed");
|
|
2252
|
-
updated = true;
|
|
2253
|
-
} catch (error) {
|
|
2254
|
-
if (!["ENOENT", "ENOTEMPTY"].includes(error.code || ""))
|
|
2255
|
-
throw error;
|
|
2256
|
-
}
|
|
2257
|
-
let inspection = await inspectUnsupportedRules();
|
|
2258
|
-
if (inspection.diagnostics.length === 0 && inspection.directoryRemaining && inspection.entries.every((entry) => entry.kind === "directory")) {
|
|
2259
|
-
const directories = inspection.entries.map((entry) => entry.path).sort((left, right) => right.split("/").length - left.split("/").length);
|
|
2260
|
-
for (const path of directories) {
|
|
2261
|
-
const directoryPath = join3(RSP_DIR, "rules", path);
|
|
2262
|
-
await rollbackJournal.capture(directoryPath);
|
|
2263
|
-
await rmdir2(directoryPath);
|
|
2264
|
-
await rollbackJournal.mark(directoryPath);
|
|
2265
|
-
}
|
|
2266
|
-
const rulesRoot = join3(RSP_DIR, "rules");
|
|
2267
|
-
await rollbackJournal.capture(rulesRoot);
|
|
2268
|
-
await rmdir2(rulesRoot);
|
|
2269
|
-
await rollbackJournal.mark(rulesRoot);
|
|
2270
|
-
actions.push("empty rules directory tree removed");
|
|
2271
|
-
events.push({ type: "updated", message: "empty rules directory tree removed" });
|
|
2272
|
-
updated = true;
|
|
2273
|
-
inspection = await inspectUnsupportedRules();
|
|
2274
|
-
}
|
|
2275
|
-
const residualEntries = inspection.entries.map((entry) => entry.path);
|
|
2276
|
-
if (inspection.diagnostics.length > 0)
|
|
2277
|
-
events.push({ type: "migration-diagnostics", diagnostics: inspection.diagnostics });
|
|
2278
|
-
if (residualEntries.length > 0)
|
|
2279
|
-
events.push({ type: "residual-rules", entries: residualEntries });
|
|
2280
|
-
const projectName = await detectProjectName();
|
|
2281
|
-
const baseAgents = agentsInspection.exists ? await readFile4(agentsPath, "utf-8") : `# ${projectName}
|
|
2282
|
-
|
|
2283
|
-
`;
|
|
2284
|
-
const nextAgents = upsertRspAgentsBlock(baseAgents);
|
|
2285
|
-
if (!agentsInspection.exists || nextAgents.changed) {
|
|
2286
|
-
await rollbackJournal.capture(agentsPath);
|
|
2287
|
-
await writeManagedFile(agentsPath, nextAgents.content, "AGENTS.md");
|
|
2288
|
-
await rollbackJournal.mark(agentsPath);
|
|
2289
|
-
actions.push("AGENTS.md managed block refreshed");
|
|
2290
|
-
events.push({ type: "updated", message: "AGENTS.md managed block refreshed" });
|
|
2291
|
-
updated = true;
|
|
2292
|
-
}
|
|
2293
|
-
const decisionRecordsDirectoryChain = await rollbackJournal.captureDirectoryChain(decisionRecordsPath);
|
|
2294
|
-
const decisionRecordsPlaceholder = join3(decisionRecordsPath, ".gitkeep");
|
|
2295
|
-
await rollbackJournal.capture(decisionRecordsPlaceholder);
|
|
2296
|
-
if (await ensureDecisionRecordsDirectory(decisionRecordsPath)) {
|
|
2297
|
-
await rollbackJournal.mark(decisionRecordsPlaceholder);
|
|
2298
|
-
await rollbackJournal.markAll(decisionRecordsDirectoryChain);
|
|
2299
|
-
actions.push(`Decision Record directory ensured: ${decisionRecordsPath}`);
|
|
2300
|
-
events.push({ type: "updated", message: `Decision Record directory ensured: ${decisionRecordsPath}` });
|
|
2301
|
-
updated = true;
|
|
2302
|
-
}
|
|
2303
|
-
const legacyArchiveIndexPath = join3(RSP_DIR, "archives", "INDEX.md");
|
|
2304
|
-
await rollbackJournal.capture(legacyArchiveIndexPath);
|
|
2305
|
-
if (await removeLegacyArchiveIndex()) {
|
|
2306
|
-
await rollbackJournal.mark(legacyArchiveIndexPath);
|
|
2307
|
-
actions.push("legacy Archive Index removed");
|
|
2308
|
-
events.push({ type: "updated", message: "legacy Archive Index removed" });
|
|
2309
|
-
updated = true;
|
|
2310
|
-
}
|
|
2311
|
-
for (const path of specsMigrationPlan.removable)
|
|
2312
|
-
await rollbackJournal.capture(path);
|
|
2313
|
-
await options.testing?.afterPreMutations?.();
|
|
2314
|
-
const removedSpecsIndexes = await removeRecognizedGeneratedSpecsIndexes();
|
|
2315
|
-
for (const path of specsMigrationPlan.removable)
|
|
2316
|
-
await rollbackJournal.mark(path);
|
|
2317
|
-
await options.testing?.afterSpecsMigration?.();
|
|
2318
|
-
if (removedSpecsIndexes.length > 0) {
|
|
2319
|
-
actions.push(`generated Specs indexes removed: ${removedSpecsIndexes.join(", ")}`);
|
|
2320
|
-
events.push({ type: "specs-indexes-removed", paths: removedSpecsIndexes });
|
|
2321
|
-
updated = true;
|
|
2322
|
-
}
|
|
2323
|
-
return {
|
|
2324
|
-
ok: inspection.diagnostics.length === 0,
|
|
2325
|
-
actions,
|
|
2326
|
-
updated,
|
|
2327
|
-
events,
|
|
2328
|
-
migration: {
|
|
2329
|
-
inspectionComplete: inspection.diagnostics.length === 0,
|
|
2330
|
-
manualMigrationRequired: inspection.directoryRemaining,
|
|
2331
|
-
residualEntries,
|
|
2332
|
-
diagnostics: inspection.diagnostics
|
|
2333
|
-
}
|
|
2334
|
-
};
|
|
2335
|
-
} catch (error) {
|
|
2336
|
-
const rollback = await rollbackJournal.rollback();
|
|
2337
|
-
clearConfigCache();
|
|
2338
|
-
if (rollback.retainedMutations.length > 0 || rollback.recoveryPaths.length > 0) {
|
|
2339
|
-
throw new UpdateTransactionError(
|
|
2340
|
-
`RSP update failed and could not fully restore its command-scoped mutations: ${error instanceof Error ? error.message : String(error)}`,
|
|
2341
|
-
actions,
|
|
2342
|
-
rollback,
|
|
2343
|
-
{ cause: error }
|
|
2344
|
-
);
|
|
2345
|
-
}
|
|
2346
|
-
throw error;
|
|
1163
|
+
for (const name of result.removed) {
|
|
1164
|
+
resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
1165
|
+
inspectTarget(join(targetRoot, name), name);
|
|
1166
|
+
await rm(join(targetRoot, name), { recursive: true, force: true });
|
|
1167
|
+
completed.push(`removed ${name}`);
|
|
2347
1168
|
}
|
|
2348
|
-
|
|
1169
|
+
for (const skill of skills) {
|
|
1170
|
+
if (result.unchanged.includes(skill.name))
|
|
1171
|
+
continue;
|
|
1172
|
+
resolveManagedDirectoryChain(projectRoot, [".agents", "skills"], "project Skills root");
|
|
1173
|
+
const target = join(targetRoot, skill.name);
|
|
1174
|
+
inspectTarget(target, skill.name);
|
|
1175
|
+
await rm(target, { recursive: true, force: true });
|
|
1176
|
+
await renamePath(join(staging, skill.name), target);
|
|
1177
|
+
completed.push(`installed ${skill.name}`);
|
|
1178
|
+
}
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
failure2 = new Error(`Skill update stopped; completed: ${completed.join(", ") || "none"}. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
await rm(staging, { recursive: true });
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
failure2 = new Error(`Temporary Skill content cleanup failed: ${staging}`, { cause: failure2 ?? error });
|
|
1186
|
+
}
|
|
1187
|
+
if (failure2)
|
|
1188
|
+
throw failure2;
|
|
1189
|
+
return result;
|
|
2349
1190
|
}
|
|
2350
1191
|
|
|
2351
1192
|
// src/commands/doctor.ts
|
|
2352
1193
|
async function runDoctor(options = {}) {
|
|
2353
|
-
const fixed = [];
|
|
2354
|
-
let repairIssue = null;
|
|
2355
|
-
if (options.fix && existsSync2(RSP_DIR)) {
|
|
2356
|
-
try {
|
|
2357
|
-
const update = await updateProject({ testing: options.testing });
|
|
2358
|
-
fixed.push(...update.actions);
|
|
2359
|
-
} catch (error) {
|
|
2360
|
-
if (error instanceof UpdateTransactionError) {
|
|
2361
|
-
fixed.push(
|
|
2362
|
-
...error.rollback.retainedMutations.map((path) => `partial update mutation retained: ${path}`),
|
|
2363
|
-
...error.rollback.recoveryPaths.map((path) => `update recovery copy retained: ${path}`)
|
|
2364
|
-
);
|
|
2365
|
-
}
|
|
2366
|
-
repairIssue = `safe deterministic repair could not run: ${toErrorMessage(error)}`;
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
1194
|
const checks = [];
|
|
2370
|
-
|
|
1195
|
+
const runtime = [];
|
|
1196
|
+
const reportRuntime = (diagnostic) => runtime.push(diagnostic);
|
|
1197
|
+
reportCheck(checks, ".rsp exists", existsSync(RSP_DIR), "Run: rsp init");
|
|
1198
|
+
reportRetiredProtocol(checks);
|
|
1199
|
+
const sourceMode = await reportProjectRuntime(checks);
|
|
1200
|
+
if (options.checkUpdates) {
|
|
1201
|
+
const notice = await checkForUpdates(await getVersion());
|
|
2371
1202
|
checks.push({
|
|
2372
|
-
status: "
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
1203
|
+
status: "info",
|
|
1204
|
+
code: `update_${notice.status}`,
|
|
1205
|
+
label: "stable package update check",
|
|
1206
|
+
message: notice.status === "available" ? `RSP ${notice.latest} is available; running ${notice.current}. Project selection is unchanged.` : notice.status === "current" ? `No newer stable package found for ${notice.current}.` : "Update metadata is unavailable; local checks are unaffected.",
|
|
1207
|
+
hint: sourceMode ? "Stable package metadata does not establish source freshness. Keep using the local source CLI; this notice does not select a package runtime." : notice.status === "available" ? "Inspect migration guidance and choose an exact target. Upgrade notices authorize no update." : void 0
|
|
2376
1208
|
});
|
|
2377
1209
|
}
|
|
2378
|
-
const runtime = [];
|
|
2379
|
-
const reportRuntime = (diagnostic) => runtime.push(diagnostic);
|
|
2380
|
-
const designPath = join4(RSP_DIR, "specs", "design.md");
|
|
2381
|
-
reportCheck(checks, ".rsp exists", existsSync2(RSP_DIR), "Run: rsp init");
|
|
2382
|
-
reportFallbackProtocol(checks);
|
|
2383
1210
|
await reportUnsupportedRules(checks, reportRuntime);
|
|
2384
|
-
|
|
1211
|
+
const initialization = inspectRspInitialization();
|
|
1212
|
+
checks.push({ status: initialization ? "issue" : "ok", label: "specs/design.md exists", message: initialization });
|
|
2385
1213
|
await checkAgents(checks, reportRuntime);
|
|
2386
|
-
|
|
1214
|
+
if (existsSync("CONTEXT-MAP.md")) {
|
|
1215
|
+
checks.push({
|
|
1216
|
+
status: "info",
|
|
1217
|
+
code: "legacy_context_map",
|
|
1218
|
+
label: "legacy root context map detected",
|
|
1219
|
+
message: "CONTEXT-MAP.md remains; its semantic migration requires owner review.",
|
|
1220
|
+
hint: "Use Core context migration. Retain relevant legacy context until an authorized merge updates CONTEXT.md and active references; update does not migrate or delete it."
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
2387
1223
|
await checkArchiveNaming(checks);
|
|
2388
|
-
const
|
|
2389
|
-
if (
|
|
1224
|
+
const configValid = await checkConfigSemantics(checks, reportRuntime);
|
|
1225
|
+
if (configValid) {
|
|
1226
|
+
await checkSpecsTree(checks, reportRuntime);
|
|
2390
1227
|
await checkDecisionRecordsDirectory(checks, reportRuntime);
|
|
2391
1228
|
await checkInactiveDefaultDecisionRecords(checks, reportRuntime);
|
|
2392
1229
|
}
|
|
2393
1230
|
await checkActiveChangeConsistency(checks);
|
|
1231
|
+
if (configValid)
|
|
1232
|
+
await checkArchitecture(checks);
|
|
2394
1233
|
const result = {
|
|
2395
1234
|
command: "doctor",
|
|
2396
1235
|
ok: checks.every((check) => check.status !== "issue"),
|
|
2397
|
-
fixed,
|
|
2398
1236
|
checks,
|
|
2399
1237
|
runtime,
|
|
2400
1238
|
summary: {
|
|
@@ -2403,32 +1241,88 @@ async function runDoctor(options = {}) {
|
|
|
2403
1241
|
};
|
|
2404
1242
|
return result;
|
|
2405
1243
|
}
|
|
2406
|
-
function
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
1244
|
+
async function checkArchitecture(checks) {
|
|
1245
|
+
try {
|
|
1246
|
+
const architecture = await queryArchitecture();
|
|
1247
|
+
if (!architecture.configured) {
|
|
1248
|
+
checks.push({
|
|
1249
|
+
status: "info",
|
|
1250
|
+
code: "architecture_not_configured",
|
|
1251
|
+
label: ARCHITECTURE_FILE,
|
|
1252
|
+
message: "Optional architecture model is not configured."
|
|
1253
|
+
});
|
|
1254
|
+
} else if (architecture.diagnostics.length) {
|
|
1255
|
+
for (const diagnostic of architecture.diagnostics) {
|
|
1256
|
+
checks.push({
|
|
1257
|
+
status: "issue",
|
|
1258
|
+
code: `architecture_${diagnostic.code}`,
|
|
1259
|
+
label: `architecture binding: ${diagnostic.file ?? ARCHITECTURE_FILE}`,
|
|
1260
|
+
message: `${diagnostic.file ?? ARCHITECTURE_FILE}: ${diagnostic.message}`
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
} else {
|
|
1264
|
+
checks.push({
|
|
1265
|
+
status: "ok",
|
|
1266
|
+
code: "architecture_valid",
|
|
1267
|
+
label: "architecture structure and document bindings",
|
|
1268
|
+
message: `${architecture.nodes.length} nodes, ${architecture.relations.length} relations, ${architecture.views.length} views. Structural and reference validity does not establish semantic agreement with implementation.`
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
} catch (error) {
|
|
2410
1272
|
checks.push({
|
|
2411
1273
|
status: "issue",
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
1274
|
+
code: "architecture_inspection_failed",
|
|
1275
|
+
label: ARCHITECTURE_FILE,
|
|
1276
|
+
message: `${ARCHITECTURE_FILE}: ${toErrorMessage(error)}`
|
|
2415
1277
|
});
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async function reportProjectRuntime(checks) {
|
|
1281
|
+
let sourceMode = false;
|
|
1282
|
+
try {
|
|
1283
|
+
const selected = await readProjectRuntime();
|
|
1284
|
+
sourceMode = selected.mode === "source";
|
|
1285
|
+
const running = await getVersion();
|
|
2419
1286
|
checks.push({
|
|
2420
|
-
status: "issue",
|
|
2421
|
-
|
|
2422
|
-
|
|
1287
|
+
status: selected.mode === "source" || selected.version === running ? "ok" : "issue",
|
|
1288
|
+
code: "project_cli_version",
|
|
1289
|
+
label: "project CLI runtime",
|
|
1290
|
+
message: selected.mode === "source" ? `Source mode: this checkout's local CLI; package metadata ${running} is not source identity.` : selected.version ? `Project pins ${selected.version}; running ${running}.` : "Project has no exact CLI pin.",
|
|
1291
|
+
hint: selected.mode === "source" ? `Build current source, then use ${SOURCE_INVOCATION} <command>. Retain commit, uncommitted diff and build inputs for candidate evidence.` : selected.version ? `Use ${exactInvocation(selected.version)} <command>; only an authorized migration uses update.` : "Choose an exact CLI version, then run its update command."
|
|
2423
1292
|
});
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
checks.push({ status: "issue", code: "project_cli_version", label: "project CLI runtime", message: toErrorMessage(error) });
|
|
2424
1295
|
}
|
|
2425
|
-
|
|
1296
|
+
try {
|
|
1297
|
+
const inventory = await inspectPackagedSkillInventory();
|
|
1298
|
+
const drift = [
|
|
1299
|
+
...inventory.skills.filter((skill) => skill.status !== "unchanged").map((skill) => `${skill.name}: ${skill.status}`),
|
|
1300
|
+
...inventory.retired.map((name) => `${name}: retired`)
|
|
1301
|
+
];
|
|
2426
1302
|
checks.push({
|
|
2427
|
-
status: "issue",
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
1303
|
+
status: drift.length === 0 ? "ok" : "issue",
|
|
1304
|
+
code: "project_skill_suite",
|
|
1305
|
+
label: "project Skill suite matches running package",
|
|
1306
|
+
message: drift.length > 0 ? drift.join(", ") : void 0,
|
|
1307
|
+
hint: drift.length > 0 ? "Preserve needed custom content in managed Skills, then run the selected exact CLI: update." : void 0
|
|
2431
1308
|
});
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
checks.push({ status: "issue", code: "project_skill_suite", label: "project Skill suite is inspectable", message: toErrorMessage(error) });
|
|
1311
|
+
}
|
|
1312
|
+
return sourceMode;
|
|
1313
|
+
}
|
|
1314
|
+
function reportRetiredProtocol(checks) {
|
|
1315
|
+
for (const path of [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH]) {
|
|
1316
|
+
const inspection = inspectManagedFile(path, "retired Rules file", { allowMissing: true });
|
|
1317
|
+
if (inspection.issue || inspection.exists) {
|
|
1318
|
+
checks.push({
|
|
1319
|
+
status: "issue",
|
|
1320
|
+
code: "retired_rules",
|
|
1321
|
+
label: `retired Rules path: ${path}`,
|
|
1322
|
+
message: inspection.issue?.message ?? "Rules are no longer read or generated.",
|
|
1323
|
+
hint: "Preserve needed custom content, then run update. Symlinks and special files require owner review."
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
2432
1326
|
}
|
|
2433
1327
|
}
|
|
2434
1328
|
async function reportUnsupportedRules(checks, reportRuntime) {
|
|
@@ -2468,7 +1362,7 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
2468
1362
|
status: "issue",
|
|
2469
1363
|
label: "AGENTS.md is a regular managed file",
|
|
2470
1364
|
message: inspection.issue.message,
|
|
2471
|
-
hint: "
|
|
1365
|
+
hint: "Check AGENTS.md path type and permissions; preserve user content before correcting the entry. Then run: rsp update"
|
|
2472
1366
|
});
|
|
2473
1367
|
return;
|
|
2474
1368
|
}
|
|
@@ -2478,7 +1372,7 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
2478
1372
|
}
|
|
2479
1373
|
let agents;
|
|
2480
1374
|
try {
|
|
2481
|
-
agents = await
|
|
1375
|
+
agents = await readFile3("AGENTS.md", "utf-8");
|
|
2482
1376
|
} catch (error) {
|
|
2483
1377
|
reportRuntime({
|
|
2484
1378
|
code: "agents_read_failed",
|
|
@@ -2486,7 +1380,7 @@ async function checkAgents(checks, reportRuntime) {
|
|
|
2486
1380
|
path: "AGENTS.md",
|
|
2487
1381
|
message: toErrorMessage(error)
|
|
2488
1382
|
});
|
|
2489
|
-
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md could not be read", hint: "Check AGENTS.md permissions
|
|
1383
|
+
checks.push({ status: "issue", label: "AGENTS.md contains managed RSP block", message: "AGENTS.md could not be read", hint: "Check AGENTS.md permissions and filesystem access; init/update do not repair unreadable user content." });
|
|
2490
1384
|
return;
|
|
2491
1385
|
}
|
|
2492
1386
|
if (hasRspAgentsBlock(agents)) {
|
|
@@ -2537,7 +1431,7 @@ async function checkSpecsTree(checks, reportRuntime) {
|
|
|
2537
1431
|
}
|
|
2538
1432
|
}
|
|
2539
1433
|
async function checkArchiveNaming(checks) {
|
|
2540
|
-
const archivesDir =
|
|
1434
|
+
const archivesDir = join2(RSP_DIR, "archives");
|
|
2541
1435
|
const inspection = await inspectArchiveTree({ archivesDir });
|
|
2542
1436
|
if (inspection.diagnostics.length > 0) {
|
|
2543
1437
|
checks.push({
|
|
@@ -2549,7 +1443,7 @@ async function checkArchiveNaming(checks) {
|
|
|
2549
1443
|
return;
|
|
2550
1444
|
}
|
|
2551
1445
|
checks.push({ status: "ok", label: "archives use supported managed paths" });
|
|
2552
|
-
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(
|
|
1446
|
+
const invalidFiles = inspection.files.map((fp) => normalizeLogicalPath(relative2(archivesDir, fp))).filter((fp) => /^\d{4}-\d{2}-\d{2}_.+\.md$/.test(basename(fp)) === false);
|
|
2553
1447
|
if (invalidFiles.length === 0) {
|
|
2554
1448
|
checks.push({ status: "ok", label: "archived change files follow the date_name.md convention" });
|
|
2555
1449
|
return;
|
|
@@ -2558,7 +1452,7 @@ async function checkArchiveNaming(checks) {
|
|
|
2558
1452
|
status: "issue",
|
|
2559
1453
|
label: "archived change files follow the date_name.md convention",
|
|
2560
1454
|
message: `archived change files with invalid names: ${invalidFiles.join(", ")}`,
|
|
2561
|
-
hint: "Rename archive files to YYYY-MM-DD_name.md
|
|
1455
|
+
hint: "Rename archive files to YYYY-MM-DD_name.md, then rerun rsp doctor"
|
|
2562
1456
|
});
|
|
2563
1457
|
}
|
|
2564
1458
|
async function checkConfigSemantics(checks, reportRuntime) {
|
|
@@ -2605,7 +1499,7 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
|
|
|
2605
1499
|
status: "issue",
|
|
2606
1500
|
label: `${decisionRecordsPath} is the authoritative Decision Record directory`,
|
|
2607
1501
|
message: filesystemError,
|
|
2608
|
-
hint: "
|
|
1502
|
+
hint: "Correct decisions.path or its filesystem entry; it must resolve to a project-local directory. Preserve existing user content."
|
|
2609
1503
|
});
|
|
2610
1504
|
return;
|
|
2611
1505
|
}
|
|
@@ -2616,10 +1510,12 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
|
|
|
2616
1510
|
checks,
|
|
2617
1511
|
`${decisionRecordsPath} is the authoritative Decision Record directory`,
|
|
2618
1512
|
info.isDirectory(),
|
|
2619
|
-
"
|
|
1513
|
+
"Correct the path type; init/update do not replace existing user content."
|
|
2620
1514
|
);
|
|
2621
1515
|
} catch (error) {
|
|
2622
|
-
|
|
1516
|
+
const code = error.code;
|
|
1517
|
+
const missing = code === "ENOENT";
|
|
1518
|
+
if (!missing) {
|
|
2623
1519
|
reportRuntime({
|
|
2624
1520
|
code: "decision_records_path_stat_failed",
|
|
2625
1521
|
operation: "stat",
|
|
@@ -2630,14 +1526,14 @@ async function checkDecisionRecordsDirectory(checks, reportRuntime) {
|
|
|
2630
1526
|
checks.push({
|
|
2631
1527
|
status: "issue",
|
|
2632
1528
|
label: `${decisionRecordsPath} is the authoritative Decision Record directory`,
|
|
2633
|
-
message: `${decisionRecordsPath} Decision Record directory is missing
|
|
2634
|
-
hint: "
|
|
1529
|
+
message: missing ? `${decisionRecordsPath} Decision Record directory is missing` : `Unable to inspect Decision Record directory ${decisionRecordsPath}: ${toErrorMessage(error)}`,
|
|
1530
|
+
hint: missing ? "Create the directory yourself, or run rsp init to create missing scaffolding. Update does not create user directories." : code === "EACCES" || code === "EPERM" ? "Check directory and parent permissions; init/update do not repair filesystem access." : "Resolve the filesystem error or correct decisions.path, then rerun rsp doctor."
|
|
2635
1531
|
});
|
|
2636
1532
|
}
|
|
2637
1533
|
}
|
|
2638
1534
|
async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
2639
1535
|
const decisionRecordsPath = resolveDecisionRecordsPath(await loadRspConfig());
|
|
2640
|
-
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !
|
|
1536
|
+
if (decisionRecordsPath === DEFAULT_DECISION_RECORDS_PATH || !existsSync(DEFAULT_DECISION_RECORDS_PATH))
|
|
2641
1537
|
return;
|
|
2642
1538
|
let inspectionFailed = false;
|
|
2643
1539
|
const inactiveRecords = (await walkMarkdownFiles(DEFAULT_DECISION_RECORDS_PATH, {
|
|
@@ -2645,7 +1541,7 @@ async function checkInactiveDefaultDecisionRecords(checks, reportRuntime) {
|
|
|
2645
1541
|
inspectionFailed = true;
|
|
2646
1542
|
reportRuntime(diagnostic);
|
|
2647
1543
|
}
|
|
2648
|
-
})).map((path) => normalizeLogicalPath(
|
|
1544
|
+
})).map((path) => normalizeLogicalPath(relative2(DEFAULT_DECISION_RECORDS_PATH, path))).sort();
|
|
2649
1545
|
if (inspectionFailed) {
|
|
2650
1546
|
checks.push({
|
|
2651
1547
|
status: "issue",
|
|
@@ -2899,7 +1795,7 @@ function emptyCollectionSummary() {
|
|
|
2899
1795
|
}
|
|
2900
1796
|
|
|
2901
1797
|
// src/commands/ready.ts
|
|
2902
|
-
import { readFile as
|
|
1798
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
2903
1799
|
function readyError(name, error, kind) {
|
|
2904
1800
|
return { command: "ready", ok: false, change: name || null, path: null, warnings: [], runtime: [], error, kind };
|
|
2905
1801
|
}
|
|
@@ -2919,7 +1815,7 @@ async function showReady(name) {
|
|
|
2919
1815
|
const runtime = [];
|
|
2920
1816
|
let content;
|
|
2921
1817
|
try {
|
|
2922
|
-
content = await
|
|
1818
|
+
content = await readFile4(srcPath, "utf-8");
|
|
2923
1819
|
} catch {
|
|
2924
1820
|
return readyError(name, { code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, "read");
|
|
2925
1821
|
}
|
|
@@ -2937,9 +1833,10 @@ async function showReady(name) {
|
|
|
2937
1833
|
return readyError(name, { code: "invalid_config", message: `.rsp/config.yaml could not be parsed: ${toErrorMessage(error)}` }, "config");
|
|
2938
1834
|
}
|
|
2939
1835
|
const dependencyInspection = await inspectChangeDependencies();
|
|
2940
|
-
const
|
|
1836
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
1837
|
+
const readinessDetails = collectArchiveReadiness(document, {
|
|
2941
1838
|
activeBlockers: dependencyInspection.activeBlockers.get(name),
|
|
2942
|
-
documentDiagnostics: inspectChangeDocument(
|
|
1839
|
+
documentDiagnostics: inspectChangeDocument(document, {
|
|
2943
1840
|
name,
|
|
2944
1841
|
validKinds: resolveKinds(configInspection.config),
|
|
2945
1842
|
requiredSections: resolveRequiredSections(configInspection.config)
|
|
@@ -2962,7 +1859,7 @@ async function showReady(name) {
|
|
|
2962
1859
|
}
|
|
2963
1860
|
|
|
2964
1861
|
// src/commands/show.ts
|
|
2965
|
-
import { readFile as
|
|
1862
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
2966
1863
|
function showError(error, runtime = []) {
|
|
2967
1864
|
const nextActions = error.code === "no_focused_change" ? ["Run: rsp status", "Run: rsp focus <name>", "Or run: rsp create <name>"] : [];
|
|
2968
1865
|
return { command: "show", ok: false, change: null, contextPaths: [], runtime, nextActions, error };
|
|
@@ -3006,14 +1903,15 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
3006
1903
|
const srcPath = workRef.path;
|
|
3007
1904
|
let content;
|
|
3008
1905
|
try {
|
|
3009
|
-
content = await
|
|
1906
|
+
content = await readFile5(srcPath, "utf-8");
|
|
3010
1907
|
} catch {
|
|
3011
1908
|
return showError({ code: "change_read_failed", message: `unable to read .rsp/changes/${name}.md` }, runtime);
|
|
3012
1909
|
}
|
|
3013
1910
|
let kind = "\u2014";
|
|
1911
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
3014
1912
|
let issues = [];
|
|
3015
1913
|
try {
|
|
3016
|
-
const fm = parseFrontmatter(
|
|
1914
|
+
const fm = parseFrontmatter(document.markdown);
|
|
3017
1915
|
kind = fm?.kind ? String(fm.kind) : "\u2014";
|
|
3018
1916
|
issues = parseIssueRelationships(fm);
|
|
3019
1917
|
} catch (error) {
|
|
@@ -3034,7 +1932,7 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
3034
1932
|
const markerPath = focusMarkers.get(name);
|
|
3035
1933
|
let markerContent;
|
|
3036
1934
|
try {
|
|
3037
|
-
markerContent = await
|
|
1935
|
+
markerContent = await readFile5(markerPath);
|
|
3038
1936
|
} catch (error) {
|
|
3039
1937
|
reportRuntime({
|
|
3040
1938
|
code: "focus_marker_read_failed",
|
|
@@ -3060,11 +1958,10 @@ async function showChange(nameOrFocused, options = {}) {
|
|
|
3060
1958
|
recovery = capsule.recovery;
|
|
3061
1959
|
}
|
|
3062
1960
|
const dependencyInspection = await inspectChangeDependencies();
|
|
3063
|
-
const blockers = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(
|
|
3064
|
-
const scenarios = parseScenarios(
|
|
3065
|
-
const readinessDetails = collectArchiveReadiness(
|
|
3066
|
-
const
|
|
3067
|
-
const taskCheckboxes = countCheckboxes(getDocumentSectionBody(document, "tasks"));
|
|
1961
|
+
const blockers = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(document);
|
|
1962
|
+
const scenarios = parseScenarios(document.markdown);
|
|
1963
|
+
const readinessDetails = collectArchiveReadiness(document, { activeBlockers: blockers });
|
|
1964
|
+
const taskCheckboxes = countCheckboxes(getDocumentSectionMarkdown(document, "tasks"));
|
|
3068
1965
|
const progress = {
|
|
3069
1966
|
done: taskCheckboxes.done + readinessDetails.verifyCriticality.required.done,
|
|
3070
1967
|
total: taskCheckboxes.total + readinessDetails.verifyCriticality.required.total
|
|
@@ -3221,13 +2118,13 @@ function publicQuery(query) {
|
|
|
3221
2118
|
}
|
|
3222
2119
|
|
|
3223
2120
|
// src/status/derive.ts
|
|
3224
|
-
function deriveStatusView(
|
|
2121
|
+
function deriveStatusView(snapshot, options = {}) {
|
|
3225
2122
|
const query = {
|
|
3226
2123
|
focused: Boolean(options.focused),
|
|
3227
2124
|
blocked: Boolean(options.blocked),
|
|
3228
2125
|
stale: typeof options.stale === "number" ? options.stale : null
|
|
3229
2126
|
};
|
|
3230
|
-
const records =
|
|
2127
|
+
const records = snapshot.records.filter(({ output: record }) => {
|
|
3231
2128
|
if (query.focused && !record.isFocused)
|
|
3232
2129
|
return false;
|
|
3233
2130
|
if (query.blocked && !record.isBlocked)
|
|
@@ -3237,26 +2134,26 @@ function deriveStatusView(snapshot2, options = {}) {
|
|
|
3237
2134
|
return true;
|
|
3238
2135
|
});
|
|
3239
2136
|
const filtered = query.focused || query.blocked || query.stale !== null;
|
|
3240
|
-
const ok =
|
|
3241
|
-
const nextActions = ok ? buildStatusNextActions(
|
|
2137
|
+
const ok = snapshot.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
|
|
2138
|
+
const nextActions = ok ? buildStatusNextActions(snapshot.focused.length, snapshot.records.map((record) => record.output), snapshot.groups, filtered) : ["Run: rsp doctor"];
|
|
3242
2139
|
return {
|
|
3243
|
-
|
|
3244
|
-
language:
|
|
2140
|
+
workflow: snapshot.workflow,
|
|
2141
|
+
language: snapshot.language,
|
|
3245
2142
|
query,
|
|
3246
|
-
focused:
|
|
2143
|
+
focused: snapshot.focused,
|
|
3247
2144
|
records,
|
|
3248
|
-
groups:
|
|
3249
|
-
plan: filterDependencyPlan(
|
|
2145
|
+
groups: snapshot.groups,
|
|
2146
|
+
plan: filterDependencyPlan(snapshot.plan, new Set(records.map((record) => record.output.name))),
|
|
3250
2147
|
summary: {
|
|
3251
2148
|
total: records.length,
|
|
3252
2149
|
focused: records.filter((record) => record.output.isFocused).length,
|
|
3253
2150
|
blocked: records.filter((record) => record.output.isBlocked).length
|
|
3254
2151
|
},
|
|
3255
2152
|
nextActions,
|
|
3256
|
-
archiveTrend:
|
|
3257
|
-
diagnostics:
|
|
3258
|
-
runtime:
|
|
3259
|
-
hasExecutableChanges:
|
|
2153
|
+
archiveTrend: snapshot.archiveTrend,
|
|
2154
|
+
diagnostics: snapshot.diagnostics,
|
|
2155
|
+
runtime: snapshot.runtime,
|
|
2156
|
+
hasExecutableChanges: snapshot.records.length > 0,
|
|
3260
2157
|
ok
|
|
3261
2158
|
};
|
|
3262
2159
|
}
|
|
@@ -3304,13 +2201,13 @@ function recommendOpenChange(records, groups) {
|
|
|
3304
2201
|
}
|
|
3305
2202
|
function filterDependencyPlan(plan, names) {
|
|
3306
2203
|
const included = new Set(names);
|
|
3307
|
-
let
|
|
3308
|
-
while (
|
|
3309
|
-
|
|
2204
|
+
let changed = true;
|
|
2205
|
+
while (changed) {
|
|
2206
|
+
changed = false;
|
|
3310
2207
|
for (const edge of plan.edges) {
|
|
3311
2208
|
if (included.has(edge.change) && !included.has(edge.requires)) {
|
|
3312
2209
|
included.add(edge.requires);
|
|
3313
|
-
|
|
2210
|
+
changed = true;
|
|
3314
2211
|
}
|
|
3315
2212
|
}
|
|
3316
2213
|
}
|
|
@@ -3326,46 +2223,49 @@ function filterDependencyPlan(plan, names) {
|
|
|
3326
2223
|
|
|
3327
2224
|
// src/commands/status.ts
|
|
3328
2225
|
async function showStatus(options = {}) {
|
|
3329
|
-
const
|
|
3330
|
-
return deriveStatusView(
|
|
2226
|
+
const snapshot = await inspectProjectStatus();
|
|
2227
|
+
return deriveStatusView(snapshot, options);
|
|
3331
2228
|
}
|
|
3332
2229
|
|
|
3333
2230
|
// src/status/v3-json.ts
|
|
3334
|
-
function
|
|
3335
|
-
return
|
|
2231
|
+
function hasActiveFilters(query) {
|
|
2232
|
+
return query.focused || query.blocked || query.stale !== null;
|
|
2233
|
+
}
|
|
2234
|
+
function toStatusJson(view, options = {}) {
|
|
2235
|
+
const output = {
|
|
3336
2236
|
command: "status",
|
|
3337
2237
|
ok: view.ok,
|
|
3338
|
-
manage: view.manage,
|
|
3339
|
-
language: view.language,
|
|
3340
|
-
filters: view.query,
|
|
3341
2238
|
focused: view.focused,
|
|
3342
2239
|
records: view.records.map((record) => record.output),
|
|
3343
2240
|
groups: view.groups,
|
|
3344
|
-
plan:
|
|
2241
|
+
plan: {
|
|
2242
|
+
nodes: view.plan.nodes,
|
|
2243
|
+
edges: view.plan.edges,
|
|
2244
|
+
blocked: view.plan.blocked,
|
|
2245
|
+
waves: view.plan.waves
|
|
2246
|
+
},
|
|
3345
2247
|
summary: view.summary,
|
|
3346
|
-
|
|
3347
|
-
archiveTrend: view.archiveTrend,
|
|
3348
|
-
diagnostics: view.diagnostics,
|
|
3349
|
-
runtime: view.runtime
|
|
2248
|
+
diagnostics: view.diagnostics
|
|
3350
2249
|
};
|
|
2250
|
+
if (options.verbose || hasActiveFilters(view.query)) {
|
|
2251
|
+
output.filters = view.query;
|
|
2252
|
+
}
|
|
2253
|
+
if (options.verbose) {
|
|
2254
|
+
output.nextActions = view.nextActions;
|
|
2255
|
+
output.archiveTrend = view.archiveTrend;
|
|
2256
|
+
output.runtime = view.runtime;
|
|
2257
|
+
}
|
|
2258
|
+
return output;
|
|
3351
2259
|
}
|
|
3352
2260
|
function toStatusJsonError(error, options) {
|
|
3353
|
-
|
|
2261
|
+
const output = {
|
|
3354
2262
|
command: "status",
|
|
3355
2263
|
ok: false,
|
|
3356
|
-
manage: { activation: "explicit", closeout: "manual" },
|
|
3357
|
-
language: { artifacts: null, commit: null },
|
|
3358
|
-
filters: {
|
|
3359
|
-
focused: options.focused,
|
|
3360
|
-
blocked: options.blocked,
|
|
3361
|
-
stale: null
|
|
3362
|
-
},
|
|
3363
2264
|
focused: [],
|
|
3364
2265
|
records: [],
|
|
3365
2266
|
groups: [],
|
|
3366
2267
|
plan: {
|
|
3367
2268
|
nodes: [],
|
|
3368
|
-
ready: [],
|
|
3369
2269
|
edges: [],
|
|
3370
2270
|
blocked: [],
|
|
3371
2271
|
waves: []
|
|
@@ -3375,12 +2275,22 @@ function toStatusJsonError(error, options) {
|
|
|
3375
2275
|
focused: 0,
|
|
3376
2276
|
blocked: 0
|
|
3377
2277
|
},
|
|
3378
|
-
archiveTrend: [],
|
|
3379
|
-
nextActions: [],
|
|
3380
2278
|
diagnostics: [],
|
|
3381
|
-
runtime: [],
|
|
3382
2279
|
error
|
|
3383
2280
|
};
|
|
2281
|
+
if (options.verbose || options.focused || options.blocked) {
|
|
2282
|
+
output.filters = {
|
|
2283
|
+
focused: options.focused,
|
|
2284
|
+
blocked: options.blocked,
|
|
2285
|
+
stale: null
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
if (options.verbose) {
|
|
2289
|
+
output.archiveTrend = [];
|
|
2290
|
+
output.nextActions = [];
|
|
2291
|
+
output.runtime = [];
|
|
2292
|
+
}
|
|
2293
|
+
return output;
|
|
3384
2294
|
}
|
|
3385
2295
|
|
|
3386
2296
|
// src/cli/adapter.ts
|
|
@@ -3393,9 +2303,6 @@ async function executeCliCommand(adapter, args) {
|
|
|
3393
2303
|
return result;
|
|
3394
2304
|
}
|
|
3395
2305
|
|
|
3396
|
-
// src/cli/presenters/inspection.ts
|
|
3397
|
-
import { existsSync as existsSync3 } from "fs";
|
|
3398
|
-
|
|
3399
2306
|
// src/status/plain.ts
|
|
3400
2307
|
var COL_CHANGE = 32;
|
|
3401
2308
|
var COL_KIND = 10;
|
|
@@ -3409,10 +2316,6 @@ function printStatusPlain(view, options = {}) {
|
|
|
3409
2316
|
console.log();
|
|
3410
2317
|
console.log(` ${pc.bold("RSP status")}`);
|
|
3411
2318
|
console.log();
|
|
3412
|
-
if (options.verbose)
|
|
3413
|
-
console.log(` ${pc.bold("Manage:")} activation ${view.manage.activation} \xB7 closeout ${view.manage.closeout}`);
|
|
3414
|
-
if (options.verbose && (view.language.artifacts !== null || view.language.commit !== null))
|
|
3415
|
-
console.log(` ${pc.bold("Language:")} artifacts ${view.language.artifacts ?? "unset"} \xB7 commit ${view.language.commit ?? "unset"}`);
|
|
3416
2319
|
if (options.verbose)
|
|
3417
2320
|
console.log();
|
|
3418
2321
|
for (const diagnostic of view.diagnostics) {
|
|
@@ -3542,6 +2445,25 @@ function printArchiveTrend(trend) {
|
|
|
3542
2445
|
}
|
|
3543
2446
|
|
|
3544
2447
|
// src/cli/presenters/inspection.ts
|
|
2448
|
+
function presentConfig(result, options) {
|
|
2449
|
+
if (options.json) {
|
|
2450
|
+
emitJson(result.ok ? result.summary : result, options);
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2453
|
+
if (!result.ok) {
|
|
2454
|
+
console.error(` ${pc.red("Error:")} ${result.diagnostics.map((diagnostic) => diagnostic.message).join("; ")}`);
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
console.log();
|
|
2458
|
+
console.log(` ${pc.bold("RSP config")}`);
|
|
2459
|
+
console.log(` ${pc.dim("Path:")} ${result.path}`);
|
|
2460
|
+
console.log(` ${pc.dim("Valid:")} ${pc.green("yes")}`);
|
|
2461
|
+
console.log(` ${pc.dim("Kinds:")} ${result.summary.kinds.join(", ")}`);
|
|
2462
|
+
console.log(` ${pc.dim("Decision Records:")} ${result.summary.decisions.path}`);
|
|
2463
|
+
console.log(` ${pc.dim("Workflow:")} delegation ${result.summary.workflow.delegation} \xB7 closeout ${result.summary.workflow.closeout}`);
|
|
2464
|
+
console.log(` ${pc.dim("Language:")} artifacts ${result.summary.language.artifacts ?? "unset"} \xB7 commit ${result.summary.language.commit ?? "unset"}`);
|
|
2465
|
+
console.log();
|
|
2466
|
+
}
|
|
3545
2467
|
function presentStatus(result, options) {
|
|
3546
2468
|
if (result.kind === "error") {
|
|
3547
2469
|
if (options.json)
|
|
@@ -3553,7 +2475,7 @@ function presentStatus(result, options) {
|
|
|
3553
2475
|
if (options.verbose && !options.json)
|
|
3554
2476
|
printStatusRuntimeDiagnostics(result.view.runtime);
|
|
3555
2477
|
if (options.json)
|
|
3556
|
-
emitJson(toStatusJson(result.view), options);
|
|
2478
|
+
emitJson(toStatusJson(result.view, { verbose: options.verbose }), options);
|
|
3557
2479
|
else
|
|
3558
2480
|
printStatusPlain(result.view, { verbose: options.verbose });
|
|
3559
2481
|
}
|
|
@@ -3694,13 +2616,6 @@ function presentDoctor(result, options) {
|
|
|
3694
2616
|
console.log();
|
|
3695
2617
|
console.log(` ${pc.bold("RSP doctor")}`);
|
|
3696
2618
|
console.log();
|
|
3697
|
-
if (result.fixed.length > 0) {
|
|
3698
|
-
console.log(` ${pc.green("Fixed:")} ${result.fixed.join(", ")}`);
|
|
3699
|
-
console.log();
|
|
3700
|
-
} else if (options.fix && result.checks.every((check) => check.message?.startsWith("safe deterministic repair could not run:") !== true) && existsSync3(RSP_DIR)) {
|
|
3701
|
-
console.log(` ${pc.dim("No safe fixes needed.")}`);
|
|
3702
|
-
console.log();
|
|
3703
|
-
}
|
|
3704
2619
|
for (const check of result.checks)
|
|
3705
2620
|
printDoctorCheck(check);
|
|
3706
2621
|
if (result.summary.issues === 0)
|
|
@@ -3918,7 +2833,23 @@ function formatArchiveReady(value) {
|
|
|
3918
2833
|
}
|
|
3919
2834
|
|
|
3920
2835
|
// src/cli/commands/inspection.ts
|
|
3921
|
-
var
|
|
2836
|
+
var configCommand = defineCommand2({
|
|
2837
|
+
meta: {
|
|
2838
|
+
name: "config",
|
|
2839
|
+
description: "Inspect effective project configuration without scanning work state"
|
|
2840
|
+
},
|
|
2841
|
+
args: {
|
|
2842
|
+
...compactJsonArgs
|
|
2843
|
+
},
|
|
2844
|
+
async run({ args }) {
|
|
2845
|
+
await executeCliCommand({
|
|
2846
|
+
execute: () => showConfig(),
|
|
2847
|
+
present: (result) => presentConfig(result, args),
|
|
2848
|
+
exitCode: (result) => result.ok ? void 0 : 1
|
|
2849
|
+
}, args);
|
|
2850
|
+
}
|
|
2851
|
+
});
|
|
2852
|
+
var specsCommand = defineCommand2({
|
|
3922
2853
|
meta: {
|
|
3923
2854
|
name: "specs",
|
|
3924
2855
|
description: "Inspect current Specs and Decision Records or run bounded literal search"
|
|
@@ -3957,7 +2888,7 @@ var specsCommand = defineCommand({
|
|
|
3957
2888
|
}, args);
|
|
3958
2889
|
}
|
|
3959
2890
|
});
|
|
3960
|
-
var statusCommand =
|
|
2891
|
+
var statusCommand = defineCommand2({
|
|
3961
2892
|
meta: {
|
|
3962
2893
|
name: "status",
|
|
3963
2894
|
description: "Show project status summary with current focus information"
|
|
@@ -3980,7 +2911,7 @@ var statusCommand = defineCommand({
|
|
|
3980
2911
|
...compactJsonArgs,
|
|
3981
2912
|
verbose: {
|
|
3982
2913
|
type: "boolean",
|
|
3983
|
-
description: "
|
|
2914
|
+
description: "Show auxiliary status details and runtime diagnostics",
|
|
3984
2915
|
default: false
|
|
3985
2916
|
}
|
|
3986
2917
|
},
|
|
@@ -3993,7 +2924,8 @@ var statusCommand = defineCommand({
|
|
|
3993
2924
|
kind: "error",
|
|
3994
2925
|
output: toStatusJsonError({ code: "invalid_stale_filter", message: "--stale must be a non-negative integer number of days" }, {
|
|
3995
2926
|
focused: Boolean(args.focused),
|
|
3996
|
-
blocked: Boolean(args.blocked)
|
|
2927
|
+
blocked: Boolean(args.blocked),
|
|
2928
|
+
verbose: Boolean(args.verbose)
|
|
3997
2929
|
})
|
|
3998
2930
|
};
|
|
3999
2931
|
}
|
|
@@ -4004,7 +2936,7 @@ var statusCommand = defineCommand({
|
|
|
4004
2936
|
}, args);
|
|
4005
2937
|
}
|
|
4006
2938
|
});
|
|
4007
|
-
var checkCommand =
|
|
2939
|
+
var checkCommand = defineCommand2({
|
|
4008
2940
|
meta: {
|
|
4009
2941
|
name: "check",
|
|
4010
2942
|
description: "Validate changes (frontmatter, sections, template text, deltas, scenarios)"
|
|
@@ -4030,33 +2962,35 @@ var checkCommand = defineCommand({
|
|
|
4030
2962
|
}, args);
|
|
4031
2963
|
}
|
|
4032
2964
|
});
|
|
4033
|
-
var doctorCommand =
|
|
2965
|
+
var doctorCommand = defineCommand2({
|
|
4034
2966
|
meta: {
|
|
4035
2967
|
name: "doctor",
|
|
4036
2968
|
description: "Check RSP setup health and common integration issues"
|
|
4037
2969
|
},
|
|
4038
2970
|
args: {
|
|
4039
2971
|
...compactJsonArgs,
|
|
4040
|
-
verbose: {
|
|
2972
|
+
"verbose": {
|
|
4041
2973
|
type: "boolean",
|
|
4042
2974
|
description: "Print runtime diagnostics for suppressed I/O issues",
|
|
4043
2975
|
default: false
|
|
4044
2976
|
},
|
|
4045
|
-
|
|
2977
|
+
"check-updates": {
|
|
4046
2978
|
type: "boolean",
|
|
4047
|
-
description: "
|
|
2979
|
+
description: "Explicitly check stable registry metadata without upgrading; timeout is non-blocking",
|
|
4048
2980
|
default: false
|
|
4049
2981
|
}
|
|
4050
2982
|
},
|
|
4051
|
-
async run({ args }) {
|
|
2983
|
+
async run({ args, rawArgs }) {
|
|
2984
|
+
if (rawArgs.some((arg) => arg === "--fix" || arg.startsWith("--fix=") || arg === "--no-fix"))
|
|
2985
|
+
throw new Error("doctor is read-only; use update to reconcile managed assets");
|
|
4052
2986
|
await executeCliCommand({
|
|
4053
|
-
execute: () => runDoctor({
|
|
2987
|
+
execute: () => runDoctor({ checkUpdates: Boolean(args["check-updates"]) }),
|
|
4054
2988
|
present: (result) => presentDoctor(result, args),
|
|
4055
2989
|
exitCode: (result) => result.ok ? void 0 : 1
|
|
4056
2990
|
}, args);
|
|
4057
2991
|
}
|
|
4058
2992
|
});
|
|
4059
|
-
var readyCommand =
|
|
2993
|
+
var readyCommand = defineCommand2({
|
|
4060
2994
|
meta: {
|
|
4061
2995
|
name: "ready",
|
|
4062
2996
|
description: "Preview archive readiness for a change without moving it"
|
|
@@ -4082,7 +3016,7 @@ var readyCommand = defineCommand({
|
|
|
4082
3016
|
}, args);
|
|
4083
3017
|
}
|
|
4084
3018
|
});
|
|
4085
|
-
var showCommand =
|
|
3019
|
+
var showCommand = defineCommand2({
|
|
4086
3020
|
meta: {
|
|
4087
3021
|
name: "show",
|
|
4088
3022
|
description: "Show change context with readiness signals and relevant paths"
|
|
@@ -4113,7 +3047,7 @@ var showCommand = defineCommand({
|
|
|
4113
3047
|
}, args);
|
|
4114
3048
|
}
|
|
4115
3049
|
});
|
|
4116
|
-
var historyCommand =
|
|
3050
|
+
var historyCommand = defineCommand2({
|
|
4117
3051
|
meta: {
|
|
4118
3052
|
name: "history",
|
|
4119
3053
|
description: "Query bounded archived Change summaries or one exact archived WorkRef"
|
|
@@ -4168,6 +3102,7 @@ var historyCommand = defineCommand({
|
|
|
4168
3102
|
}
|
|
4169
3103
|
});
|
|
4170
3104
|
var inspectionCommands = {
|
|
3105
|
+
config: configCommand,
|
|
4171
3106
|
specs: specsCommand,
|
|
4172
3107
|
ready: readyCommand,
|
|
4173
3108
|
show: showCommand,
|
|
@@ -4178,12 +3113,74 @@ var inspectionCommands = {
|
|
|
4178
3113
|
};
|
|
4179
3114
|
|
|
4180
3115
|
// src/cli/commands/setup.ts
|
|
4181
|
-
import { defineCommand as
|
|
3116
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
3117
|
+
|
|
3118
|
+
// src/commands/add-spec.ts
|
|
3119
|
+
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
3120
|
+
import { join as join3 } from "path";
|
|
3121
|
+
|
|
3122
|
+
// src/core/lock.ts
|
|
3123
|
+
import { mkdir as mkdir2, readFile as readFile6, unlink, writeFile } from "fs/promises";
|
|
3124
|
+
async function withRspLock(operation, fn) {
|
|
3125
|
+
requireManagedDirectory(RSP_DIR, "RSP root", { allowMissing: true });
|
|
3126
|
+
await mkdir2(RSP_DIR, { recursive: true });
|
|
3127
|
+
try {
|
|
3128
|
+
await writeFile(LOCK_PATH, `${process.pid}
|
|
3129
|
+
${operation}
|
|
3130
|
+
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
3131
|
+
} catch (err) {
|
|
3132
|
+
if (err.code === "EEXIST")
|
|
3133
|
+
await handleStaleLock(operation);
|
|
3134
|
+
else
|
|
3135
|
+
throw err;
|
|
3136
|
+
}
|
|
3137
|
+
try {
|
|
3138
|
+
return await fn();
|
|
3139
|
+
} finally {
|
|
3140
|
+
try {
|
|
3141
|
+
await unlink(LOCK_PATH);
|
|
3142
|
+
} catch {
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
async function handleStaleLock(operation) {
|
|
3147
|
+
try {
|
|
3148
|
+
const content = await readFile6(LOCK_PATH, "utf-8");
|
|
3149
|
+
const lines = content.trim().split("\n");
|
|
3150
|
+
const pid = Number(lines[0]);
|
|
3151
|
+
if (pid && Number.isFinite(pid)) {
|
|
3152
|
+
try {
|
|
3153
|
+
process.kill(pid, 0);
|
|
3154
|
+
const op = lines[1] || "unknown";
|
|
3155
|
+
const ts = lines[2] || "unknown";
|
|
3156
|
+
throw new Error(`RSP locked by pid ${pid} (${op}) since ${ts}`);
|
|
3157
|
+
} catch (e) {
|
|
3158
|
+
if (e instanceof Error && e.message.startsWith("RSP locked"))
|
|
3159
|
+
throw e;
|
|
3160
|
+
const code = e.code;
|
|
3161
|
+
if (code === "ESRCH") {
|
|
3162
|
+
await unlink(LOCK_PATH);
|
|
3163
|
+
await writeFile(LOCK_PATH, `${process.pid}
|
|
3164
|
+
${operation}
|
|
3165
|
+
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
throw e;
|
|
3169
|
+
}
|
|
3170
|
+
} else {
|
|
3171
|
+
await unlink(LOCK_PATH);
|
|
3172
|
+
await writeFile(LOCK_PATH, `${process.pid}
|
|
3173
|
+
${operation}
|
|
3174
|
+
${(/* @__PURE__ */ new Date()).toISOString()}`, { flag: "wx" });
|
|
3175
|
+
}
|
|
3176
|
+
} catch (e) {
|
|
3177
|
+
if (e instanceof Error && e.message.startsWith("RSP locked"))
|
|
3178
|
+
throw e;
|
|
3179
|
+
throw new Error(`RSP is locked. Remove ${LOCK_PATH} manually if no other process is running.`);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
4182
3182
|
|
|
4183
3183
|
// src/commands/add-spec.ts
|
|
4184
|
-
import { existsSync as existsSync4 } from "fs";
|
|
4185
|
-
import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
|
|
4186
|
-
import { join as join5 } from "path";
|
|
4187
3184
|
async function addSpec(name, projectName = "project") {
|
|
4188
3185
|
if (!name || !isValidSpecName(name)) {
|
|
4189
3186
|
return { ok: false, message: "spec name must be kebab-case with optional subdirectory" };
|
|
@@ -4194,13 +3191,9 @@ async function addSpec(name, projectName = "project") {
|
|
|
4194
3191
|
if (["index", "00-index"].includes(name.split("/").at(-1) || "")) {
|
|
4195
3192
|
return { ok: false, message: 'spec name "index" is reserved for Specs migration compatibility' };
|
|
4196
3193
|
}
|
|
4197
|
-
const
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
const initialized = existsSync4(RSP_DIR);
|
|
4201
|
-
return { ok: false, message: `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
4202
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}` };
|
|
4203
|
-
}
|
|
3194
|
+
const initialization = inspectRspInitialization();
|
|
3195
|
+
if (initialization)
|
|
3196
|
+
return { ok: false, message: initialization };
|
|
4204
3197
|
try {
|
|
4205
3198
|
const configInspection = await inspectRspConfig();
|
|
4206
3199
|
if (configInspection.issues.length > 0) {
|
|
@@ -4210,14 +3203,14 @@ async function addSpec(name, projectName = "project") {
|
|
|
4210
3203
|
return { ok: false, message: `.rsp/config.yaml could not be parsed: ${toErrorMessage(error)}` };
|
|
4211
3204
|
}
|
|
4212
3205
|
return withRspLock("add-spec", async () => {
|
|
4213
|
-
const specsDir =
|
|
3206
|
+
const specsDir = join3(RSP_DIR, "specs");
|
|
4214
3207
|
const segments = name.split("/");
|
|
4215
3208
|
const parent = resolveManagedDirectoryChain(specsDir, segments.slice(0, -1), "spec path");
|
|
4216
|
-
const path =
|
|
3209
|
+
const path = join3(parent, `${segments.at(-1)}.md`);
|
|
4217
3210
|
const inspection = await inspectManagedFileTree(specsDir, "Specs");
|
|
4218
3211
|
if (inspection.issues.length > 0)
|
|
4219
3212
|
throw inspection.issues[0];
|
|
4220
|
-
await
|
|
3213
|
+
await mkdir3(parent, { recursive: true });
|
|
4221
3214
|
const resolvedProjectName = projectName === "project" ? await detectProjectName() : projectName;
|
|
4222
3215
|
const content = name === "design" ? generateDesignContent(resolvedProjectName) : generateSpecContent(name);
|
|
4223
3216
|
try {
|
|
@@ -4232,114 +3225,245 @@ async function addSpec(name, projectName = "project") {
|
|
|
4232
3225
|
}
|
|
4233
3226
|
|
|
4234
3227
|
// src/commands/init.ts
|
|
4235
|
-
import { existsSync as
|
|
4236
|
-
import { mkdir as
|
|
4237
|
-
import { join as
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
}
|
|
4241
|
-
|
|
4242
|
-
|
|
3228
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3229
|
+
import { mkdir as mkdir4, readdir as readdir3, readFile as readFile8 } from "fs/promises";
|
|
3230
|
+
import { join as join5 } from "path";
|
|
3231
|
+
|
|
3232
|
+
// src/core/setup-files.ts
|
|
3233
|
+
import { constants } from "fs";
|
|
3234
|
+
import { access, chmod as chmod2, link, mkdtemp as mkdtemp2, readFile as readFile7, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
3235
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
3236
|
+
async function updateSetupFile(path, render) {
|
|
3237
|
+
const exists = requireManagedFile(path, "setup file", { allowMissing: true });
|
|
3238
|
+
const current = exists ? await readFile7(path, "utf8") : null;
|
|
3239
|
+
const next = render(current);
|
|
3240
|
+
if (next === current)
|
|
3241
|
+
return false;
|
|
3242
|
+
if (exists)
|
|
3243
|
+
await access(path, constants.W_OK);
|
|
3244
|
+
const temporary = await mkdtemp2(join4(dirname2(path), ".rsp-write-"));
|
|
3245
|
+
const prepared = join4(temporary, "next");
|
|
3246
|
+
let failure2;
|
|
3247
|
+
try {
|
|
3248
|
+
await writeFile3(prepared, next, { flag: "wx" });
|
|
3249
|
+
if (exists)
|
|
3250
|
+
await chmod2(prepared, (await stat2(path)).mode & 511);
|
|
3251
|
+
const stillExists = requireManagedFile(path, "setup file", { allowMissing: true });
|
|
3252
|
+
if (stillExists !== exists || stillExists && await readFile7(path, "utf8") !== current)
|
|
3253
|
+
throw new Error(`Setup file changed during update: ${path}`);
|
|
3254
|
+
if (exists)
|
|
3255
|
+
await rename2(prepared, path);
|
|
3256
|
+
else
|
|
3257
|
+
await link(prepared, path);
|
|
3258
|
+
} catch (error) {
|
|
3259
|
+
failure2 = error instanceof Error ? error : new Error(String(error));
|
|
3260
|
+
}
|
|
3261
|
+
try {
|
|
3262
|
+
await rm2(temporary, { recursive: true });
|
|
3263
|
+
} catch (error) {
|
|
3264
|
+
failure2 = new Error(`Temporary content cleanup failed: ${temporary}`, { cause: failure2 ?? error });
|
|
3265
|
+
}
|
|
3266
|
+
if (failure2)
|
|
3267
|
+
throw failure2;
|
|
3268
|
+
return true;
|
|
4243
3269
|
}
|
|
3270
|
+
|
|
3271
|
+
// src/commands/init.ts
|
|
4244
3272
|
async function initProject(args = {}) {
|
|
4245
|
-
const isNew = !
|
|
3273
|
+
const isNew = !existsSync2(RSP_DIR);
|
|
4246
3274
|
const projectName = await detectProjectName();
|
|
4247
3275
|
const mode = args.agentsMode ?? "managed";
|
|
4248
|
-
if (mode !== "managed" && mode !== "print")
|
|
4249
|
-
return { ok: false, message:
|
|
4250
|
-
}
|
|
4251
|
-
const agentsPath = "AGENTS.md";
|
|
4252
|
-
const initialAgentsInspection = inspectManagedFile(agentsPath, "AGENTS.md", { allowMissing: true });
|
|
4253
|
-
if (initialAgentsInspection.issue)
|
|
4254
|
-
throw initialAgentsInspection.issue;
|
|
3276
|
+
if (mode !== "managed" && mode !== "print")
|
|
3277
|
+
return { ok: false, message: "unsupported --agents-mode; use managed or print" };
|
|
4255
3278
|
return withRspLock("init", async () => {
|
|
4256
|
-
const
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
if (
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
3279
|
+
const runtime = await selectProjectRuntime();
|
|
3280
|
+
const { version } = runtime;
|
|
3281
|
+
const retiredRules = [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH].map((path) => requireManagedFile(path, "retired Rules", { allowMissing: true })).some(Boolean);
|
|
3282
|
+
if (retiredRules)
|
|
3283
|
+
return { ok: false, message: "Retired RSP assets require migration; use the selected target CLI: rsp update" };
|
|
3284
|
+
requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true });
|
|
3285
|
+
clearConfigCache();
|
|
3286
|
+
const inspection = await inspectRspConfig();
|
|
3287
|
+
if (inspection.issues.length)
|
|
3288
|
+
throw new Error(inspection.issues.join("; "));
|
|
3289
|
+
const decisionRecordsPath = resolveDecisionRecordsPath(inspection.config);
|
|
3290
|
+
const pathIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath);
|
|
3291
|
+
if (pathIssue)
|
|
3292
|
+
throw new Error(pathIssue);
|
|
3293
|
+
const dirs = [join5(RSP_DIR, "specs"), CHANGES_DIR, FOCUS_DIR, join5(RSP_DIR, "archives"), decisionRecordsPath];
|
|
3294
|
+
for (const path of dirs)
|
|
3295
|
+
resolveManagedDirectoryChain(process.cwd(), path.split("/"), "project directory");
|
|
3296
|
+
const completed = [];
|
|
3297
|
+
async function ensureFile(path, content) {
|
|
3298
|
+
if (await updateSetupFile(path, (current) => current ?? content))
|
|
3299
|
+
completed.push(`created ${path}`);
|
|
4268
3300
|
}
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
{
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
await mkdir5(directory.path, { recursive: true });
|
|
4279
|
-
let created = false;
|
|
4280
|
-
const bundledRules = await readFile8(join6(PKG_ROOT, "rules", "rsp-rules.md"), "utf-8");
|
|
4281
|
-
created = await ensureFile(RSP_RULES_PATH, bundledRules) || created;
|
|
4282
|
-
if (existsSync5(CONFIG_PATH)) {
|
|
4283
|
-
const existing = await readFile8(CONFIG_PATH, "utf-8");
|
|
4284
|
-
const reconciled = reconcileRspConfigDefaults(existing);
|
|
4285
|
-
if (reconciled.changed) {
|
|
4286
|
-
await writeManagedFile(CONFIG_PATH, reconciled.content, "config file");
|
|
4287
|
-
created = true;
|
|
3301
|
+
try {
|
|
3302
|
+
const skills = await updatePackagedSkills();
|
|
3303
|
+
completed.push(...skills.installed.map((name) => `installed ${name}`), ...skills.replaced.map((name) => `updated ${name}`), ...skills.removed.map((name) => `removed ${name}`));
|
|
3304
|
+
for (const path of dirs) {
|
|
3305
|
+
if (!requireManagedDirectory(path, "project directory", { allowMissing: true })) {
|
|
3306
|
+
await mkdir4(path, { recursive: true });
|
|
3307
|
+
requireManagedDirectory(path, "project directory");
|
|
3308
|
+
completed.push(`created ${path}`);
|
|
3309
|
+
}
|
|
4288
3310
|
}
|
|
4289
|
-
|
|
4290
|
-
|
|
3311
|
+
await ensureFile(CONFIG_PATH, generateConfigTemplate());
|
|
3312
|
+
clearConfigCache();
|
|
3313
|
+
await ensureFile(join5(RSP_DIR, "specs/design.md"), generateDesignContent(projectName));
|
|
3314
|
+
if ((await readdir3(decisionRecordsPath)).length === 0)
|
|
3315
|
+
await ensureFile(join5(decisionRecordsPath, ".gitkeep"), "");
|
|
3316
|
+
await ensureFile(join5(CHANGES_DIR, ".gitkeep"), "");
|
|
3317
|
+
await ensureFile(join5(FOCUS_DIR, ".gitkeep"), "");
|
|
3318
|
+
await ensureFile(join5(RSP_DIR, ".gitignore"), "# Transient files that should not be committed\n.lock\n");
|
|
3319
|
+
if (args.withProjectSetup) {
|
|
3320
|
+
const work = resolveWorkRef("project-setup", { executable: true });
|
|
3321
|
+
await ensureFile(work.path, generateChangeContent("project-setup"));
|
|
3322
|
+
await ensureFile(resolveFocusMarkerPath(work), "");
|
|
3323
|
+
}
|
|
3324
|
+
const title = projectName.startsWith("@") ? projectName.split("/")[1] || projectName : projectName;
|
|
3325
|
+
const agentsUpdated = await updateSetupFile("AGENTS.md", (current) => upsertRspAgentsBlock(current ?? `# ${title}
|
|
3326
|
+
|
|
3327
|
+
`, runtime).content);
|
|
3328
|
+
if (agentsUpdated)
|
|
3329
|
+
completed.push("updated AGENTS.md managed block");
|
|
3330
|
+
if (runtime.mode === "package" && await updateSetupFile(PROJECT_VERSION_PATH, () => `${version}
|
|
3331
|
+
`))
|
|
3332
|
+
completed.push(`pinned ${version}`);
|
|
3333
|
+
return { ok: true, isNew, created: completed.length > 0, decisionRecordsPath, withProjectSetup: Boolean(args.withProjectSetup), agentsUpdated, skills, managedAgents: mode === "print" ? await readFile8("AGENTS.md", "utf8") : void 0 };
|
|
3334
|
+
} catch (error) {
|
|
3335
|
+
throw new Error(`Init stopped; completed: ${completed.join(", ") || "none"}. No rollback was attempted. Fix the cause and rerun init with the same target CLI. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
4291
3336
|
}
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
// src/commands/update.ts
|
|
3341
|
+
import { existsSync as existsSync3 } from "fs";
|
|
3342
|
+
import { readFile as readFile11, unlink as unlink3 } from "fs/promises";
|
|
3343
|
+
import { join as join7 } from "path";
|
|
3344
|
+
|
|
3345
|
+
// src/commands/archive-index-migration.ts
|
|
3346
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3347
|
+
import { join as join6 } from "path";
|
|
3348
|
+
async function isLegacyArchiveIndex() {
|
|
3349
|
+
const path = join6(ARCHIVES_DIR, "INDEX.md");
|
|
3350
|
+
if (!requireManagedFile(path, "legacy archive index", { allowMissing: true }))
|
|
3351
|
+
return false;
|
|
3352
|
+
return isLegacyArchiveIndexContent(await readFile9(path, "utf-8"));
|
|
3353
|
+
}
|
|
3354
|
+
function isLegacyArchiveIndexContent(content) {
|
|
3355
|
+
try {
|
|
3356
|
+
const frontmatter = parseFrontmatter(content);
|
|
3357
|
+
return frontmatter?.kind === "generated-index" && frontmatter.index_type === "archives";
|
|
3358
|
+
} catch {
|
|
3359
|
+
return false;
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
// src/commands/specs-index-migration.ts
|
|
3364
|
+
import { readFile as readFile10, unlink as unlink2 } from "fs/promises";
|
|
3365
|
+
import { dirname as dirname3, resolve } from "path";
|
|
3366
|
+
async function inspectGeneratedSpecsIndexMigration(cwd = process.cwd()) {
|
|
3367
|
+
const inspection = await inspectSpecs({ cwd, allowMissingDecisionRecords: true });
|
|
3368
|
+
return {
|
|
3369
|
+
inspectionComplete: specsInspectionComplete(inspection),
|
|
3370
|
+
removable: inspection.generatedIndexes.filter((index) => index.classification === "safe-removal").map((index) => index.path).sort(),
|
|
3371
|
+
ownerControlled: inspection.generatedIndexes.filter((index) => index.classification === "owner-controlled").map((index) => index.path).sort(),
|
|
3372
|
+
diagnostics: inspection.diagnostics.map((diagnostic) => `${diagnostic.path}: ${diagnostic.message}`)
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
async function removeGeneratedSpecsIndex(path) {
|
|
3376
|
+
resolveManagedDirectoryChain(process.cwd(), dirname3(path).split("/"), "Specs parent");
|
|
3377
|
+
if (!requireManagedFile(path, "generated Specs index", { allowMissing: true }))
|
|
3378
|
+
return false;
|
|
3379
|
+
if (!isRecognizedGeneratedSpecsIndex(await readFile10(resolve(path), "utf8"), path))
|
|
3380
|
+
throw new Error(`Specs index is no longer generated; left untouched: ${path}`);
|
|
3381
|
+
await unlink2(path);
|
|
3382
|
+
return true;
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// src/commands/update.ts
|
|
3386
|
+
async function updateProject(options = {}) {
|
|
3387
|
+
if (!existsSync3(RSP_DIR))
|
|
3388
|
+
return { ok: false, actions: [], updated: false, events: [{ type: "not-initialized" }], migration: { inspectionComplete: false, manualMigrationRequired: false, residualEntries: [], diagnostics: [] } };
|
|
3389
|
+
const execute = async () => {
|
|
3390
|
+
const runtime = await selectProjectRuntime("update");
|
|
3391
|
+
const { version } = runtime;
|
|
4292
3392
|
clearConfigCache();
|
|
4293
|
-
const
|
|
4294
|
-
if (
|
|
4295
|
-
throw new Error(
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
const
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
}
|
|
4311
|
-
const title = toTitle(projectName);
|
|
4312
|
-
let agentsUpdated = false;
|
|
4313
|
-
if (!agentsInspection.exists) {
|
|
4314
|
-
await writeManagedFile(agentsPath, `# ${title}
|
|
3393
|
+
const config = await inspectRspConfig();
|
|
3394
|
+
if (config.issues.length)
|
|
3395
|
+
throw new Error(config.issues.join("; "));
|
|
3396
|
+
resolveManagedDirectoryChain(process.cwd(), [RSP_DIR, "rules"], "retired Rules parent");
|
|
3397
|
+
const retired = [RSP_RULES_PATH, OBSOLETE_RSP_RULES_PATH].filter((path) => requireManagedFile(path, "retired Rules", { allowMissing: true }));
|
|
3398
|
+
const rules = await inspectUnsupportedRules();
|
|
3399
|
+
const migration = { inspectionComplete: rules.diagnostics.length === 0, manualMigrationRequired: rules.entries.length > 0, residualEntries: rules.entries.map((entry) => entry.path), diagnostics: rules.diagnostics };
|
|
3400
|
+
if (!migration.inspectionComplete)
|
|
3401
|
+
return { ok: false, actions: [], updated: false, events: [{ type: "migration-diagnostics", diagnostics: rules.diagnostics }], migration };
|
|
3402
|
+
requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true });
|
|
3403
|
+
resolveManagedDirectoryChain(process.cwd(), [RSP_DIR, "archives"], "archive parent");
|
|
3404
|
+
requireManagedFile(join7(RSP_DIR, "archives/INDEX.md"), "archive index", { allowMissing: true });
|
|
3405
|
+
const plan = await inspectGeneratedSpecsIndexMigration();
|
|
3406
|
+
if (!plan.inspectionComplete || plan.ownerControlled.length)
|
|
3407
|
+
throw new Error(`Generated Specs-index migration requires owner review before update: ${[...plan.ownerControlled, ...plan.diagnostics].join("; ")}`);
|
|
3408
|
+
const projectName = await detectProjectName();
|
|
3409
|
+
const agents = requireManagedFile("AGENTS.md", "AGENTS.md", { allowMissing: true }) ? await readFile11("AGENTS.md", "utf8") : null;
|
|
3410
|
+
const refreshAgents = (current) => upsertRspAgentsBlock(current ?? `# ${projectName}
|
|
4315
3411
|
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
3412
|
+
`, runtime).content;
|
|
3413
|
+
const removeArchive = await isLegacyArchiveIndex();
|
|
3414
|
+
const actions = [];
|
|
3415
|
+
const events = [];
|
|
3416
|
+
if (existsSync3("CONTEXT-MAP.md"))
|
|
3417
|
+
events.push({ type: "legacy-context-map" });
|
|
3418
|
+
if (migration.residualEntries.length)
|
|
3419
|
+
events.push({ type: "residual-rules", entries: migration.residualEntries });
|
|
3420
|
+
const record = (message) => {
|
|
3421
|
+
actions.push(message);
|
|
3422
|
+
events.push({ type: "updated", message });
|
|
3423
|
+
};
|
|
3424
|
+
try {
|
|
3425
|
+
const skills = await updatePackagedSkills({ dryRun: options.dryRun });
|
|
3426
|
+
for (const name of skills.installed) record(`Skill installed: ${name}`);
|
|
3427
|
+
for (const name of skills.replaced) record(`Skill updated: ${name}`);
|
|
3428
|
+
for (const name of skills.removed) record(`retired Skill removed: ${name}`);
|
|
3429
|
+
if (options.dryRun) {
|
|
3430
|
+
actions.push(...retired.map((path) => `retired Rules file removed: ${path}`), ...plan.removable.map((path) => `generated Specs index removed: ${path}`));
|
|
3431
|
+
if (removeArchive)
|
|
3432
|
+
actions.push("legacy Archive Index removed");
|
|
3433
|
+
if (refreshAgents(agents) !== agents)
|
|
3434
|
+
actions.push("AGENTS.md managed block refreshed");
|
|
3435
|
+
if (runtime.mode === "package" && await readProjectVersion() !== version)
|
|
3436
|
+
actions.push(`project CLI pinned to ${version}`);
|
|
3437
|
+
return { ok: true, actions, updated: false, skills, events: [], migration };
|
|
3438
|
+
}
|
|
3439
|
+
for (const path of retired) {
|
|
3440
|
+
if (requireManagedFile(path, "retired Rules", { allowMissing: true })) {
|
|
3441
|
+
await unlink3(path);
|
|
3442
|
+
record(`retired Rules file removed: ${path}`);
|
|
3443
|
+
}
|
|
4327
3444
|
}
|
|
3445
|
+
for (const path of plan.removable) {
|
|
3446
|
+
if (await removeGeneratedSpecsIndex(path))
|
|
3447
|
+
record(`generated Specs index removed: ${path}`);
|
|
3448
|
+
}
|
|
3449
|
+
if (await isLegacyArchiveIndex()) {
|
|
3450
|
+
await unlink3(join7(RSP_DIR, "archives/INDEX.md"));
|
|
3451
|
+
record("legacy Archive Index removed");
|
|
3452
|
+
}
|
|
3453
|
+
if (await updateSetupFile("AGENTS.md", refreshAgents))
|
|
3454
|
+
record("AGENTS.md managed block refreshed");
|
|
3455
|
+
if (runtime.mode === "package" && await updateSetupFile(PROJECT_VERSION_PATH, () => `${version}
|
|
3456
|
+
`))
|
|
3457
|
+
record(`project CLI pinned to ${version}`);
|
|
3458
|
+
return { ok: true, actions, updated: actions.length > 0, skills, events, migration };
|
|
3459
|
+
} catch (error) {
|
|
3460
|
+
throw new Error(`Update stopped; completed: ${actions.join(", ") || "none"}. No rollback was attempted. Fix the cause and rerun update with the same target CLI. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
4328
3461
|
}
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
isNew,
|
|
4332
|
-
created,
|
|
4333
|
-
decisionRecordsPath,
|
|
4334
|
-
withProjectSetup: Boolean(args.withProjectSetup),
|
|
4335
|
-
agentsUpdated,
|
|
4336
|
-
managedAgents: mode === "print" ? await readFile8(agentsPath, "utf-8") : void 0
|
|
4337
|
-
};
|
|
4338
|
-
});
|
|
3462
|
+
};
|
|
3463
|
+
return options.dryRun ? execute() : withRspLock("update", execute);
|
|
4339
3464
|
}
|
|
4340
3465
|
|
|
4341
3466
|
// src/cli/presenters/setup.ts
|
|
4342
|
-
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";
|
|
4343
3467
|
function presentAddSpec(result) {
|
|
4344
3468
|
if (!result.ok) {
|
|
4345
3469
|
console.error(` ${pc.red("Error:")} ${result.message}`);
|
|
@@ -4360,11 +3484,12 @@ ${result.managedAgents}
|
|
|
4360
3484
|
if (result.isNew) {
|
|
4361
3485
|
const createdProjectSetup = result.withProjectSetup ? "\n .rsp/changes/project-setup.md\n .rsp/focus.d/project-setup" : "";
|
|
4362
3486
|
const createdAgents = result.agentsUpdated ? "\n AGENTS.md" : "";
|
|
4363
|
-
const next = result.withProjectSetup ? "fill .rsp/changes/project-setup.md" : "rsp
|
|
3487
|
+
const next = result.withProjectSetup ? "fill .rsp/changes/project-setup.md" : "rsp status";
|
|
4364
3488
|
console.log(`
|
|
4365
3489
|
${pc.green("RSP scaffolded.")}
|
|
4366
3490
|
`);
|
|
4367
|
-
console.log(` Created:
|
|
3491
|
+
console.log(` Created: complete core Skill suite
|
|
3492
|
+
.rsp/version
|
|
4368
3493
|
.rsp/specs/
|
|
4369
3494
|
${result.decisionRecordsPath}/
|
|
4370
3495
|
.rsp/changes/
|
|
@@ -4388,7 +3513,13 @@ ${result.managedAgents}
|
|
|
4388
3513
|
`);
|
|
4389
3514
|
}
|
|
4390
3515
|
}
|
|
4391
|
-
function presentUpdate(result) {
|
|
3516
|
+
function presentUpdate(result, dryRun = false) {
|
|
3517
|
+
if (dryRun && result.ok) {
|
|
3518
|
+
console.log(" Update preview (no files changed):");
|
|
3519
|
+
for (const action of result.actions)
|
|
3520
|
+
console.log(` ${action}`);
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
4392
3523
|
if (!result.ok && result.events.some((event) => event.type === "not-initialized")) {
|
|
4393
3524
|
console.error(` ${pc.red("Error:")} RSP is not initialized in this project`);
|
|
4394
3525
|
console.error(` ${pc.dim("Run: rsp init")}`);
|
|
@@ -4410,10 +3541,9 @@ function presentUpdate(result) {
|
|
|
4410
3541
|
console.log(` ${pc.dim("Move stable scoped instructions to the nearest project-owned AGENTS.md, then remove the old files.")}`);
|
|
4411
3542
|
console.log(` ${pc.dim("Run: rsp doctor")}`);
|
|
4412
3543
|
console.log();
|
|
4413
|
-
} else if (event.type === "
|
|
4414
|
-
console.log(` ${pc.
|
|
4415
|
-
|
|
4416
|
-
console.log(` ${path}`);
|
|
3544
|
+
} else if (event.type === "legacy-context-map") {
|
|
3545
|
+
console.log(` ${pc.yellow("Note:")} CONTEXT-MAP.md remains; context migration needs owner review.`);
|
|
3546
|
+
console.log(` ${pc.dim("Use Core context migration. Keep relevant legacy context until an authorized merge updates CONTEXT.md and active references; update does not migrate or delete it.")}`);
|
|
4417
3547
|
}
|
|
4418
3548
|
}
|
|
4419
3549
|
if (!result.migration.inspectionComplete)
|
|
@@ -4428,11 +3558,10 @@ function presentUpdate(result) {
|
|
|
4428
3558
|
else
|
|
4429
3559
|
console.log(` ${pc.green("Update complete.")}
|
|
4430
3560
|
`);
|
|
4431
|
-
console.log(SKILL_REFRESH_HINT);
|
|
4432
3561
|
}
|
|
4433
3562
|
|
|
4434
3563
|
// src/cli/commands/setup.ts
|
|
4435
|
-
var initCommand =
|
|
3564
|
+
var initCommand = defineCommand3({
|
|
4436
3565
|
meta: {
|
|
4437
3566
|
name: "init",
|
|
4438
3567
|
description: "Scaffold .rsp/ and ensure AGENTS.md contains the RSP entry block"
|
|
@@ -4460,7 +3589,7 @@ var initCommand = defineCommand2({
|
|
|
4460
3589
|
}, args);
|
|
4461
3590
|
}
|
|
4462
3591
|
});
|
|
4463
|
-
var addSpecCommand =
|
|
3592
|
+
var addSpecCommand = defineCommand3({
|
|
4464
3593
|
meta: {
|
|
4465
3594
|
name: "spec",
|
|
4466
3595
|
description: "Create .rsp/specs/<name>.md for direct Specs queries"
|
|
@@ -4480,7 +3609,7 @@ var addSpecCommand = defineCommand2({
|
|
|
4480
3609
|
}, args);
|
|
4481
3610
|
}
|
|
4482
3611
|
});
|
|
4483
|
-
var addCommand =
|
|
3612
|
+
var addCommand = defineCommand3({
|
|
4484
3613
|
meta: {
|
|
4485
3614
|
name: "add",
|
|
4486
3615
|
description: "Add an optional durable spec file"
|
|
@@ -4489,17 +3618,22 @@ var addCommand = defineCommand2({
|
|
|
4489
3618
|
spec: addSpecCommand
|
|
4490
3619
|
}
|
|
4491
3620
|
});
|
|
4492
|
-
var updateCommand =
|
|
3621
|
+
var updateCommand = defineCommand3({
|
|
4493
3622
|
meta: {
|
|
4494
3623
|
name: "update",
|
|
4495
|
-
description: "
|
|
3624
|
+
description: "Update the complete pinned environment and remove retired managed assets"
|
|
3625
|
+
},
|
|
3626
|
+
args: {
|
|
3627
|
+
"dry-run": { type: "boolean", description: "Preview the complete update without changing files", default: false }
|
|
4496
3628
|
},
|
|
4497
|
-
async run() {
|
|
3629
|
+
async run({ args, rawArgs }) {
|
|
3630
|
+
if (args._.length || rawArgs.some((arg) => !["--dry-run", "--dry-run=true", "--dry-run=false", "--no-dry-run"].includes(arg)))
|
|
3631
|
+
throw new Error("rsp update accepts only --dry-run; use the selected target CLI to update all managed assets");
|
|
4498
3632
|
await executeCliCommand({
|
|
4499
|
-
execute: () => updateProject(),
|
|
4500
|
-
present: presentUpdate,
|
|
3633
|
+
execute: () => updateProject({ dryRun: Boolean(args["dry-run"]) }),
|
|
3634
|
+
present: (result) => presentUpdate(result, Boolean(args["dry-run"])),
|
|
4501
3635
|
exitCode: (result) => result.migration.inspectionComplete ? void 0 : 1
|
|
4502
|
-
},
|
|
3636
|
+
}, args);
|
|
4503
3637
|
}
|
|
4504
3638
|
});
|
|
4505
3639
|
var setupCommands = {
|
|
@@ -4509,50 +3643,27 @@ var setupCommands = {
|
|
|
4509
3643
|
};
|
|
4510
3644
|
|
|
4511
3645
|
// src/cli/commands/skills.ts
|
|
4512
|
-
import { defineCommand as
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
"name": {
|
|
4520
|
-
type: "positional",
|
|
4521
|
-
description: "Exact packaged Skill name (default: lifecycle suite)",
|
|
4522
|
-
required: false
|
|
4523
|
-
},
|
|
4524
|
-
"dry-run": {
|
|
4525
|
-
type: "boolean",
|
|
4526
|
-
description: "Preflight selected packaged Skills without changing files",
|
|
4527
|
-
default: false
|
|
4528
|
-
},
|
|
4529
|
-
"force": {
|
|
4530
|
-
type: "boolean",
|
|
4531
|
-
description: "Replace divergent package-owned Skill directories",
|
|
4532
|
-
default: false
|
|
4533
|
-
}
|
|
4534
|
-
},
|
|
4535
|
-
async run({ args }) {
|
|
4536
|
-
if (args._.length > 1)
|
|
4537
|
-
throw new Error("rsp skills install accepts at most one Skill name");
|
|
4538
|
-
await executeCliCommand({
|
|
4539
|
-
execute: () => installPackagedSkills({
|
|
4540
|
-
dryRun: Boolean(args["dry-run"]),
|
|
4541
|
-
force: Boolean(args.force),
|
|
4542
|
-
names: args.name ? [args.name] : void 0
|
|
4543
|
-
}),
|
|
4544
|
-
present: (result) => presentSkillInstallResult(result, Boolean(args["dry-run"]))
|
|
4545
|
-
}, args);
|
|
3646
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
3647
|
+
|
|
3648
|
+
// src/cli/presenters/skills.ts
|
|
3649
|
+
function presentPackagedSkillInventory(inventory, json = false) {
|
|
3650
|
+
if (json) {
|
|
3651
|
+
emitJson(inventory);
|
|
3652
|
+
return;
|
|
4546
3653
|
}
|
|
4547
|
-
});
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
3654
|
+
console.log(` ${inventory.package.name}@${inventory.package.version}`);
|
|
3655
|
+
console.log(` target: ${inventory.target}`);
|
|
3656
|
+
console.log(" Core suite (managed by init/update)");
|
|
3657
|
+
for (const skill of inventory.skills)
|
|
3658
|
+
console.log(` ${skill.name} ${skill.status}`);
|
|
3659
|
+
for (const name of inventory.retired)
|
|
3660
|
+
console.log(` ${name} retired`);
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
// src/cli/commands/skills.ts
|
|
3664
|
+
var skillsListCommand = defineCommand4({
|
|
3665
|
+
meta: { name: "list", description: "Inspect the complete package-owned Skill inventory" },
|
|
3666
|
+
args: { ...jsonArgs },
|
|
4556
3667
|
async run({ args }) {
|
|
4557
3668
|
await executeCliCommand({
|
|
4558
3669
|
execute: () => inspectPackagedSkillInventory(),
|
|
@@ -4560,32 +3671,25 @@ var skillsListCommand = defineCommand3({
|
|
|
4560
3671
|
}, args);
|
|
4561
3672
|
}
|
|
4562
3673
|
});
|
|
4563
|
-
var skillsCommand = defineCommand3({
|
|
4564
|
-
meta: {
|
|
4565
|
-
name: "skills",
|
|
4566
|
-
description: "Manage package-bundled project Skills"
|
|
4567
|
-
},
|
|
4568
|
-
subCommands: {
|
|
4569
|
-
install: skillsInstallCommand,
|
|
4570
|
-
list: skillsListCommand
|
|
4571
|
-
}
|
|
4572
|
-
});
|
|
4573
3674
|
var skillsCommands = {
|
|
4574
|
-
skills:
|
|
3675
|
+
skills: defineCommand4({
|
|
3676
|
+
meta: { name: "skills", description: "Inspect Skills; use init or update to manage the complete environment" },
|
|
3677
|
+
subCommands: { list: skillsListCommand }
|
|
3678
|
+
})
|
|
4575
3679
|
};
|
|
4576
3680
|
|
|
4577
3681
|
// src/cli/commands/work.ts
|
|
4578
|
-
import { defineCommand as
|
|
3682
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
4579
3683
|
|
|
4580
3684
|
// src/commands/archive.ts
|
|
4581
|
-
import { existsSync as
|
|
4582
|
-
import { mkdir as
|
|
4583
|
-
import { basename as basename2, join as
|
|
3685
|
+
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
3686
|
+
import { mkdir as mkdir5, readFile as readFile12, rename as rename3, unlink as unlink4 } from "fs/promises";
|
|
3687
|
+
import { basename as basename2, join as join8 } from "path";
|
|
4584
3688
|
async function archiveChange(name) {
|
|
4585
3689
|
if (!name) {
|
|
4586
3690
|
return { ok: false, kind: "usage", message: "rsp archive <name>" };
|
|
4587
3691
|
}
|
|
4588
|
-
const initialization =
|
|
3692
|
+
const initialization = inspectRspInitialization();
|
|
4589
3693
|
if (initialization)
|
|
4590
3694
|
return { ok: false, kind: "error", message: initialization };
|
|
4591
3695
|
try {
|
|
@@ -4597,7 +3701,7 @@ async function archiveChange(name) {
|
|
|
4597
3701
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4598
3702
|
const base = basename2(workRef.name);
|
|
4599
3703
|
const archiveName = resolveArchiveName(archiveSubdir, date, base);
|
|
4600
|
-
const content = await
|
|
3704
|
+
const content = await readFile12(srcPath, "utf-8");
|
|
4601
3705
|
const configInspection = await inspectRspConfig();
|
|
4602
3706
|
if (configInspection.issues.length > 0) {
|
|
4603
3707
|
return {
|
|
@@ -4607,9 +3711,10 @@ async function archiveChange(name) {
|
|
|
4607
3711
|
};
|
|
4608
3712
|
}
|
|
4609
3713
|
const dependencyInspection = await inspectChangeDependencies();
|
|
4610
|
-
const
|
|
3714
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
3715
|
+
const readiness = collectArchiveReadiness(document, {
|
|
4611
3716
|
activeBlockers: dependencyInspection.activeBlockers.get(workRef.name),
|
|
4612
|
-
documentDiagnostics: inspectChangeDocument(
|
|
3717
|
+
documentDiagnostics: inspectChangeDocument(document, {
|
|
4613
3718
|
name: workRef.name,
|
|
4614
3719
|
validKinds: resolveKinds(configInspection.config),
|
|
4615
3720
|
requiredSections: resolveRequiredSections(configInspection.config)
|
|
@@ -4624,13 +3729,13 @@ async function archiveChange(name) {
|
|
|
4624
3729
|
warnings: checklist
|
|
4625
3730
|
};
|
|
4626
3731
|
}
|
|
4627
|
-
await
|
|
4628
|
-
await rename3(srcPath,
|
|
3732
|
+
await mkdir5(archiveSubdir, { recursive: true });
|
|
3733
|
+
await rename3(srcPath, join8(archiveSubdir, archiveName));
|
|
4629
3734
|
const postArchiveWarnings = [];
|
|
4630
3735
|
let focusCleared = false;
|
|
4631
|
-
if (
|
|
3736
|
+
if (existsSync4(focusEntry)) {
|
|
4632
3737
|
try {
|
|
4633
|
-
await
|
|
3738
|
+
await unlink4(focusEntry);
|
|
4634
3739
|
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
4635
3740
|
focusCleared = true;
|
|
4636
3741
|
} catch (error) {
|
|
@@ -4642,7 +3747,7 @@ async function archiveChange(name) {
|
|
|
4642
3747
|
} catch (error) {
|
|
4643
3748
|
postArchiveWarnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
4644
3749
|
}
|
|
4645
|
-
return { ok: true, archiveName, focusCleared, readinessWarnings: checklist, cleanupWarnings: postArchiveWarnings, git:
|
|
3750
|
+
return { ok: true, archiveName, focusCleared, readinessWarnings: checklist, cleanupWarnings: postArchiveWarnings, git: existsSync4(".git") };
|
|
4646
3751
|
});
|
|
4647
3752
|
} catch (error) {
|
|
4648
3753
|
if (error instanceof WorkRefError) {
|
|
@@ -4651,15 +3756,6 @@ async function archiveChange(name) {
|
|
|
4651
3756
|
throw error;
|
|
4652
3757
|
}
|
|
4653
3758
|
}
|
|
4654
|
-
function inspectInitialization() {
|
|
4655
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
4656
|
-
const design = inspectManagedFile(join7(RSP_DIR, "specs", "design.md"), "design Spec", { allowMissing: true });
|
|
4657
|
-
if (!rules.issue && !design.issue && rules.exists && design.exists)
|
|
4658
|
-
return void 0;
|
|
4659
|
-
const initialized = existsSync6(RSP_DIR);
|
|
4660
|
-
return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
4661
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}`;
|
|
4662
|
-
}
|
|
4663
3759
|
function resolveArchiveName(archiveSubdir, date, base) {
|
|
4664
3760
|
const initialName = `${date}_${base}.md`;
|
|
4665
3761
|
if (!archiveNameExists(archiveSubdir, initialName))
|
|
@@ -4677,7 +3773,7 @@ function resolveArchiveName(archiveSubdir, date, base) {
|
|
|
4677
3773
|
);
|
|
4678
3774
|
}
|
|
4679
3775
|
function archiveNameExists(archiveSubdir, name) {
|
|
4680
|
-
if (!
|
|
3776
|
+
if (!existsSync4(archiveSubdir))
|
|
4681
3777
|
return false;
|
|
4682
3778
|
return readdirSync(archiveSubdir).some((entry) => {
|
|
4683
3779
|
if (entry.normalize("NFC") !== name)
|
|
@@ -4693,252 +3789,10 @@ function archiveNameExists(archiveSubdir, name) {
|
|
|
4693
3789
|
});
|
|
4694
3790
|
}
|
|
4695
3791
|
|
|
4696
|
-
// src/commands/commit.ts
|
|
4697
|
-
import { execFile, spawn } from "child_process";
|
|
4698
|
-
import { lstat as lstat4, readFile as readFile10 } from "fs/promises";
|
|
4699
|
-
import { promisify } from "util";
|
|
4700
|
-
var execFileAsync = promisify(execFile);
|
|
4701
|
-
function messagesMatch(prepared, observed) {
|
|
4702
|
-
if (prepared === observed)
|
|
4703
|
-
return true;
|
|
4704
|
-
if (prepared.endsWith("\n") && prepared.slice(0, -1) === observed)
|
|
4705
|
-
return true;
|
|
4706
|
-
return observed.endsWith("\n") && observed.slice(0, -1) === prepared;
|
|
4707
|
-
}
|
|
4708
|
-
function extractCommittedMessage(output) {
|
|
4709
|
-
const separator = output.indexOf("\0");
|
|
4710
|
-
return separator === -1 ? output : output.slice(0, separator);
|
|
4711
|
-
}
|
|
4712
|
-
async function readStagedPaths() {
|
|
4713
|
-
const { stdout } = await execFileAsync("git", ["diff", "--cached", "--name-only", "--no-renames", "--diff-filter=ACDMRTUXB"], {
|
|
4714
|
-
encoding: "utf8"
|
|
4715
|
-
});
|
|
4716
|
-
return stdout.split("\n").filter(Boolean);
|
|
4717
|
-
}
|
|
4718
|
-
async function readHead() {
|
|
4719
|
-
try {
|
|
4720
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "--verify", "HEAD"], { encoding: "utf8" });
|
|
4721
|
-
return stdout.trim();
|
|
4722
|
-
} catch {
|
|
4723
|
-
return null;
|
|
4724
|
-
}
|
|
4725
|
-
}
|
|
4726
|
-
async function readPathLines(args) {
|
|
4727
|
-
const { stdout } = await execFileAsync("git", args, { encoding: "utf8" });
|
|
4728
|
-
return stdout.split("\n").filter(Boolean);
|
|
4729
|
-
}
|
|
4730
|
-
async function readCommittedPaths(commit) {
|
|
4731
|
-
return readPathLines(["diff-tree", "--root", "--no-commit-id", "--name-only", "--no-renames", "-r", commit]);
|
|
4732
|
-
}
|
|
4733
|
-
async function readRemainingWorktreePaths() {
|
|
4734
|
-
const [unstaged, staged, untracked] = await Promise.all([
|
|
4735
|
-
readPathLines(["diff", "--name-only", "--no-renames", "--diff-filter=ACDMRTUXB"]),
|
|
4736
|
-
readPathLines(["diff", "--cached", "--name-only", "--no-renames", "--diff-filter=ACDMRTUXB"]),
|
|
4737
|
-
readPathLines(["ls-files", "--others", "--exclude-standard"])
|
|
4738
|
-
]);
|
|
4739
|
-
return [.../* @__PURE__ */ new Set([...unstaged, ...staged, ...untracked])].sort();
|
|
4740
|
-
}
|
|
4741
|
-
async function gitPathExists(path) {
|
|
4742
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", path], { encoding: "utf8" });
|
|
4743
|
-
try {
|
|
4744
|
-
await lstat4(stdout.trim());
|
|
4745
|
-
return true;
|
|
4746
|
-
} catch (error) {
|
|
4747
|
-
if (error.code === "ENOENT")
|
|
4748
|
-
return false;
|
|
4749
|
-
throw error;
|
|
4750
|
-
}
|
|
4751
|
-
}
|
|
4752
|
-
async function detectGitOperation() {
|
|
4753
|
-
const operations = [
|
|
4754
|
-
["merge", "MERGE_HEAD"],
|
|
4755
|
-
["cherry-pick", "CHERRY_PICK_HEAD"],
|
|
4756
|
-
["revert", "REVERT_HEAD"],
|
|
4757
|
-
["rebase", "rebase-merge"],
|
|
4758
|
-
["rebase-or-am", "rebase-apply"],
|
|
4759
|
-
["sequencer", "sequencer"]
|
|
4760
|
-
];
|
|
4761
|
-
for (const [operation, path] of operations) {
|
|
4762
|
-
if (await gitPathExists(path))
|
|
4763
|
-
return operation;
|
|
4764
|
-
}
|
|
4765
|
-
return null;
|
|
4766
|
-
}
|
|
4767
|
-
function samePaths(expected, observed) {
|
|
4768
|
-
const left = [...expected].sort();
|
|
4769
|
-
const right = [...observed].sort();
|
|
4770
|
-
return left.length === right.length && left.every((path, index) => path === right[index]);
|
|
4771
|
-
}
|
|
4772
|
-
async function runGitCommit(message) {
|
|
4773
|
-
return await new Promise((resolve5) => {
|
|
4774
|
-
const child = spawn("git", ["commit", "--cleanup=verbatim", "-F", "-"], {
|
|
4775
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4776
|
-
});
|
|
4777
|
-
let stdout = "";
|
|
4778
|
-
let stderr = "";
|
|
4779
|
-
let settled = false;
|
|
4780
|
-
child.stdout.setEncoding("utf8");
|
|
4781
|
-
child.stderr.setEncoding("utf8");
|
|
4782
|
-
child.stdout.on("data", (chunk) => stdout += chunk);
|
|
4783
|
-
child.stderr.on("data", (chunk) => stderr += chunk);
|
|
4784
|
-
child.on("error", (error) => {
|
|
4785
|
-
if (!settled) {
|
|
4786
|
-
settled = true;
|
|
4787
|
-
resolve5({ ok: false, message: toErrorMessage(error) });
|
|
4788
|
-
}
|
|
4789
|
-
});
|
|
4790
|
-
child.on("close", (code) => {
|
|
4791
|
-
if (settled)
|
|
4792
|
-
return;
|
|
4793
|
-
settled = true;
|
|
4794
|
-
if (code === 0)
|
|
4795
|
-
resolve5({ ok: true, stdout });
|
|
4796
|
-
else
|
|
4797
|
-
resolve5({ ok: false, message: stderr.trim() || stdout.trim() || `git commit exited with code ${code ?? "unknown"}` });
|
|
4798
|
-
});
|
|
4799
|
-
child.stdin.end(message);
|
|
4800
|
-
});
|
|
4801
|
-
}
|
|
4802
|
-
async function commitFromMessageFile(messageFile) {
|
|
4803
|
-
let preparedMessage;
|
|
4804
|
-
try {
|
|
4805
|
-
preparedMessage = await readFile10(messageFile, "utf8");
|
|
4806
|
-
} catch (error) {
|
|
4807
|
-
return {
|
|
4808
|
-
ok: false,
|
|
4809
|
-
command: "commit",
|
|
4810
|
-
code: "message_file_read_failed",
|
|
4811
|
-
message: `unable to read message file: ${toErrorMessage(error)}`
|
|
4812
|
-
};
|
|
4813
|
-
}
|
|
4814
|
-
if (preparedMessage.includes("\\n")) {
|
|
4815
|
-
return {
|
|
4816
|
-
ok: false,
|
|
4817
|
-
command: "commit",
|
|
4818
|
-
code: "literal_newline_escape",
|
|
4819
|
-
message: "message file contains literal \\\\n; use actual line breaks"
|
|
4820
|
-
};
|
|
4821
|
-
}
|
|
4822
|
-
let operation;
|
|
4823
|
-
try {
|
|
4824
|
-
operation = await detectGitOperation();
|
|
4825
|
-
} catch (error) {
|
|
4826
|
-
return {
|
|
4827
|
-
ok: false,
|
|
4828
|
-
command: "commit",
|
|
4829
|
-
code: "git_operation_in_progress",
|
|
4830
|
-
message: `unable to verify current Git operation state: ${toErrorMessage(error)}`
|
|
4831
|
-
};
|
|
4832
|
-
}
|
|
4833
|
-
if (operation) {
|
|
4834
|
-
return {
|
|
4835
|
-
ok: false,
|
|
4836
|
-
command: "commit",
|
|
4837
|
-
code: "git_operation_in_progress",
|
|
4838
|
-
operation,
|
|
4839
|
-
message: `refusing to commit while Git operation is in progress: ${operation}`
|
|
4840
|
-
};
|
|
4841
|
-
}
|
|
4842
|
-
let stagedPaths;
|
|
4843
|
-
try {
|
|
4844
|
-
stagedPaths = await readStagedPaths();
|
|
4845
|
-
} catch (error) {
|
|
4846
|
-
return {
|
|
4847
|
-
ok: false,
|
|
4848
|
-
command: "commit",
|
|
4849
|
-
code: "no_staged_boundary",
|
|
4850
|
-
message: `unable to inspect staged boundary: ${toErrorMessage(error)}`
|
|
4851
|
-
};
|
|
4852
|
-
}
|
|
4853
|
-
if (stagedPaths.length === 0) {
|
|
4854
|
-
return {
|
|
4855
|
-
ok: false,
|
|
4856
|
-
command: "commit",
|
|
4857
|
-
code: "no_staged_boundary",
|
|
4858
|
-
message: "no staged boundary exists"
|
|
4859
|
-
};
|
|
4860
|
-
}
|
|
4861
|
-
const headBefore = await readHead();
|
|
4862
|
-
const commitResult = await runGitCommit(preparedMessage);
|
|
4863
|
-
if (!commitResult.ok) {
|
|
4864
|
-
return {
|
|
4865
|
-
ok: false,
|
|
4866
|
-
command: "commit",
|
|
4867
|
-
code: "git_commit_failed",
|
|
4868
|
-
message: commitResult.message
|
|
4869
|
-
};
|
|
4870
|
-
}
|
|
4871
|
-
let headAfter;
|
|
4872
|
-
let observedMessage;
|
|
4873
|
-
let committedPaths;
|
|
4874
|
-
let remainingWorktreePaths;
|
|
4875
|
-
try {
|
|
4876
|
-
const [head, observed] = await Promise.all([
|
|
4877
|
-
execFileAsync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }),
|
|
4878
|
-
execFileAsync("git", ["show", "-s", "--format=%B%x00", "HEAD"], { encoding: "utf8" })
|
|
4879
|
-
]);
|
|
4880
|
-
headAfter = head.stdout.trim();
|
|
4881
|
-
observedMessage = extractCommittedMessage(observed.stdout);
|
|
4882
|
-
[committedPaths, remainingWorktreePaths] = await Promise.all([
|
|
4883
|
-
readCommittedPaths(headAfter),
|
|
4884
|
-
readRemainingWorktreePaths()
|
|
4885
|
-
]);
|
|
4886
|
-
} catch (error) {
|
|
4887
|
-
return {
|
|
4888
|
-
ok: false,
|
|
4889
|
-
command: "commit",
|
|
4890
|
-
code: "receipt_observation_failed",
|
|
4891
|
-
message: `commit created but its complete receipt could not be observed: ${toErrorMessage(error)}`,
|
|
4892
|
-
headBefore,
|
|
4893
|
-
preparedLength: preparedMessage.length
|
|
4894
|
-
};
|
|
4895
|
-
}
|
|
4896
|
-
if (!messagesMatch(preparedMessage, observedMessage)) {
|
|
4897
|
-
return {
|
|
4898
|
-
ok: false,
|
|
4899
|
-
command: "commit",
|
|
4900
|
-
code: "message_mismatch",
|
|
4901
|
-
message: "committed message differs from the prepared message; stopped without amend or a second commit",
|
|
4902
|
-
commit: headAfter,
|
|
4903
|
-
headBefore,
|
|
4904
|
-
headAfter,
|
|
4905
|
-
storedMessage: observedMessage,
|
|
4906
|
-
committedPaths,
|
|
4907
|
-
remainingWorktreePaths,
|
|
4908
|
-
preparedLength: preparedMessage.length,
|
|
4909
|
-
observedLength: observedMessage.length
|
|
4910
|
-
};
|
|
4911
|
-
}
|
|
4912
|
-
if (!samePaths(stagedPaths, committedPaths)) {
|
|
4913
|
-
return {
|
|
4914
|
-
ok: false,
|
|
4915
|
-
command: "commit",
|
|
4916
|
-
code: "commit_boundary_mismatch",
|
|
4917
|
-
message: "committed paths differ from the reviewed staged boundary; stopped without amend or a second commit",
|
|
4918
|
-
commit: headAfter,
|
|
4919
|
-
headBefore,
|
|
4920
|
-
headAfter,
|
|
4921
|
-
storedMessage: observedMessage,
|
|
4922
|
-
committedPaths,
|
|
4923
|
-
remainingWorktreePaths
|
|
4924
|
-
};
|
|
4925
|
-
}
|
|
4926
|
-
return {
|
|
4927
|
-
ok: true,
|
|
4928
|
-
command: "commit",
|
|
4929
|
-
commit: headAfter,
|
|
4930
|
-
headBefore,
|
|
4931
|
-
headAfter,
|
|
4932
|
-
storedMessage: observedMessage,
|
|
4933
|
-
committedPaths,
|
|
4934
|
-
remainingWorktreePaths
|
|
4935
|
-
};
|
|
4936
|
-
}
|
|
4937
|
-
|
|
4938
3792
|
// src/commands/create.ts
|
|
4939
|
-
import { existsSync as
|
|
4940
|
-
import { mkdir as
|
|
4941
|
-
import { dirname as
|
|
3793
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3794
|
+
import { mkdir as mkdir6, unlink as unlink5, writeFile as writeFile4 } from "fs/promises";
|
|
3795
|
+
import { dirname as dirname4 } from "path";
|
|
4942
3796
|
async function createChange(name, summary = "", kind, options = {}) {
|
|
4943
3797
|
if (!name) {
|
|
4944
3798
|
return { ok: false, kind: "usage", message: "rsp create <name> [summary]" };
|
|
@@ -4948,31 +3802,31 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
4948
3802
|
if (options.issueRelation && !hasIssueOption)
|
|
4949
3803
|
throw new Error("--issue-relation requires --issue");
|
|
4950
3804
|
const issues = options.issue !== void 0 ? [createIssueRelationship(options.issue, options.issueRelation)] : [];
|
|
4951
|
-
const initialization =
|
|
3805
|
+
const initialization = inspectRspInitialization();
|
|
4952
3806
|
if (initialization)
|
|
4953
3807
|
return { ok: false, kind: "error", message: initialization, warning };
|
|
4954
3808
|
try {
|
|
4955
3809
|
return await withRspLock("create-change", async () => {
|
|
4956
3810
|
const workRef = await resolveExecutableChange(name);
|
|
4957
3811
|
const changePath = workRef.path;
|
|
4958
|
-
const existed =
|
|
3812
|
+
const existed = existsSync5(changePath);
|
|
4959
3813
|
if (existed && hasIssueOption)
|
|
4960
3814
|
throw new Error(`Change already exists; --issue cannot update ${workRef.name}`);
|
|
4961
3815
|
if (!existed) {
|
|
4962
3816
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
4963
3817
|
const content = generateChangeContent(workRef.name, summary, kind, { issues });
|
|
4964
|
-
await
|
|
3818
|
+
await mkdir6(dirname4(changePath), { recursive: true });
|
|
4965
3819
|
let changeCreated = false;
|
|
4966
3820
|
try {
|
|
4967
|
-
await
|
|
3821
|
+
await writeFile4(changePath, content, { flag: "wx" });
|
|
4968
3822
|
changeCreated = true;
|
|
4969
|
-
await
|
|
4970
|
-
await
|
|
3823
|
+
await mkdir6(FOCUS_DIR, { recursive: true });
|
|
3824
|
+
await mkdir6(dirname4(focusEntry), { recursive: true });
|
|
4971
3825
|
await writeManagedFile(focusEntry, "", "focus marker");
|
|
4972
3826
|
} catch (error) {
|
|
4973
3827
|
if (changeCreated) {
|
|
4974
3828
|
try {
|
|
4975
|
-
await
|
|
3829
|
+
await unlink5(changePath);
|
|
4976
3830
|
await cleanupEmptyParentDirs(changePath, CHANGES_DIR);
|
|
4977
3831
|
} catch {
|
|
4978
3832
|
}
|
|
@@ -4989,35 +3843,26 @@ async function createChange(name, summary = "", kind, options = {}) {
|
|
|
4989
3843
|
throw error;
|
|
4990
3844
|
}
|
|
4991
3845
|
}
|
|
4992
|
-
function inspectInitialization2() {
|
|
4993
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
4994
|
-
const design = inspectManagedFile(`${RSP_DIR}/specs/design.md`, "design Spec", { allowMissing: true });
|
|
4995
|
-
if (!rules.issue && !design.issue && rules.exists && design.exists)
|
|
4996
|
-
return void 0;
|
|
4997
|
-
const initialized = existsSync7(RSP_DIR);
|
|
4998
|
-
return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
4999
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}`;
|
|
5000
|
-
}
|
|
5001
3846
|
|
|
5002
3847
|
// src/commands/focus.ts
|
|
5003
|
-
import { Buffer
|
|
5004
|
-
import { randomUUID
|
|
5005
|
-
import { existsSync as
|
|
5006
|
-
import { mkdir as
|
|
5007
|
-
import { basename as basename3, dirname as
|
|
3848
|
+
import { Buffer } from "buffer";
|
|
3849
|
+
import { randomUUID } from "crypto";
|
|
3850
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3851
|
+
import { mkdir as mkdir7, open, rename as rename4, unlink as unlink6 } from "fs/promises";
|
|
3852
|
+
import { basename as basename3, dirname as dirname5, join as join9 } from "path";
|
|
5008
3853
|
async function focusChange(name, options = {}) {
|
|
5009
3854
|
if (!name) {
|
|
5010
3855
|
return { ok: false, kind: "usage", message: "rsp focus <name>" };
|
|
5011
3856
|
}
|
|
5012
|
-
const initialization =
|
|
3857
|
+
const initialization = inspectRspInitialization();
|
|
5013
3858
|
if (initialization)
|
|
5014
3859
|
return { ok: false, kind: "error", message: initialization };
|
|
5015
3860
|
try {
|
|
5016
3861
|
return await withRspLock("focus-change", async () => {
|
|
5017
3862
|
const workRef = await resolveExecutableChange(name, { mustExist: true });
|
|
5018
3863
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
5019
|
-
await
|
|
5020
|
-
await
|
|
3864
|
+
await mkdir7(FOCUS_DIR, { recursive: true });
|
|
3865
|
+
await mkdir7(dirname5(focusEntry), { recursive: true });
|
|
5021
3866
|
if (options.capsuleFile === void 0)
|
|
5022
3867
|
await ensureManagedFile(focusEntry, "", "focus marker");
|
|
5023
3868
|
else
|
|
@@ -5039,10 +3884,10 @@ async function readCapsule(source) {
|
|
|
5039
3884
|
}
|
|
5040
3885
|
async function replaceFocusCapsule(path, content) {
|
|
5041
3886
|
requireManagedFile(path, "focus marker", { allowMissing: true });
|
|
5042
|
-
const temporaryPath =
|
|
3887
|
+
const temporaryPath = join9(dirname5(path), `.${basename3(path)}.${randomUUID()}.tmp`);
|
|
5043
3888
|
let handle;
|
|
5044
3889
|
try {
|
|
5045
|
-
handle = await
|
|
3890
|
+
handle = await open(temporaryPath, "wx");
|
|
5046
3891
|
await handle.writeFile(content, "utf-8");
|
|
5047
3892
|
await handle.close();
|
|
5048
3893
|
handle = void 0;
|
|
@@ -5051,7 +3896,7 @@ async function replaceFocusCapsule(path, content) {
|
|
|
5051
3896
|
} finally {
|
|
5052
3897
|
await handle?.close().catch(() => {
|
|
5053
3898
|
});
|
|
5054
|
-
await
|
|
3899
|
+
await unlink6(temporaryPath).catch((error) => {
|
|
5055
3900
|
if (error.code !== "ENOENT")
|
|
5056
3901
|
throw error;
|
|
5057
3902
|
});
|
|
@@ -5059,7 +3904,7 @@ async function replaceFocusCapsule(path, content) {
|
|
|
5059
3904
|
}
|
|
5060
3905
|
async function readRegularCapsuleFile(path) {
|
|
5061
3906
|
requireManagedFile(path, "focus capsule input");
|
|
5062
|
-
const handle = await
|
|
3907
|
+
const handle = await open(path, "r");
|
|
5063
3908
|
try {
|
|
5064
3909
|
if (!(await handle.stat()).isFile())
|
|
5065
3910
|
throw new Error(`focus capsule input must be a regular file: ${path}`);
|
|
@@ -5072,28 +3917,28 @@ async function readBoundedStdin() {
|
|
|
5072
3917
|
const chunks = [];
|
|
5073
3918
|
let size = 0;
|
|
5074
3919
|
for await (const chunk of process.stdin) {
|
|
5075
|
-
const buffer =
|
|
3920
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5076
3921
|
size += buffer.byteLength;
|
|
5077
3922
|
if (size > MAX_FOCUS_CAPSULE_BYTES)
|
|
5078
3923
|
throw new Error(`focus capsule exceeds ${MAX_FOCUS_CAPSULE_BYTES} UTF-8 bytes`);
|
|
5079
3924
|
chunks.push(buffer);
|
|
5080
3925
|
}
|
|
5081
|
-
return
|
|
3926
|
+
return Buffer.concat(chunks, size);
|
|
5082
3927
|
}
|
|
5083
3928
|
async function unfocusChange(name) {
|
|
5084
3929
|
if (!name) {
|
|
5085
3930
|
return { ok: false, kind: "usage", message: "rsp unfocus <name>" };
|
|
5086
3931
|
}
|
|
5087
|
-
const initialization =
|
|
3932
|
+
const initialization = inspectRspInitialization();
|
|
5088
3933
|
if (initialization)
|
|
5089
3934
|
return { ok: false, kind: "error", message: initialization };
|
|
5090
3935
|
try {
|
|
5091
3936
|
return await withRspLock("unfocus-change", async () => {
|
|
5092
3937
|
const workRef = resolveWorkRef(name, { executable: false, mustExist: false });
|
|
5093
3938
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
5094
|
-
if (!
|
|
3939
|
+
if (!existsSync6(focusEntry))
|
|
5095
3940
|
throw new WorkRefError("focus_marker_not_found", `.rsp/focus.d/${name}`, name);
|
|
5096
|
-
await
|
|
3941
|
+
await unlink6(focusEntry);
|
|
5097
3942
|
await cleanupEmptyParentDirs(focusEntry, FOCUS_DIR);
|
|
5098
3943
|
return { ok: true, action: "unfocus", workRef: workRef.name };
|
|
5099
3944
|
});
|
|
@@ -5109,21 +3954,12 @@ function workRefFailure(error) {
|
|
|
5109
3954
|
}
|
|
5110
3955
|
throw error;
|
|
5111
3956
|
}
|
|
5112
|
-
function inspectInitialization3() {
|
|
5113
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
5114
|
-
const design = inspectManagedFile(`${RSP_DIR}/specs/design.md`, "design Spec", { allowMissing: true });
|
|
5115
|
-
if (!rules.issue && !design.issue && rules.exists && design.exists)
|
|
5116
|
-
return void 0;
|
|
5117
|
-
const initialized = existsSync8(RSP_DIR);
|
|
5118
|
-
return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
5119
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}`;
|
|
5120
|
-
}
|
|
5121
3957
|
|
|
5122
3958
|
// src/commands/group.ts
|
|
5123
|
-
import { randomUUID as
|
|
5124
|
-
import { existsSync as
|
|
5125
|
-
import { link as
|
|
5126
|
-
import { join as
|
|
3959
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3960
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3961
|
+
import { link as link2, mkdir as mkdir8, readdir as readdir4, readFile as readFile13, rename as rename5, unlink as unlink7, writeFile as writeFile5 } from "fs/promises";
|
|
3962
|
+
import { join as join10 } from "path";
|
|
5127
3963
|
var ChangeGroupError = class extends Error {
|
|
5128
3964
|
name = "ChangeGroupError";
|
|
5129
3965
|
};
|
|
@@ -5131,7 +3967,7 @@ async function createChangeGroup(name, goal = "") {
|
|
|
5131
3967
|
if (!name) {
|
|
5132
3968
|
return failure("usage", "rsp group create <name> [goal]");
|
|
5133
3969
|
}
|
|
5134
|
-
const initialization =
|
|
3970
|
+
const initialization = inspectRspInitialization();
|
|
5135
3971
|
if (initialization)
|
|
5136
3972
|
return failure("error", initialization);
|
|
5137
3973
|
try {
|
|
@@ -5143,9 +3979,9 @@ async function createChangeGroup(name, goal = "") {
|
|
|
5143
3979
|
if (await hasArchivedGroupBrief(groupName))
|
|
5144
3980
|
throw new ChangeGroupError(`archived Change Group cannot be reopened: ${groupName}`);
|
|
5145
3981
|
const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
|
|
5146
|
-
await
|
|
3982
|
+
await mkdir8(groupDirectory, { recursive: true });
|
|
5147
3983
|
try {
|
|
5148
|
-
await
|
|
3984
|
+
await writeFile5(ref.path, generateGroupBriefContent(groupName, goal), { flag: "wx" });
|
|
5149
3985
|
} catch (error) {
|
|
5150
3986
|
if (error.code === "EEXIST")
|
|
5151
3987
|
throw new ChangeGroupError(`Change Group already exists: ${groupName}`);
|
|
@@ -5164,7 +4000,7 @@ async function closeChangeGroup(name) {
|
|
|
5164
4000
|
if (!name) {
|
|
5165
4001
|
return failure("usage", "rsp group close <name>");
|
|
5166
4002
|
}
|
|
5167
|
-
const initialization =
|
|
4003
|
+
const initialization = inspectRspInitialization();
|
|
5168
4004
|
if (initialization)
|
|
5169
4005
|
return failure("error", initialization);
|
|
5170
4006
|
try {
|
|
@@ -5183,15 +4019,15 @@ async function closeChangeGroup(name) {
|
|
|
5183
4019
|
const archiveDirectory = resolveArchiveDirectory(ref);
|
|
5184
4020
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5185
4021
|
const archiveName = resolveArchiveName(archiveDirectory, date, "brief");
|
|
5186
|
-
await
|
|
5187
|
-
await rename5(ref.path,
|
|
4022
|
+
await mkdir8(archiveDirectory, { recursive: true });
|
|
4023
|
+
await rename5(ref.path, join10(archiveDirectory, archiveName));
|
|
5188
4024
|
const warnings = [];
|
|
5189
4025
|
try {
|
|
5190
4026
|
await cleanupEmptyParentDirs(ref.path, CHANGES_DIR);
|
|
5191
4027
|
} catch (error) {
|
|
5192
4028
|
warnings.push(`changes directory cleanup failed: ${toErrorMessage(error)}`);
|
|
5193
4029
|
}
|
|
5194
|
-
return { ok: true, action: "close", groupName, archivePath:
|
|
4030
|
+
return { ok: true, action: "close", groupName, archivePath: join10(archiveDirectory, archiveName), warnings };
|
|
5195
4031
|
});
|
|
5196
4032
|
} catch (error) {
|
|
5197
4033
|
if (error instanceof WorkRefError || error instanceof ChangeGroupError) {
|
|
@@ -5205,18 +4041,18 @@ async function reopenChangeGroup(name, options) {
|
|
|
5205
4041
|
return failure("usage", "rsp group reopen <name> --reason <text> [--from <archive-path>]");
|
|
5206
4042
|
}
|
|
5207
4043
|
const reason = options.reason?.trim();
|
|
5208
|
-
if (!reason || /[\r\n]/.test(reason)) {
|
|
4044
|
+
if (!reason || /[\r\n]/.test(reason) || !markdownText(parseMarkdown(reason).root).trim()) {
|
|
5209
4045
|
return failure("error", "--reason must be one non-empty line");
|
|
5210
4046
|
}
|
|
5211
|
-
const initialization =
|
|
4047
|
+
const initialization = inspectRspInitialization();
|
|
5212
4048
|
if (initialization)
|
|
5213
4049
|
return failure("error", initialization);
|
|
5214
4050
|
try {
|
|
5215
4051
|
return await withRspLock("reopen-change-group", async () => {
|
|
5216
4052
|
const warnings = [];
|
|
5217
4053
|
const groupName = normalizeWorkRefSegment(name);
|
|
5218
|
-
await requireEmptyGroupRecoverySubtree(
|
|
5219
|
-
await requireEmptyGroupRecoverySubtree(
|
|
4054
|
+
await requireEmptyGroupRecoverySubtree(join10(CHANGES_DIR, groupName), `Change Group work subtree for ${groupName}`);
|
|
4055
|
+
await requireEmptyGroupRecoverySubtree(join10(FOCUS_DIR, groupName), `Change Group focus subtree for ${groupName}`);
|
|
5220
4056
|
const ref = resolveWorkRef(`${groupName}/brief`);
|
|
5221
4057
|
if (ref.kind !== "group-brief")
|
|
5222
4058
|
throw new ChangeGroupError(`invalid Change Group name: ${name}`);
|
|
@@ -5237,16 +4073,16 @@ async function reopenChangeGroup(name, options) {
|
|
|
5237
4073
|
`selected archived Change Group belongs to ${record.group}, not ${groupName}`
|
|
5238
4074
|
);
|
|
5239
4075
|
}
|
|
5240
|
-
if (
|
|
4076
|
+
if (existsSync7(ref.path))
|
|
5241
4077
|
throw new ArchiveHistoryError("open_group_exists", `open Change Group already exists: ${groupName}`);
|
|
5242
|
-
const archivedContent = await
|
|
4078
|
+
const archivedContent = await readFile13(record.sourcePath, "utf-8");
|
|
5243
4079
|
const document = parseRspDocument(archivedContent, GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
5244
4080
|
validateArchivedGroupBrief(document, groupName);
|
|
5245
4081
|
const evidenceKey = `${GROUP_REOPEN_COMPLETION_PREFIX} \`${record.path}\`: ${reason}`;
|
|
5246
4082
|
const retainedEvidence = /* @__PURE__ */ new Set();
|
|
5247
4083
|
for (const retained of inspection.groupBriefs.filter((candidate) => candidate.group === groupName)) {
|
|
5248
|
-
const
|
|
5249
|
-
for (const item of getGroupReopenEvidence(
|
|
4084
|
+
const retainedDocument = retained.sourcePath === record.sourcePath ? document : parseRspDocument(await readFile13(retained.sourcePath, "utf-8"), GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
4085
|
+
for (const item of getGroupReopenEvidence(retainedDocument))
|
|
5250
4086
|
retainedEvidence.add(item.key);
|
|
5251
4087
|
}
|
|
5252
4088
|
if (retainedEvidence.has(evidenceKey)) {
|
|
@@ -5261,17 +4097,17 @@ async function reopenChangeGroup(name, options) {
|
|
|
5261
4097
|
`- [ ] ${evidenceKey}`
|
|
5262
4098
|
);
|
|
5263
4099
|
const groupDirectory = resolveManagedDirectoryChain(CHANGES_DIR, [groupName], "Change Group path");
|
|
5264
|
-
await
|
|
5265
|
-
const temporaryPath =
|
|
4100
|
+
await mkdir8(groupDirectory, { recursive: true });
|
|
4101
|
+
const temporaryPath = join10(RSP_DIR, `.group-reopen-${process.pid}-${randomUUID2()}.tmp`);
|
|
5266
4102
|
let mutationError;
|
|
5267
4103
|
try {
|
|
5268
|
-
await
|
|
5269
|
-
await
|
|
4104
|
+
await writeFile5(temporaryPath, reopenedContent, { flag: "wx" });
|
|
4105
|
+
await link2(temporaryPath, ref.path);
|
|
5270
4106
|
} catch (error) {
|
|
5271
4107
|
mutationError = error;
|
|
5272
4108
|
}
|
|
5273
4109
|
try {
|
|
5274
|
-
await
|
|
4110
|
+
await unlink7(temporaryPath);
|
|
5275
4111
|
} catch (error) {
|
|
5276
4112
|
if (!mutationError)
|
|
5277
4113
|
warnings.push(`temporary reopen file cleanup failed: ${toErrorMessage(error)}`);
|
|
@@ -5302,21 +4138,12 @@ async function reopenChangeGroup(name, options) {
|
|
|
5302
4138
|
function failure(kind, message, candidates = [], remainingCandidates = 0) {
|
|
5303
4139
|
return { ok: false, kind, message, candidates, remainingCandidates };
|
|
5304
4140
|
}
|
|
5305
|
-
function inspectInitialization4() {
|
|
5306
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
5307
|
-
const design = inspectManagedFile(join9(RSP_DIR, "specs", "design.md"), "design Spec", { allowMissing: true });
|
|
5308
|
-
if (!rules.issue && !design.issue && rules.exists && design.exists)
|
|
5309
|
-
return void 0;
|
|
5310
|
-
const initialized = existsSync9(RSP_DIR);
|
|
5311
|
-
return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
5312
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}`;
|
|
5313
|
-
}
|
|
5314
4141
|
async function requireEmptyGroupRecoverySubtree(path, label) {
|
|
5315
4142
|
if (!requireManagedDirectory(path, label, { allowMissing: true }))
|
|
5316
4143
|
return;
|
|
5317
4144
|
let entries;
|
|
5318
4145
|
try {
|
|
5319
|
-
entries = await
|
|
4146
|
+
entries = await readdir4(path);
|
|
5320
4147
|
} catch (error) {
|
|
5321
4148
|
throw new ChangeGroupError(`unable to inspect ${label}: ${toErrorMessage(error)}`);
|
|
5322
4149
|
}
|
|
@@ -5329,7 +4156,7 @@ function validateArchivedGroupBrief(document, group) {
|
|
|
5329
4156
|
throw new ArchiveHistoryError("archive_heading_invalid", `archived Change Group must contain exactly one canonical identity heading for ${group}`);
|
|
5330
4157
|
for (const section of GROUP_BRIEF_DOCUMENT_SCHEMA.sections) {
|
|
5331
4158
|
const matches = getDocumentSections(document, section.id);
|
|
5332
|
-
if (matches.length !== 1 || !matches[0].canonical ||
|
|
4159
|
+
if (matches.length !== 1 || !matches[0].canonical || getVisibleBodyLines(getDocumentSectionMarkdown(document, section.id)).length === 0) {
|
|
5333
4160
|
throw new ArchiveHistoryError(
|
|
5334
4161
|
"archive_section_invalid",
|
|
5335
4162
|
`archived Change Group must contain exactly one canonical required section: ${section.heading}`
|
|
@@ -5339,10 +4166,10 @@ function validateArchivedGroupBrief(document, group) {
|
|
|
5339
4166
|
}
|
|
5340
4167
|
function collectCloseReasons(group, errors) {
|
|
5341
4168
|
const reasons = [...errors, ...group.warnings];
|
|
5342
|
-
const
|
|
4169
|
+
const open2 = group.slices.filter((slice) => slice.state === "open").map((slice) => slice.name);
|
|
5343
4170
|
const missing = group.slices.filter((slice) => slice.state === "missing").map((slice) => slice.name);
|
|
5344
|
-
if (
|
|
5345
|
-
reasons.push(`open slices: ${
|
|
4171
|
+
if (open2.length > 0)
|
|
4172
|
+
reasons.push(`open slices: ${open2.join(", ")}`);
|
|
5346
4173
|
if (missing.length > 0)
|
|
5347
4174
|
reasons.push(`missing slices: ${missing.join(", ")}`);
|
|
5348
4175
|
if (group.completion.total === 0)
|
|
@@ -5355,18 +4182,18 @@ function collectCloseReasons(group, errors) {
|
|
|
5355
4182
|
}
|
|
5356
4183
|
|
|
5357
4184
|
// src/commands/reopen.ts
|
|
5358
|
-
import { existsSync as
|
|
5359
|
-
import { mkdir as
|
|
5360
|
-
import { dirname as
|
|
4185
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4186
|
+
import { mkdir as mkdir9, readFile as readFile14, unlink as unlink8, writeFile as writeFile6 } from "fs/promises";
|
|
4187
|
+
import { dirname as dirname6 } from "path";
|
|
5361
4188
|
async function reopenChange(name, options) {
|
|
5362
4189
|
if (!name) {
|
|
5363
4190
|
return { ok: false, kind: "usage", message: "rsp reopen <name> --reason <text> [--from <archive-path>]", candidates: [], remainingCandidates: 0 };
|
|
5364
4191
|
}
|
|
5365
4192
|
const reason = options.reason?.trim();
|
|
5366
|
-
if (!reason || /[\r\n]/.test(reason)) {
|
|
4193
|
+
if (!reason || /[\r\n]/.test(reason) || !markdownText(parseMarkdown(reason).root).trim()) {
|
|
5367
4194
|
return { ok: false, kind: "error", message: "--reason must be one non-empty line", candidates: [], remainingCandidates: 0 };
|
|
5368
4195
|
}
|
|
5369
|
-
const initialization =
|
|
4196
|
+
const initialization = inspectRspInitialization();
|
|
5370
4197
|
if (initialization)
|
|
5371
4198
|
return { ok: false, kind: "error", message: initialization, candidates: [], remainingCandidates: 0 };
|
|
5372
4199
|
try {
|
|
@@ -5401,36 +4228,36 @@ async function reopenChange(name, options) {
|
|
|
5401
4228
|
}
|
|
5402
4229
|
throw error;
|
|
5403
4230
|
}
|
|
5404
|
-
if (
|
|
4231
|
+
if (existsSync8(workRef.path))
|
|
5405
4232
|
throw new ArchiveHistoryError("open_change_exists", `open Change already exists: ${name}`);
|
|
5406
4233
|
const focusEntry = resolveFocusMarkerPath(workRef);
|
|
5407
|
-
if (
|
|
4234
|
+
if (existsSync8(focusEntry))
|
|
5408
4235
|
throw new ArchiveHistoryError("focus_marker_exists", `focus marker already exists for non-open Change: ${name}; run rsp doctor`);
|
|
5409
|
-
const archivedContent = await
|
|
4236
|
+
const archivedContent = await readFile14(record.sourcePath, "utf-8");
|
|
5410
4237
|
const reopenedContent = appendChecklistItem(
|
|
5411
4238
|
appendChecklistItem(archivedContent, "tasks", `- [ ] Resolve reopened concern: ${reason}`),
|
|
5412
4239
|
"verify",
|
|
5413
4240
|
`- [ ] Verify reopened concern: ${reason}`
|
|
5414
4241
|
);
|
|
5415
|
-
await
|
|
4242
|
+
await mkdir9(dirname6(workRef.path), { recursive: true });
|
|
5416
4243
|
let changeCreated = false;
|
|
5417
4244
|
try {
|
|
5418
|
-
await
|
|
4245
|
+
await writeFile6(workRef.path, reopenedContent, { flag: "wx" });
|
|
5419
4246
|
changeCreated = true;
|
|
5420
|
-
await
|
|
5421
|
-
await
|
|
4247
|
+
await mkdir9(FOCUS_DIR, { recursive: true });
|
|
4248
|
+
await mkdir9(dirname6(focusEntry), { recursive: true });
|
|
5422
4249
|
await writeManagedFile(focusEntry, "", "focus marker");
|
|
5423
4250
|
} catch (error) {
|
|
5424
4251
|
if (changeCreated) {
|
|
5425
4252
|
try {
|
|
5426
|
-
await
|
|
4253
|
+
await unlink8(workRef.path);
|
|
5427
4254
|
await cleanupEmptyParentDirs(workRef.path, CHANGES_DIR);
|
|
5428
4255
|
} catch {
|
|
5429
4256
|
}
|
|
5430
4257
|
}
|
|
5431
4258
|
throw error;
|
|
5432
4259
|
}
|
|
5433
|
-
return { ok: true, workRef: workRef.name, archivePath: record.path, git:
|
|
4260
|
+
return { ok: true, workRef: workRef.name, archivePath: record.path, git: existsSync8(".git") };
|
|
5434
4261
|
});
|
|
5435
4262
|
} catch (error) {
|
|
5436
4263
|
if (error instanceof ArchiveHistoryError || error instanceof WorkRefError) {
|
|
@@ -5445,15 +4272,6 @@ async function reopenChange(name, options) {
|
|
|
5445
4272
|
throw error;
|
|
5446
4273
|
}
|
|
5447
4274
|
}
|
|
5448
|
-
function inspectInitialization5() {
|
|
5449
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
5450
|
-
const design = inspectManagedFile(`${RSP_DIR}/specs/design.md`, "design Spec", { allowMissing: true });
|
|
5451
|
-
if (!rules.issue && !design.issue && rules.exists && design.exists)
|
|
5452
|
-
return void 0;
|
|
5453
|
-
const initialized = existsSync10(RSP_DIR);
|
|
5454
|
-
return `${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}
|
|
5455
|
-
${initialized ? "Run: rsp update" : "Run: rsp init"}`;
|
|
5456
|
-
}
|
|
5457
4275
|
function appendChecklistItem(content, sectionId, item) {
|
|
5458
4276
|
try {
|
|
5459
4277
|
return appendDocumentSectionItem(parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), sectionId, item);
|
|
@@ -5468,25 +4286,6 @@ function appendChecklistItem(content, sectionId, item) {
|
|
|
5468
4286
|
}
|
|
5469
4287
|
}
|
|
5470
4288
|
|
|
5471
|
-
// src/cli/presenters/commit.ts
|
|
5472
|
-
function presentCommit(result, json) {
|
|
5473
|
-
if (json) {
|
|
5474
|
-
emitJson(result);
|
|
5475
|
-
return;
|
|
5476
|
-
}
|
|
5477
|
-
if (!result.ok) {
|
|
5478
|
-
console.error(` Commit stopped: ${result.message}`);
|
|
5479
|
-
return;
|
|
5480
|
-
}
|
|
5481
|
-
console.log(` Committed: ${result.commit}`);
|
|
5482
|
-
console.log(` HEAD: ${result.headBefore ?? "(unborn)"} -> ${result.headAfter}`);
|
|
5483
|
-
console.log(` committed paths: ${result.committedPaths?.join(", ") || "none"}`);
|
|
5484
|
-
console.log(` remaining worktree paths: ${result.remainingWorktreePaths?.join(", ") || "none"}`);
|
|
5485
|
-
console.log(" stored message:");
|
|
5486
|
-
for (const line of (result.storedMessage ?? "").split("\n"))
|
|
5487
|
-
console.log(` ${line}`);
|
|
5488
|
-
}
|
|
5489
|
-
|
|
5490
4289
|
// src/cli/presenters/work.ts
|
|
5491
4290
|
function presentFailure(result) {
|
|
5492
4291
|
const label = result.kind === "usage" ? "Usage:" : "Error:";
|
|
@@ -5604,7 +4403,7 @@ function presentReopen(result) {
|
|
|
5604
4403
|
|
|
5605
4404
|
// src/cli/commands/work.ts
|
|
5606
4405
|
function createCreateCommand(deprecatedLite) {
|
|
5607
|
-
return
|
|
4406
|
+
return defineCommand5({
|
|
5608
4407
|
meta: {
|
|
5609
4408
|
name: "create",
|
|
5610
4409
|
description: "Create .rsp/changes/<name>.md [summary]"
|
|
@@ -5629,7 +4428,7 @@ function createCreateCommand(deprecatedLite) {
|
|
|
5629
4428
|
}
|
|
5630
4429
|
},
|
|
5631
4430
|
async run({ args }) {
|
|
5632
|
-
const summary =
|
|
4431
|
+
const summary = args._.slice(1).join(" ");
|
|
5633
4432
|
await executeCliCommand({
|
|
5634
4433
|
execute: () => createChange(args.name, summary, args.kind, {
|
|
5635
4434
|
issue: args.issue,
|
|
@@ -5642,7 +4441,7 @@ function createCreateCommand(deprecatedLite) {
|
|
|
5642
4441
|
}
|
|
5643
4442
|
});
|
|
5644
4443
|
}
|
|
5645
|
-
var groupCreateCommand =
|
|
4444
|
+
var groupCreateCommand = defineCommand5({
|
|
5646
4445
|
meta: {
|
|
5647
4446
|
name: "create",
|
|
5648
4447
|
description: "Create an unfocused .rsp/changes/<group>/00-brief.md"
|
|
@@ -5655,18 +4454,18 @@ var groupCreateCommand = defineCommand4({
|
|
|
5655
4454
|
}
|
|
5656
4455
|
},
|
|
5657
4456
|
async run({ args }) {
|
|
5658
|
-
const goal =
|
|
4457
|
+
const goal = args._.slice(1).join(" ");
|
|
5659
4458
|
await executeCliCommand({ execute: () => createChangeGroup(args.name, goal), present: presentGroup, exitCode: (result) => result.ok ? void 0 : 1 }, args);
|
|
5660
4459
|
}
|
|
5661
4460
|
});
|
|
5662
|
-
var groupCommand =
|
|
4461
|
+
var groupCommand = defineCommand5({
|
|
5663
4462
|
meta: {
|
|
5664
4463
|
name: "group",
|
|
5665
4464
|
description: "Create, close, and explicitly reopen shallow Change Groups"
|
|
5666
4465
|
},
|
|
5667
4466
|
subCommands: {
|
|
5668
4467
|
create: groupCreateCommand,
|
|
5669
|
-
close:
|
|
4468
|
+
close: defineCommand5({
|
|
5670
4469
|
meta: {
|
|
5671
4470
|
name: "close",
|
|
5672
4471
|
description: "Archive a completed Group Brief after all child Changes are archived"
|
|
@@ -5682,7 +4481,7 @@ var groupCommand = defineCommand4({
|
|
|
5682
4481
|
await executeCliCommand({ execute: () => closeChangeGroup(args.name), present: presentGroup, exitCode: (result) => result.ok ? void 0 : 1 }, args);
|
|
5683
4482
|
}
|
|
5684
4483
|
}),
|
|
5685
|
-
reopen:
|
|
4484
|
+
reopen: defineCommand5({
|
|
5686
4485
|
meta: {
|
|
5687
4486
|
name: "reopen",
|
|
5688
4487
|
description: "Restore one archived Group Brief while retaining history"
|
|
@@ -5709,7 +4508,7 @@ var groupCommand = defineCommand4({
|
|
|
5709
4508
|
})
|
|
5710
4509
|
}
|
|
5711
4510
|
});
|
|
5712
|
-
var archiveCommand =
|
|
4511
|
+
var archiveCommand = defineCommand5({
|
|
5713
4512
|
meta: {
|
|
5714
4513
|
name: "archive",
|
|
5715
4514
|
description: "Archive an open change into .rsp/archives/"
|
|
@@ -5733,7 +4532,7 @@ var archiveCommand = defineCommand4({
|
|
|
5733
4532
|
await executeCliCommand({ execute: () => archiveChange(args.name), present: presentArchive, exitCode: (result) => result.ok ? void 0 : 1 }, args);
|
|
5734
4533
|
}
|
|
5735
4534
|
});
|
|
5736
|
-
var reopenCommand =
|
|
4535
|
+
var reopenCommand = defineCommand5({
|
|
5737
4536
|
meta: {
|
|
5738
4537
|
name: "reopen",
|
|
5739
4538
|
description: "Restore one archived Change as focused open work while retaining history"
|
|
@@ -5758,7 +4557,7 @@ var reopenCommand = defineCommand4({
|
|
|
5758
4557
|
await executeCliCommand({ execute: () => reopenChange(args.name, { from: args.from, reason: args.reason }), present: presentReopen, exitCode: (result) => result.ok ? void 0 : 1 }, args);
|
|
5759
4558
|
}
|
|
5760
4559
|
});
|
|
5761
|
-
var focusCommand =
|
|
4560
|
+
var focusCommand = defineCommand5({
|
|
5762
4561
|
meta: {
|
|
5763
4562
|
name: "focus",
|
|
5764
4563
|
description: "Mark an existing change as currently focused in .rsp/focus.d/"
|
|
@@ -5778,7 +4577,7 @@ var focusCommand = defineCommand4({
|
|
|
5778
4577
|
await executeCliCommand({ execute: () => focusChange(args.name, { capsuleFile: args["capsule-file"] }), present: presentFocus, exitCode: (result) => result.ok ? void 0 : 1 }, args);
|
|
5779
4578
|
}
|
|
5780
4579
|
});
|
|
5781
|
-
var unfocusCommand =
|
|
4580
|
+
var unfocusCommand = defineCommand5({
|
|
5782
4581
|
meta: {
|
|
5783
4582
|
name: "unfocus",
|
|
5784
4583
|
description: "Remove a change from the current focus set in .rsp/focus.d/"
|
|
@@ -5794,30 +4593,8 @@ var unfocusCommand = defineCommand4({
|
|
|
5794
4593
|
await executeCliCommand({ execute: () => unfocusChange(args.name), present: presentFocus, exitCode: (result) => result.ok ? void 0 : 1 }, args);
|
|
5795
4594
|
}
|
|
5796
4595
|
});
|
|
5797
|
-
var commitCommand = defineCommand4({
|
|
5798
|
-
meta: {
|
|
5799
|
-
name: "commit",
|
|
5800
|
-
description: "Create one local commit from a message file on the existing staged boundary"
|
|
5801
|
-
},
|
|
5802
|
-
args: {
|
|
5803
|
-
"message-file": {
|
|
5804
|
-
type: "string",
|
|
5805
|
-
description: "Path to the prepared commit message file",
|
|
5806
|
-
required: true
|
|
5807
|
-
},
|
|
5808
|
-
...jsonArgs
|
|
5809
|
-
},
|
|
5810
|
-
async run({ args }) {
|
|
5811
|
-
await executeCliCommand({
|
|
5812
|
-
execute: () => commitFromMessageFile(args["message-file"]),
|
|
5813
|
-
present: (result) => presentCommit(result, Boolean(args.json)),
|
|
5814
|
-
exitCode: (result) => result.ok ? void 0 : 1
|
|
5815
|
-
}, args);
|
|
5816
|
-
}
|
|
5817
|
-
});
|
|
5818
4596
|
function createWorkCommands(deprecatedLite) {
|
|
5819
4597
|
return {
|
|
5820
|
-
commit: commitCommand,
|
|
5821
4598
|
create: createCreateCommand(deprecatedLite),
|
|
5822
4599
|
group: groupCommand,
|
|
5823
4600
|
focus: focusCommand,
|
|
@@ -5830,17 +4607,19 @@ function createWorkCommands(deprecatedLite) {
|
|
|
5830
4607
|
// src/cli/registry.ts
|
|
5831
4608
|
function createRootCommand(options) {
|
|
5832
4609
|
const workCommands = createWorkCommands(options.deprecatedLite);
|
|
5833
|
-
return
|
|
4610
|
+
return defineCommand6({
|
|
5834
4611
|
meta: {
|
|
5835
4612
|
name: "rsp",
|
|
5836
4613
|
version: options.version,
|
|
5837
4614
|
description: "RSP (Reliable Software Practice), a repository-native engineering workflow for humans and AI agents"
|
|
5838
4615
|
},
|
|
5839
4616
|
subCommands: {
|
|
4617
|
+
architecture: architectureCommand,
|
|
4618
|
+
view: viewCommand,
|
|
5840
4619
|
init: setupCommands.init,
|
|
5841
4620
|
add: setupCommands.add,
|
|
5842
4621
|
specs: inspectionCommands.specs,
|
|
5843
|
-
|
|
4622
|
+
config: inspectionCommands.config,
|
|
5844
4623
|
create: workCommands.create,
|
|
5845
4624
|
group: workCommands.group,
|
|
5846
4625
|
focus: workCommands.focus,
|
|
@@ -5866,9 +4645,6 @@ function isInteractiveTerminal(environment) {
|
|
|
5866
4645
|
function shouldAutoLaunchUi(args, environment) {
|
|
5867
4646
|
return args.length === 0 && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
|
|
5868
4647
|
}
|
|
5869
|
-
function shouldLaunchSkillsUi(args, environment) {
|
|
5870
|
-
return args.length === 1 && args[0] === "skills" && isInteractiveTerminal(environment) && (environment.ci === void 0 || environment.ci === "false");
|
|
5871
|
-
}
|
|
5872
4648
|
function validateUiArgs(args) {
|
|
5873
4649
|
let lang = "auto";
|
|
5874
4650
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -5908,6 +4684,7 @@ function normalizeLegacyCreateOptions(rawArgs) {
|
|
|
5908
4684
|
};
|
|
5909
4685
|
}
|
|
5910
4686
|
async function runCli(rawArgs = process.argv.slice(2)) {
|
|
4687
|
+
await guardProjectVersion(rawArgs);
|
|
5911
4688
|
const terminalEnvironment = {
|
|
5912
4689
|
stdinTty: Boolean(process.stdin.isTTY),
|
|
5913
4690
|
stdoutTty: Boolean(process.stdout.isTTY),
|
|
@@ -5915,19 +4692,6 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
5915
4692
|
ci: process.env.CI
|
|
5916
4693
|
};
|
|
5917
4694
|
const explicitUi = rawArgs[0] === "ui";
|
|
5918
|
-
if (shouldLaunchSkillsUi(rawArgs, terminalEnvironment)) {
|
|
5919
|
-
const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
|
|
5920
|
-
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);
|
|
5921
|
-
const { runSkillsTui } = await import("./entry-YAW3PYFA.mjs");
|
|
5922
|
-
const selection = await runSkillsTui(locale);
|
|
5923
|
-
if (selection.kind === "confirmed") {
|
|
5924
|
-
const result = await installPackagedSkills({ names: selection.names, force: selection.force });
|
|
5925
|
-
presentSkillInstallResult(result);
|
|
5926
|
-
} else if (selection.kind === "error") {
|
|
5927
|
-
process.exitCode = 1;
|
|
5928
|
-
}
|
|
5929
|
-
return;
|
|
5930
|
-
}
|
|
5931
4695
|
if (explicitUi || shouldAutoLaunchUi(rawArgs, terminalEnvironment)) {
|
|
5932
4696
|
const { lang } = validateUiArgs(explicitUi ? rawArgs.slice(1) : []);
|
|
5933
4697
|
if (!isInteractiveTerminal(terminalEnvironment)) {
|
|
@@ -5936,7 +4700,7 @@ async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
5936
4700
|
} else {
|
|
5937
4701
|
const { resolveUiLocale } = await import("./locale-HT44UIUP.mjs");
|
|
5938
4702
|
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);
|
|
5939
|
-
const { runTui } = await import("./entry-
|
|
4703
|
+
const { runTui } = await import("./entry-NDASTAI5.mjs");
|
|
5940
4704
|
const exitCode = await runTui(locale);
|
|
5941
4705
|
if (exitCode !== 0)
|
|
5942
4706
|
process.exitCode = exitCode;
|
|
@@ -5961,7 +4725,7 @@ async function runCliMain(rawArgs = process.argv.slice(2)) {
|
|
|
5961
4725
|
process.exitCode = 1;
|
|
5962
4726
|
}
|
|
5963
4727
|
}
|
|
5964
|
-
var isMain = process.argv[1] && fileURLToPath(import.meta.url) ===
|
|
4728
|
+
var isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve2(process.argv[1]);
|
|
5965
4729
|
if (isMain)
|
|
5966
4730
|
await runCliMain();
|
|
5967
4731
|
export {
|