@outcomeeng/spx 0.5.3 → 0.5.5
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 +17 -2
- package/dist/cli.js +2089 -1075
- package/dist/cli.js.map +1 -1
- package/package.json +5 -6
package/dist/cli.js
CHANGED
|
@@ -2,264 +2,6 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { createRequire as createRequire2 } from "module";
|
|
4
4
|
|
|
5
|
-
// src/domains/audit/paths.ts
|
|
6
|
-
import { existsSync } from "fs";
|
|
7
|
-
import { relative, resolve } from "path";
|
|
8
|
-
var AUDIT_PATH_DEFECT = {
|
|
9
|
-
ESCAPES_ROOT: "path escapes product directory",
|
|
10
|
-
MISSING_FILE: "missing file"
|
|
11
|
-
};
|
|
12
|
-
function validatePaths(verdict, productDir) {
|
|
13
|
-
const defects = [];
|
|
14
|
-
const root = resolve(productDir);
|
|
15
|
-
for (const gate of verdict.gates) {
|
|
16
|
-
for (const finding of gate.findings) {
|
|
17
|
-
checkPath(finding.spec_file, root, defects);
|
|
18
|
-
checkPath(finding.test_file, root, defects);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return defects;
|
|
22
|
-
}
|
|
23
|
-
function checkPath(filePath, root, defects) {
|
|
24
|
-
if (!filePath) return;
|
|
25
|
-
const rel = relative(root, resolve(root, filePath));
|
|
26
|
-
if (rel.startsWith("..")) {
|
|
27
|
-
defects.push(`${AUDIT_PATH_DEFECT.ESCAPES_ROOT}: ${filePath}`);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
if (!existsSync(resolve(root, filePath))) {
|
|
31
|
-
defects.push(`${AUDIT_PATH_DEFECT.MISSING_FILE}: ${filePath}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// src/domains/audit/reader.ts
|
|
36
|
-
import { readFile } from "fs/promises";
|
|
37
|
-
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
38
|
-
var AUDIT_VERDICT_VALUE = {
|
|
39
|
-
APPROVED: "APPROVED",
|
|
40
|
-
REJECT: "REJECT"
|
|
41
|
-
};
|
|
42
|
-
var AUDIT_GATE_STATUS = {
|
|
43
|
-
FAIL: "FAIL",
|
|
44
|
-
PASS: "PASS",
|
|
45
|
-
SKIPPED: "SKIPPED"
|
|
46
|
-
};
|
|
47
|
-
var AUDIT_VERDICT_XML = {
|
|
48
|
-
ROOT: "audit_verdict",
|
|
49
|
-
HEADER: "header",
|
|
50
|
-
SPEC_NODE: "spec_node",
|
|
51
|
-
VERDICT: "verdict",
|
|
52
|
-
TIMESTAMP: "timestamp",
|
|
53
|
-
GATES: "gates",
|
|
54
|
-
GATE: "gate",
|
|
55
|
-
NAME: "name",
|
|
56
|
-
STATUS: "status",
|
|
57
|
-
SKIPPED_REASON: "skipped_reason",
|
|
58
|
-
FINDINGS: "findings",
|
|
59
|
-
FINDING: "finding",
|
|
60
|
-
COUNT: "count",
|
|
61
|
-
PARSED_COUNT: "@_count",
|
|
62
|
-
SPEC_FILE: "spec_file",
|
|
63
|
-
TEST_FILE: "test_file"
|
|
64
|
-
};
|
|
65
|
-
var ARRAY_TAGS = /* @__PURE__ */ new Set([AUDIT_VERDICT_XML.GATE, AUDIT_VERDICT_XML.FINDING]);
|
|
66
|
-
var PARSER_OPTIONS = {
|
|
67
|
-
ignoreAttributes: false,
|
|
68
|
-
attributeNamePrefix: "@_",
|
|
69
|
-
parseTagValue: false,
|
|
70
|
-
parseAttributeValue: false,
|
|
71
|
-
isArray: (name) => ARRAY_TAGS.has(name)
|
|
72
|
-
};
|
|
73
|
-
async function readVerdictFile(filePath) {
|
|
74
|
-
let xml;
|
|
75
|
-
try {
|
|
76
|
-
xml = await readFile(filePath, "utf-8");
|
|
77
|
-
} catch (cause) {
|
|
78
|
-
throw new Error(`Failed to read verdict file: ${filePath}`, { cause });
|
|
79
|
-
}
|
|
80
|
-
const validation = XMLValidator.validate(xml);
|
|
81
|
-
if (validation !== true) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
`Malformed XML in verdict file: ${filePath}: ${validation.err.msg}`
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
const parser = new XMLParser(PARSER_OPTIONS);
|
|
87
|
-
const parsed = parser.parse(xml);
|
|
88
|
-
return buildVerdict(parsed);
|
|
89
|
-
}
|
|
90
|
-
function isObject(value) {
|
|
91
|
-
return typeof value === "object" && value !== null;
|
|
92
|
-
}
|
|
93
|
-
function asString(value) {
|
|
94
|
-
return typeof value === "string" ? value : void 0;
|
|
95
|
-
}
|
|
96
|
-
function buildVerdict(parsed) {
|
|
97
|
-
const rootUnknown = parsed[AUDIT_VERDICT_XML.ROOT];
|
|
98
|
-
if (!isObject(rootUnknown)) {
|
|
99
|
-
return { gates: [] };
|
|
100
|
-
}
|
|
101
|
-
const headerUnknown = rootUnknown[AUDIT_VERDICT_XML.HEADER];
|
|
102
|
-
const gatesUnknown = rootUnknown[AUDIT_VERDICT_XML.GATES];
|
|
103
|
-
return {
|
|
104
|
-
header: isObject(headerUnknown) ? buildHeader(headerUnknown) : void 0,
|
|
105
|
-
gates: isObject(gatesUnknown) ? buildGates(gatesUnknown) : []
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
function buildHeader(raw) {
|
|
109
|
-
return {
|
|
110
|
-
spec_node: asString(raw[AUDIT_VERDICT_XML.SPEC_NODE]),
|
|
111
|
-
verdict: asString(raw[AUDIT_VERDICT_XML.VERDICT]),
|
|
112
|
-
timestamp: asString(raw[AUDIT_VERDICT_XML.TIMESTAMP])
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
function buildGates(raw) {
|
|
116
|
-
const gatesUnknown = raw[AUDIT_VERDICT_XML.GATE];
|
|
117
|
-
if (!Array.isArray(gatesUnknown)) return [];
|
|
118
|
-
return gatesUnknown.filter(isObject).map(buildGate);
|
|
119
|
-
}
|
|
120
|
-
function buildGate(raw) {
|
|
121
|
-
const findingsUnknown = raw[AUDIT_VERDICT_XML.FINDINGS];
|
|
122
|
-
return {
|
|
123
|
-
name: asString(raw[AUDIT_VERDICT_XML.NAME]),
|
|
124
|
-
status: asString(raw[AUDIT_VERDICT_XML.STATUS]),
|
|
125
|
-
skipped_reason: asString(raw[AUDIT_VERDICT_XML.SKIPPED_REASON]),
|
|
126
|
-
count: isObject(findingsUnknown) ? asString(findingsUnknown[AUDIT_VERDICT_XML.PARSED_COUNT]) : void 0,
|
|
127
|
-
findings: isObject(findingsUnknown) ? buildFindings(findingsUnknown) : []
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
function buildFindings(raw) {
|
|
131
|
-
const findingsUnknown = raw[AUDIT_VERDICT_XML.FINDING];
|
|
132
|
-
if (!Array.isArray(findingsUnknown)) return [];
|
|
133
|
-
return findingsUnknown.filter(isObject).map(buildFinding);
|
|
134
|
-
}
|
|
135
|
-
function buildFinding(raw) {
|
|
136
|
-
return {
|
|
137
|
-
spec_file: asString(raw[AUDIT_VERDICT_XML.SPEC_FILE]),
|
|
138
|
-
test_file: asString(raw[AUDIT_VERDICT_XML.TEST_FILE])
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// src/domains/audit/semantic.ts
|
|
143
|
-
function validateSemantics(verdict) {
|
|
144
|
-
const defects = [];
|
|
145
|
-
const hasFail = verdict.gates.some((g) => g.status === AUDIT_GATE_STATUS.FAIL);
|
|
146
|
-
const hasSkipped = verdict.gates.some((g) => g.status === AUDIT_GATE_STATUS.SKIPPED);
|
|
147
|
-
if (verdict.header?.verdict === AUDIT_VERDICT_VALUE.APPROVED && (hasFail || hasSkipped)) {
|
|
148
|
-
defects.push("incoherent verdict: APPROVED but at least one gate is not PASS");
|
|
149
|
-
} else if (verdict.header?.verdict === AUDIT_VERDICT_VALUE.REJECT && !hasFail && !hasSkipped) {
|
|
150
|
-
defects.push("incoherent verdict: REJECT but all gates are PASS");
|
|
151
|
-
}
|
|
152
|
-
for (const gate of verdict.gates) {
|
|
153
|
-
const label = gate.name ? `gate "${gate.name}"` : "gate";
|
|
154
|
-
if (gate.status === AUDIT_GATE_STATUS.FAIL && gate.findings.length === 0) {
|
|
155
|
-
defects.push(`failed gate has no findings: ${label}`);
|
|
156
|
-
}
|
|
157
|
-
if (gate.status === AUDIT_GATE_STATUS.SKIPPED && !gate.skipped_reason) {
|
|
158
|
-
defects.push(`skipped gate missing reason: ${label}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return defects;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// src/domains/audit/structural.ts
|
|
165
|
-
var VALID_VERDICTS = new Set(Object.values(AUDIT_VERDICT_VALUE));
|
|
166
|
-
var VALID_GATE_STATUSES = new Set(Object.values(AUDIT_GATE_STATUS));
|
|
167
|
-
var STRUCTURAL_DEFECT_TEXT = {
|
|
168
|
-
MISSING_REQUIRED_ELEMENT: "missing required element",
|
|
169
|
-
INVALID_ENUM_VALUE: "invalid enum value"
|
|
170
|
-
};
|
|
171
|
-
function validateStructure(verdict) {
|
|
172
|
-
const defects = [];
|
|
173
|
-
if (!verdict.header) {
|
|
174
|
-
defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <header>`);
|
|
175
|
-
return defects;
|
|
176
|
-
}
|
|
177
|
-
if (!verdict.header.spec_node) {
|
|
178
|
-
defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <spec_node>`);
|
|
179
|
-
}
|
|
180
|
-
if (!verdict.header.verdict) {
|
|
181
|
-
defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <verdict> in <header>`);
|
|
182
|
-
} else if (!VALID_VERDICTS.has(verdict.header.verdict)) {
|
|
183
|
-
defects.push(
|
|
184
|
-
`${STRUCTURAL_DEFECT_TEXT.INVALID_ENUM_VALUE}: verdict "${verdict.header.verdict}" is not one of APPROVED, REJECT`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
if (!verdict.header.timestamp) {
|
|
188
|
-
defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: <timestamp>`);
|
|
189
|
-
}
|
|
190
|
-
if (verdict.gates.length === 0) {
|
|
191
|
-
defects.push(`${STRUCTURAL_DEFECT_TEXT.MISSING_REQUIRED_ELEMENT}: at least one <gate> in <gates>`);
|
|
192
|
-
}
|
|
193
|
-
for (const gate of verdict.gates) {
|
|
194
|
-
const label = gate.name ? `gate "${gate.name}"` : "gate";
|
|
195
|
-
if (gate.status !== void 0 && !VALID_GATE_STATUSES.has(gate.status)) {
|
|
196
|
-
defects.push(
|
|
197
|
-
`${STRUCTURAL_DEFECT_TEXT.INVALID_ENUM_VALUE}: ${label} status "${gate.status}" is not one of PASS, FAIL, SKIPPED`
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
if (gate.count !== void 0) {
|
|
201
|
-
const declared = parseInt(gate.count, 10);
|
|
202
|
-
const actual = gate.findings.length;
|
|
203
|
-
if (isNaN(declared) || declared !== actual) {
|
|
204
|
-
defects.push(
|
|
205
|
-
`count mismatch: ${label} declares count="${gate.count}" but has ${actual} finding(s)`
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return defects;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// src/domains/audit/verify.ts
|
|
214
|
-
async function runVerifyPipeline(filePath, productDir) {
|
|
215
|
-
let verdict;
|
|
216
|
-
try {
|
|
217
|
-
verdict = await readVerdictFile(filePath);
|
|
218
|
-
} catch (error) {
|
|
219
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
-
return { lines: [`reader: ${message}`], exitCode: 1 };
|
|
221
|
-
}
|
|
222
|
-
const structuralDefects = validateStructure(verdict);
|
|
223
|
-
if (structuralDefects.length > 0) {
|
|
224
|
-
return { lines: structuralDefects.map((d) => `structural: ${d}`), exitCode: 1 };
|
|
225
|
-
}
|
|
226
|
-
const semanticDefects = validateSemantics(verdict);
|
|
227
|
-
if (semanticDefects.length > 0) {
|
|
228
|
-
return { lines: semanticDefects.map((d) => `semantic: ${d}`), exitCode: 1 };
|
|
229
|
-
}
|
|
230
|
-
const pathDefects = validatePaths(verdict, productDir);
|
|
231
|
-
if (pathDefects.length > 0) {
|
|
232
|
-
return { lines: pathDefects.map((d) => `paths: ${d}`), exitCode: 1 };
|
|
233
|
-
}
|
|
234
|
-
return { lines: [], exitCode: 0, verdict: verdict.header?.verdict };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// src/commands/audit/verify.ts
|
|
238
|
-
async function runVerifyCommand(filePath, productDir, writeLine) {
|
|
239
|
-
const result = await runVerifyPipeline(filePath, productDir);
|
|
240
|
-
if (result.exitCode === 0) {
|
|
241
|
-
writeLine(result.verdict ?? "");
|
|
242
|
-
} else {
|
|
243
|
-
for (const line of result.lines) {
|
|
244
|
-
writeLine(line);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return result.exitCode;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// src/interfaces/cli/audit.ts
|
|
251
|
-
var auditDomain = {
|
|
252
|
-
name: "audit",
|
|
253
|
-
description: "Audit verdict verification",
|
|
254
|
-
register: (program2) => {
|
|
255
|
-
const auditCmd = program2.command("audit").description("Audit verdict verification");
|
|
256
|
-
auditCmd.command("verify <file>").description("Verify an audit verdict XML file").action(async (file) => {
|
|
257
|
-
const exitCode = await runVerifyCommand(file, process.cwd(), console.log);
|
|
258
|
-
process.exit(exitCode);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
|
|
263
5
|
// src/commands/claude/init.ts
|
|
264
6
|
import { execa } from "execa";
|
|
265
7
|
async function initCommand(options = {}) {
|
|
@@ -891,20 +633,870 @@ function registerClaudeCommands(claudeCmd) {
|
|
|
891
633
|
process.exit(1);
|
|
892
634
|
}
|
|
893
635
|
}
|
|
894
|
-
);
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
var claudeDomain = {
|
|
639
|
+
name: "claude",
|
|
640
|
+
description: "Manage Claude Code settings and plugins",
|
|
641
|
+
register: (program2) => {
|
|
642
|
+
const claudeCmd = program2.command("claude").description("Manage Claude Code settings and plugins");
|
|
643
|
+
registerClaudeCommands(claudeCmd);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// src/domains/compact/index.ts
|
|
648
|
+
import { join as join3 } from "path";
|
|
649
|
+
|
|
650
|
+
// src/domains/session/agent-session.ts
|
|
651
|
+
import { createHash } from "crypto";
|
|
652
|
+
var AGENT_SESSION_ENV = {
|
|
653
|
+
CLAUDE_SESSION_ID: "CLAUDE_SESSION_ID",
|
|
654
|
+
CODEX_THREAD_ID: "CODEX_THREAD_ID"
|
|
655
|
+
};
|
|
656
|
+
var AGENT_SESSION_TOKEN_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
657
|
+
var AGENT_SESSION_TOKEN_UNSAFE_PATTERN = /[^A-Za-z0-9_-]+/g;
|
|
658
|
+
var AGENT_SESSION_TOKEN_EDGE_SEPARATOR_PATTERN = /^-+|-+$/g;
|
|
659
|
+
var AGENT_SESSION_TOKEN_SEPARATOR = "-";
|
|
660
|
+
var AGENT_SESSION_TOKEN_HASH_ALGORITHM = "sha256";
|
|
661
|
+
var AGENT_SESSION_TOKEN_HASH_ENCODING = "hex";
|
|
662
|
+
var AGENT_SESSION_TOKEN_HASH_LENGTH = 12;
|
|
663
|
+
var EMPTY_TOKEN = "";
|
|
664
|
+
function nonEmptyEnvValue(value) {
|
|
665
|
+
return value === void 0 || value.length === 0 ? void 0 : value;
|
|
666
|
+
}
|
|
667
|
+
function hashAgentSessionToken(value) {
|
|
668
|
+
return createHash(AGENT_SESSION_TOKEN_HASH_ALGORITHM).update(value).digest(AGENT_SESSION_TOKEN_HASH_ENCODING).slice(0, AGENT_SESSION_TOKEN_HASH_LENGTH);
|
|
669
|
+
}
|
|
670
|
+
function normalizeAgentSessionToken(value) {
|
|
671
|
+
if (AGENT_SESSION_TOKEN_PATTERN.test(value)) return value;
|
|
672
|
+
const normalized = value.replace(AGENT_SESSION_TOKEN_UNSAFE_PATTERN, AGENT_SESSION_TOKEN_SEPARATOR).replace(AGENT_SESSION_TOKEN_EDGE_SEPARATOR_PATTERN, EMPTY_TOKEN);
|
|
673
|
+
const hash = hashAgentSessionToken(value);
|
|
674
|
+
return normalized.length === 0 ? hash : `${normalized}${AGENT_SESSION_TOKEN_SEPARATOR}${hash}`;
|
|
675
|
+
}
|
|
676
|
+
function resolveAgentSessionId(env) {
|
|
677
|
+
const raw = nonEmptyEnvValue(env[AGENT_SESSION_ENV.CLAUDE_SESSION_ID]) ?? nonEmptyEnvValue(env[AGENT_SESSION_ENV.CODEX_THREAD_ID]);
|
|
678
|
+
return raw === void 0 ? void 0 : normalizeAgentSessionToken(raw);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/lib/state-store/index.ts
|
|
682
|
+
import { createHash as createHash2, randomBytes as nodeRandomBytes } from "crypto";
|
|
683
|
+
import {
|
|
684
|
+
appendFile as nodeAppendFile,
|
|
685
|
+
mkdir as nodeMkdir,
|
|
686
|
+
readdir as nodeReaddir,
|
|
687
|
+
readFile as nodeReadFile,
|
|
688
|
+
writeFile as nodeWriteFile
|
|
689
|
+
} from "fs/promises";
|
|
690
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
691
|
+
|
|
692
|
+
// src/git/root.ts
|
|
693
|
+
import { execa as execa2 } from "execa";
|
|
694
|
+
import { basename, dirname, isAbsolute, join, resolve } from "path";
|
|
695
|
+
|
|
696
|
+
// src/git/environment.ts
|
|
697
|
+
function withoutGitEnvironment(env) {
|
|
698
|
+
const cleaned = { ...env };
|
|
699
|
+
for (const key of Object.keys(cleaned)) {
|
|
700
|
+
if (key.startsWith("GIT_")) {
|
|
701
|
+
delete cleaned[key];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return cleaned;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/git/root.ts
|
|
708
|
+
var defaultGitDependencies = {
|
|
709
|
+
execa: async (command, args, options) => {
|
|
710
|
+
const result = await execa2(command, args, {
|
|
711
|
+
...options,
|
|
712
|
+
env: withoutGitEnvironment(process.env),
|
|
713
|
+
extendEnv: false
|
|
714
|
+
});
|
|
715
|
+
return {
|
|
716
|
+
exitCode: result.exitCode ?? 0,
|
|
717
|
+
stdout: typeof result.stdout === "string" ? result.stdout : String(result.stdout),
|
|
718
|
+
stderr: typeof result.stderr === "string" ? result.stderr : String(result.stderr)
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository; resolving session storage relative to the current directory.";
|
|
723
|
+
var GIT_ROOT_COMMAND = {
|
|
724
|
+
EXECUTABLE: "git",
|
|
725
|
+
REV_PARSE: "rev-parse",
|
|
726
|
+
ABBREV_REF: "--abbrev-ref",
|
|
727
|
+
GIT_COMMON_DIR: "--git-common-dir",
|
|
728
|
+
HEAD: "HEAD",
|
|
729
|
+
SHOW_TOPLEVEL: "--show-toplevel",
|
|
730
|
+
SYMBOLIC_REF: "symbolic-ref",
|
|
731
|
+
SHOW_REF: "show-ref",
|
|
732
|
+
VERIFY: "--verify",
|
|
733
|
+
QUIET: "--quiet",
|
|
734
|
+
SHORT: "--short",
|
|
735
|
+
ORIGIN_HEAD_REF: "refs/remotes/origin/HEAD",
|
|
736
|
+
STATUS: "status",
|
|
737
|
+
WORKTREE: "worktree",
|
|
738
|
+
LIST: "list",
|
|
739
|
+
PORCELAIN: "--porcelain",
|
|
740
|
+
PATH_FORMAT_ABSOLUTE: "--path-format=absolute",
|
|
741
|
+
CONFIG: "config",
|
|
742
|
+
CONFIG_GET: "--get",
|
|
743
|
+
CONFIG_TYPE_BOOL: "--type=bool",
|
|
744
|
+
CORE_BARE_KEY: "core.bare",
|
|
745
|
+
REMOTE: "remote",
|
|
746
|
+
GET_URL: "get-url",
|
|
747
|
+
ORIGIN: "origin"
|
|
748
|
+
};
|
|
749
|
+
var GIT_URL_SUFFIX = ".git";
|
|
750
|
+
var GIT_DIR_BASENAME = ".git";
|
|
751
|
+
var ORIGIN_REF_PREFIX = "origin/";
|
|
752
|
+
var REMOTE_ORIGIN_REF_PREFIX = "refs/remotes/origin/";
|
|
753
|
+
var GIT_CORE_BARE_TRUE = "true";
|
|
754
|
+
var GIT_SHOW_TOPLEVEL_ARGS = [
|
|
755
|
+
GIT_ROOT_COMMAND.REV_PARSE,
|
|
756
|
+
GIT_ROOT_COMMAND.SHOW_TOPLEVEL
|
|
757
|
+
];
|
|
758
|
+
var GIT_COMMON_DIR_ARGS = [
|
|
759
|
+
GIT_ROOT_COMMAND.REV_PARSE,
|
|
760
|
+
GIT_ROOT_COMMAND.PATH_FORMAT_ABSOLUTE,
|
|
761
|
+
GIT_ROOT_COMMAND.GIT_COMMON_DIR
|
|
762
|
+
];
|
|
763
|
+
var GIT_CORE_BARE_ARGS = [
|
|
764
|
+
GIT_ROOT_COMMAND.CONFIG,
|
|
765
|
+
GIT_ROOT_COMMAND.CONFIG_GET,
|
|
766
|
+
GIT_ROOT_COMMAND.CONFIG_TYPE_BOOL,
|
|
767
|
+
GIT_ROOT_COMMAND.CORE_BARE_KEY
|
|
768
|
+
];
|
|
769
|
+
var GIT_REMOTE_GET_URL_ORIGIN_ARGS = [
|
|
770
|
+
GIT_ROOT_COMMAND.REMOTE,
|
|
771
|
+
GIT_ROOT_COMMAND.GET_URL,
|
|
772
|
+
GIT_ROOT_COMMAND.ORIGIN
|
|
773
|
+
];
|
|
774
|
+
var GIT_CURRENT_BRANCH_ARGS = [
|
|
775
|
+
GIT_ROOT_COMMAND.REV_PARSE,
|
|
776
|
+
GIT_ROOT_COMMAND.ABBREV_REF,
|
|
777
|
+
GIT_ROOT_COMMAND.HEAD
|
|
778
|
+
];
|
|
779
|
+
var GIT_HEAD_SHA_ARGS = [
|
|
780
|
+
GIT_ROOT_COMMAND.REV_PARSE,
|
|
781
|
+
GIT_ROOT_COMMAND.HEAD
|
|
782
|
+
];
|
|
783
|
+
var GIT_ORIGIN_HEAD_REF_ARGS = [
|
|
784
|
+
GIT_ROOT_COMMAND.SYMBOLIC_REF,
|
|
785
|
+
GIT_ROOT_COMMAND.SHORT,
|
|
786
|
+
GIT_ROOT_COMMAND.ORIGIN_HEAD_REF
|
|
787
|
+
];
|
|
788
|
+
var GIT_STATUS_PORCELAIN_ARGS = [
|
|
789
|
+
GIT_ROOT_COMMAND.STATUS,
|
|
790
|
+
GIT_ROOT_COMMAND.PORCELAIN
|
|
791
|
+
];
|
|
792
|
+
var GIT_WORKTREE_LIST_PORCELAIN_ARGS = [
|
|
793
|
+
GIT_ROOT_COMMAND.WORKTREE,
|
|
794
|
+
GIT_ROOT_COMMAND.LIST,
|
|
795
|
+
GIT_ROOT_COMMAND.PORCELAIN
|
|
796
|
+
];
|
|
797
|
+
var GIT_WORKTREE_PORCELAIN_ROOT_PREFIX = "worktree ";
|
|
798
|
+
var GIT_WORKTREE_PORCELAIN_BARE_LINE = "bare";
|
|
799
|
+
var GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE = "prunable";
|
|
800
|
+
var GIT_WORKTREE_PORCELAIN_PRUNABLE_PREFIX = `${GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE} `;
|
|
801
|
+
var TRAILING_PATH_SEPARATORS_PATTERN = /[\\/]+$/;
|
|
802
|
+
async function detectWorktreeProductRoot(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
803
|
+
try {
|
|
804
|
+
const result = await deps.execa(
|
|
805
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
806
|
+
[...GIT_SHOW_TOPLEVEL_ARGS],
|
|
807
|
+
{ cwd, reject: false }
|
|
808
|
+
);
|
|
809
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
810
|
+
return {
|
|
811
|
+
productDir: extractStdout(result.stdout),
|
|
812
|
+
isGitRepo: true
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
productDir: cwd,
|
|
817
|
+
isGitRepo: false,
|
|
818
|
+
warning: NOT_GIT_REPO_WARNING
|
|
819
|
+
};
|
|
820
|
+
} catch {
|
|
821
|
+
return {
|
|
822
|
+
productDir: cwd,
|
|
823
|
+
isGitRepo: false,
|
|
824
|
+
warning: NOT_GIT_REPO_WARNING
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function extractStdout(stdout) {
|
|
829
|
+
if (!stdout) return "";
|
|
830
|
+
const str = typeof stdout === "string" ? stdout : String(stdout);
|
|
831
|
+
return str.trim().replace(TRAILING_PATH_SEPARATORS_PATTERN, "");
|
|
832
|
+
}
|
|
833
|
+
function trimStdout(stdout) {
|
|
834
|
+
if (!stdout) return "";
|
|
835
|
+
const str = typeof stdout === "string" ? stdout : String(stdout);
|
|
836
|
+
return str.trim();
|
|
837
|
+
}
|
|
838
|
+
async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
839
|
+
try {
|
|
840
|
+
const toplevelResult = await deps.execa(
|
|
841
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
842
|
+
[...GIT_SHOW_TOPLEVEL_ARGS],
|
|
843
|
+
{ cwd, reject: false }
|
|
844
|
+
);
|
|
845
|
+
if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) {
|
|
846
|
+
return {
|
|
847
|
+
productDir: cwd,
|
|
848
|
+
isGitRepo: false,
|
|
849
|
+
warning: NOT_GIT_REPO_WARNING,
|
|
850
|
+
worktreeRoot: cwd
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
const toplevel = extractStdout(toplevelResult.stdout);
|
|
854
|
+
const commonDirResult = await deps.execa(
|
|
855
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
856
|
+
[...GIT_COMMON_DIR_ARGS],
|
|
857
|
+
{ cwd, reject: false }
|
|
858
|
+
);
|
|
859
|
+
if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
|
|
860
|
+
return {
|
|
861
|
+
productDir: toplevel,
|
|
862
|
+
isGitRepo: true,
|
|
863
|
+
worktreeRoot: toplevel
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
const commonDir = extractStdout(commonDirResult.stdout);
|
|
867
|
+
const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve(toplevel, commonDir);
|
|
868
|
+
const gitCommonDirProductRoot = dirname(absoluteCommonDir);
|
|
869
|
+
return {
|
|
870
|
+
productDir: gitCommonDirProductRoot,
|
|
871
|
+
isGitRepo: true,
|
|
872
|
+
worktreeRoot: toplevel
|
|
873
|
+
};
|
|
874
|
+
} catch {
|
|
875
|
+
return {
|
|
876
|
+
productDir: cwd,
|
|
877
|
+
isGitRepo: false,
|
|
878
|
+
warning: NOT_GIT_REPO_WARNING,
|
|
879
|
+
worktreeRoot: cwd
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async function resolveDefaultBranch(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
884
|
+
const result = await deps.execa(
|
|
885
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
886
|
+
[...GIT_ORIGIN_HEAD_REF_ARGS],
|
|
887
|
+
{ cwd, reject: false }
|
|
888
|
+
);
|
|
889
|
+
if (result.exitCode !== 0) return null;
|
|
890
|
+
const ref = extractStdout(result.stdout);
|
|
891
|
+
if (!ref.startsWith(ORIGIN_REF_PREFIX)) return null;
|
|
892
|
+
const branch = ref.slice(ORIGIN_REF_PREFIX.length);
|
|
893
|
+
return branch.length === 0 ? null : branch;
|
|
894
|
+
}
|
|
895
|
+
async function getCurrentBranch(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
896
|
+
const result = await deps.execa(
|
|
897
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
898
|
+
[...GIT_CURRENT_BRANCH_ARGS],
|
|
899
|
+
{ cwd, reject: false }
|
|
900
|
+
);
|
|
901
|
+
if (result.exitCode !== 0) return null;
|
|
902
|
+
const branch = extractStdout(result.stdout);
|
|
903
|
+
if (branch.length === 0 || branch === GIT_ROOT_COMMAND.HEAD) return null;
|
|
904
|
+
return branch;
|
|
905
|
+
}
|
|
906
|
+
async function getHeadSha(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
907
|
+
const result = await deps.execa(
|
|
908
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
909
|
+
[...GIT_HEAD_SHA_ARGS],
|
|
910
|
+
{ cwd, reject: false }
|
|
911
|
+
);
|
|
912
|
+
if (result.exitCode !== 0) return null;
|
|
913
|
+
const sha = extractStdout(result.stdout);
|
|
914
|
+
return sha.length === 0 ? null : sha;
|
|
915
|
+
}
|
|
916
|
+
async function resolveRefSha(ref, cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
917
|
+
const result = await deps.execa(
|
|
918
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
919
|
+
[GIT_ROOT_COMMAND.REV_PARSE, ref],
|
|
920
|
+
{ cwd, reject: false }
|
|
921
|
+
);
|
|
922
|
+
if (result.exitCode !== 0) return null;
|
|
923
|
+
const sha = extractStdout(result.stdout);
|
|
924
|
+
return sha.length === 0 ? null : sha;
|
|
925
|
+
}
|
|
926
|
+
async function originBranchExists(branch, cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
927
|
+
if (branch === GIT_ROOT_COMMAND.HEAD) return false;
|
|
928
|
+
const result = await deps.execa(
|
|
929
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
930
|
+
[
|
|
931
|
+
GIT_ROOT_COMMAND.SHOW_REF,
|
|
932
|
+
GIT_ROOT_COMMAND.VERIFY,
|
|
933
|
+
GIT_ROOT_COMMAND.QUIET,
|
|
934
|
+
`${REMOTE_ORIGIN_REF_PREFIX}${branch}`
|
|
935
|
+
],
|
|
936
|
+
{ cwd, reject: false }
|
|
937
|
+
);
|
|
938
|
+
return result.exitCode === 0;
|
|
939
|
+
}
|
|
940
|
+
async function isWorkingTreeClean(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
941
|
+
const result = await deps.execa(
|
|
942
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
943
|
+
[...GIT_STATUS_PORCELAIN_ARGS],
|
|
944
|
+
{ cwd, reject: false }
|
|
945
|
+
);
|
|
946
|
+
if (result.exitCode !== 0) return false;
|
|
947
|
+
return extractStdout(result.stdout).length === 0;
|
|
948
|
+
}
|
|
949
|
+
function repositoryName(originUrl) {
|
|
950
|
+
if (originUrl === null) return null;
|
|
951
|
+
const trimmed = originUrl.trim().replace(TRAILING_PATH_SEPARATORS_PATTERN, "");
|
|
952
|
+
const lastSeparator = Math.max(
|
|
953
|
+
trimmed.lastIndexOf("/"),
|
|
954
|
+
trimmed.lastIndexOf("\\"),
|
|
955
|
+
trimmed.lastIndexOf(":")
|
|
956
|
+
);
|
|
957
|
+
const segment = trimmed.slice(lastSeparator + 1);
|
|
958
|
+
const name = segment.endsWith(GIT_URL_SUFFIX) ? segment.slice(0, -GIT_URL_SUFFIX.length) : segment;
|
|
959
|
+
return name.length === 0 ? null : name;
|
|
960
|
+
}
|
|
961
|
+
function normalizeGitPath(path6) {
|
|
962
|
+
return path6.trim().replace(TRAILING_PATH_SEPARATORS_PATTERN, "");
|
|
963
|
+
}
|
|
964
|
+
function normalizedGitPathKey(path6) {
|
|
965
|
+
return normalizeGitPath(path6).replace(/\\/g, "/");
|
|
966
|
+
}
|
|
967
|
+
function isObservedWorktreeRoot(worktreeRoots, candidate) {
|
|
968
|
+
const candidateKey = normalizedGitPathKey(candidate);
|
|
969
|
+
return worktreeRoots.some((root) => normalizedGitPathKey(root) === candidateKey);
|
|
970
|
+
}
|
|
971
|
+
function isPrunableWorktreeRecordLine(line) {
|
|
972
|
+
return line === GIT_WORKTREE_PORCELAIN_PRUNABLE_LINE || line.startsWith(GIT_WORKTREE_PORCELAIN_PRUNABLE_PREFIX);
|
|
973
|
+
}
|
|
974
|
+
function parseWorktreeRoots(stdout) {
|
|
975
|
+
const roots = [];
|
|
976
|
+
for (const record of stdout.split(/\n\n+/)) {
|
|
977
|
+
const lines = record.split("\n");
|
|
978
|
+
if (lines.includes(GIT_WORKTREE_PORCELAIN_BARE_LINE) || lines.some(isPrunableWorktreeRecordLine)) continue;
|
|
979
|
+
const rootLine = lines.find((line) => line.startsWith(GIT_WORKTREE_PORCELAIN_ROOT_PREFIX));
|
|
980
|
+
if (rootLine === void 0) continue;
|
|
981
|
+
const root = normalizeGitPath(rootLine.slice(GIT_WORKTREE_PORCELAIN_ROOT_PREFIX.length));
|
|
982
|
+
if (root.length > 0) roots.push(root);
|
|
983
|
+
}
|
|
984
|
+
return roots;
|
|
985
|
+
}
|
|
986
|
+
function observedWorktreeRoots(worktreeListResult) {
|
|
987
|
+
if (worktreeListResult.exitCode !== 0) return [];
|
|
988
|
+
return [...new Set(parseWorktreeRoots(worktreeListResult.stdout))];
|
|
989
|
+
}
|
|
990
|
+
function isMainCheckout(facts) {
|
|
991
|
+
const commonDirParent = dirname(facts.commonDir);
|
|
992
|
+
if (!facts.commonDirIsBare) return commonDirParent === facts.worktreeRoot;
|
|
993
|
+
if (commonDirParent !== dirname(facts.worktreeRoot)) return false;
|
|
994
|
+
const name = repositoryName(facts.originUrl);
|
|
995
|
+
return name !== null && basename(facts.worktreeRoot) === name && facts.worktreeListRead && isObservedWorktreeRoot(facts.worktreeRoots, facts.worktreeRoot);
|
|
996
|
+
}
|
|
997
|
+
function mainCheckoutPath(facts) {
|
|
998
|
+
const commonDirParent = dirname(facts.commonDir);
|
|
999
|
+
if (!facts.commonDirIsBare) return commonDirParent;
|
|
1000
|
+
const name = repositoryName(facts.originUrl);
|
|
1001
|
+
if (name === null) return null;
|
|
1002
|
+
if (!facts.worktreeListRead) return null;
|
|
1003
|
+
const candidate = join(commonDirParent, name);
|
|
1004
|
+
return isObservedWorktreeRoot(facts.worktreeRoots, candidate) ? candidate : null;
|
|
1005
|
+
}
|
|
1006
|
+
async function gatherGitFacts(cwd = process.cwd(), deps = defaultGitDependencies) {
|
|
1007
|
+
try {
|
|
1008
|
+
const [toplevelResult, commonDirResult, originResult, worktreeListResult] = await Promise.all([
|
|
1009
|
+
deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_SHOW_TOPLEVEL_ARGS], { cwd, reject: false }),
|
|
1010
|
+
deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_COMMON_DIR_ARGS], { cwd, reject: false }),
|
|
1011
|
+
deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_REMOTE_GET_URL_ORIGIN_ARGS], { cwd, reject: false }),
|
|
1012
|
+
deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_WORKTREE_LIST_PORCELAIN_ARGS], { cwd, reject: false })
|
|
1013
|
+
]);
|
|
1014
|
+
if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) return null;
|
|
1015
|
+
const worktreeRoot = extractStdout(toplevelResult.stdout);
|
|
1016
|
+
const worktreeListRead = worktreeListResult.exitCode === 0;
|
|
1017
|
+
const worktreeRoots = observedWorktreeRoots(worktreeListResult);
|
|
1018
|
+
const originUrl = originResult.exitCode === 0 && originResult.stdout ? trimStdout(originResult.stdout) : null;
|
|
1019
|
+
if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
|
|
1020
|
+
return {
|
|
1021
|
+
worktreeRoot,
|
|
1022
|
+
worktreeRoots,
|
|
1023
|
+
worktreeListRead,
|
|
1024
|
+
commonDir: join(worktreeRoot, GIT_DIR_BASENAME),
|
|
1025
|
+
commonDirIsBare: false,
|
|
1026
|
+
originUrl
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
const rawCommonDir = extractStdout(commonDirResult.stdout);
|
|
1030
|
+
const commonDir = isAbsolute(rawCommonDir) ? rawCommonDir : resolve(worktreeRoot, rawCommonDir);
|
|
1031
|
+
const bareResult = await deps.execa(
|
|
1032
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
1033
|
+
[...GIT_CORE_BARE_ARGS],
|
|
1034
|
+
{ cwd, reject: false }
|
|
1035
|
+
);
|
|
1036
|
+
const commonDirIsBare = bareResult.exitCode === 0 && extractStdout(bareResult.stdout) === GIT_CORE_BARE_TRUE;
|
|
1037
|
+
return { worktreeRoot, worktreeRoots, worktreeListRead, commonDir, commonDirIsBare, originUrl };
|
|
1038
|
+
} catch {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/lib/state-store/index.ts
|
|
1044
|
+
var STATE_STORE_PATH = {
|
|
1045
|
+
SPX_DIR: ".spx",
|
|
1046
|
+
BRANCH_SCOPE: "branch",
|
|
1047
|
+
WORKTREE_SCOPE: "worktree",
|
|
1048
|
+
SESSIONS_SCOPE: "sessions",
|
|
1049
|
+
WORKTREES_SCOPE: "worktrees",
|
|
1050
|
+
RUNS_DIR: "runs",
|
|
1051
|
+
RUN_FILE_PREFIX: "run-",
|
|
1052
|
+
JSONL_EXTENSION: ".jsonl"
|
|
1053
|
+
};
|
|
1054
|
+
var STATE_STORE_DOMAIN = {
|
|
1055
|
+
AUDIT: "audit",
|
|
1056
|
+
COMPACT: "compact",
|
|
1057
|
+
REVIEW: "review",
|
|
1058
|
+
TEST: "test"
|
|
1059
|
+
};
|
|
1060
|
+
var STATE_STORE_ERROR = {
|
|
1061
|
+
INVALID_TOKEN: "state-store token must be a safe path segment",
|
|
1062
|
+
INVALID_BRANCH_SLUG: "state-store branch slug must be normalized before storage",
|
|
1063
|
+
RUN_FILE_CREATE_FAILED: "state-store run file create failed",
|
|
1064
|
+
RUN_FILE_COLLISION_LIMIT: "state-store run file collision limit exhausted",
|
|
1065
|
+
RECORD_ALREADY_EXISTS: "state-store record already exists",
|
|
1066
|
+
RECORD_WRITE_FAILED: "state-store record write failed",
|
|
1067
|
+
RECORD_READ_FAILED: "state-store record read failed"
|
|
1068
|
+
};
|
|
1069
|
+
var HEX_ENCODING = "hex";
|
|
1070
|
+
var RUN_ID_BYTES = 6;
|
|
1071
|
+
var RUN_FILE_CREATE_ATTEMPTS = 10;
|
|
1072
|
+
var RUN_TIMESTAMP_SEPARATOR = "_";
|
|
1073
|
+
var RUN_TIMESTAMP_MILLISECOND_DIGITS = 3;
|
|
1074
|
+
var SLUG_SEPARATOR = "-";
|
|
1075
|
+
var EMPTY_STRING = "";
|
|
1076
|
+
var RUN_TOKEN_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[a-f0-9]{12}$/;
|
|
1077
|
+
var TOKEN_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
1078
|
+
var EXCLUSIVE_CREATE_FLAG = "wx";
|
|
1079
|
+
var WRITE_EXISTING_FLAG = "r+";
|
|
1080
|
+
var ERROR_CODE_FILE_EXISTS = "EEXIST";
|
|
1081
|
+
var ERROR_CODE_NOT_FOUND = "ENOENT";
|
|
1082
|
+
var JSONL_LINE_SEPARATOR = "\n";
|
|
1083
|
+
var ERROR_DETAIL_SEPARATOR = ": ";
|
|
1084
|
+
var defaultFileSystem = {
|
|
1085
|
+
mkdir: async (path6, options) => {
|
|
1086
|
+
await nodeMkdir(path6, options);
|
|
1087
|
+
},
|
|
1088
|
+
writeFile: nodeWriteFile,
|
|
1089
|
+
appendFile: nodeAppendFile,
|
|
1090
|
+
readFile: nodeReadFile,
|
|
1091
|
+
readdir: nodeReaddir
|
|
1092
|
+
};
|
|
1093
|
+
function validateScopeToken(token) {
|
|
1094
|
+
return TOKEN_PATTERN.test(token) ? { ok: true, value: token } : { ok: false, error: STATE_STORE_ERROR.INVALID_TOKEN };
|
|
1095
|
+
}
|
|
1096
|
+
async function resolveWorktreeScopeDir(options = {}) {
|
|
1097
|
+
const gitResult = await detectWorktreeProductRoot(options.cwd, options.deps);
|
|
1098
|
+
return worktreeScopeDir(gitResult.productDir);
|
|
1099
|
+
}
|
|
1100
|
+
async function resolveSessionsScopeDir(options = {}) {
|
|
1101
|
+
const gitResult = await detectGitCommonDirProductRoot(options.cwd, options.deps);
|
|
1102
|
+
return {
|
|
1103
|
+
sessionsDir: sessionsScopeDir(gitResult.productDir),
|
|
1104
|
+
warning: gitResult.warning
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
function worktreeScopeDir(productDir) {
|
|
1108
|
+
return { ok: true, value: join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.WORKTREE_SCOPE) };
|
|
1109
|
+
}
|
|
1110
|
+
function sessionsScopeDir(productDir) {
|
|
1111
|
+
return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
|
|
1112
|
+
}
|
|
1113
|
+
async function resolveWorktreesScopeDir(options = {}) {
|
|
1114
|
+
const gitResult = await detectGitCommonDirProductRoot(options.cwd, options.deps);
|
|
1115
|
+
return {
|
|
1116
|
+
worktreesDir: worktreesScopeDir(gitResult.productDir),
|
|
1117
|
+
warning: gitResult.warning
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
function worktreesScopeDir(productDir) {
|
|
1121
|
+
return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.WORKTREES_SCOPE);
|
|
1122
|
+
}
|
|
1123
|
+
function composeScopeDir(baseScopeDir, ...tokens) {
|
|
1124
|
+
const segments = [];
|
|
1125
|
+
for (const token of tokens) {
|
|
1126
|
+
const validated = validateScopeToken(token);
|
|
1127
|
+
if (!validated.ok) return validated;
|
|
1128
|
+
segments.push(validated.value);
|
|
1129
|
+
}
|
|
1130
|
+
return { ok: true, value: join2(baseScopeDir, ...segments) };
|
|
1131
|
+
}
|
|
1132
|
+
function domainDir(scopeDir, domainName) {
|
|
1133
|
+
return composeScopeDir(scopeDir, domainName);
|
|
1134
|
+
}
|
|
1135
|
+
function runsDir(scopeDir, domainName) {
|
|
1136
|
+
const domain = domainDir(scopeDir, domainName);
|
|
1137
|
+
if (!domain.ok) return domain;
|
|
1138
|
+
return { ok: true, value: join2(domain.value, STATE_STORE_PATH.RUNS_DIR) };
|
|
1139
|
+
}
|
|
1140
|
+
function formatRunTimestamp(date) {
|
|
1141
|
+
const year = date.getUTCFullYear();
|
|
1142
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
1143
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
1144
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
1145
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
1146
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
1147
|
+
const milliseconds = String(date.getUTCMilliseconds()).padStart(RUN_TIMESTAMP_MILLISECOND_DIGITS, "0");
|
|
1148
|
+
return `${year}-${month}-${day}${RUN_TIMESTAMP_SEPARATOR}${hours}-${minutes}-${seconds}-${milliseconds}`;
|
|
1149
|
+
}
|
|
1150
|
+
function generateRunId(randomBytes = nodeRandomBytes) {
|
|
1151
|
+
return randomBytes(RUN_ID_BYTES).toString(HEX_ENCODING);
|
|
1152
|
+
}
|
|
1153
|
+
function runFileName(runToken) {
|
|
1154
|
+
return `${STATE_STORE_PATH.RUN_FILE_PREFIX}${runToken}${STATE_STORE_PATH.JSONL_EXTENSION}`;
|
|
1155
|
+
}
|
|
1156
|
+
function isRunFileName(name) {
|
|
1157
|
+
return name.startsWith(STATE_STORE_PATH.RUN_FILE_PREFIX) && name.endsWith(STATE_STORE_PATH.JSONL_EXTENSION) && RUN_TOKEN_PATTERN.test(name.slice(
|
|
1158
|
+
STATE_STORE_PATH.RUN_FILE_PREFIX.length,
|
|
1159
|
+
-STATE_STORE_PATH.JSONL_EXTENSION.length
|
|
1160
|
+
));
|
|
1161
|
+
}
|
|
1162
|
+
async function createJsonlRunFile(scopeDir, domainName, options = {}) {
|
|
1163
|
+
const fs7 = options.fs ?? defaultFileSystem;
|
|
1164
|
+
const domainRunsDir = runsDir(scopeDir, domainName);
|
|
1165
|
+
if (!domainRunsDir.ok) return domainRunsDir;
|
|
1166
|
+
const maxAttempts = options.maxAttempts ?? RUN_FILE_CREATE_ATTEMPTS;
|
|
1167
|
+
const startedDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1168
|
+
const startedAt = formatRunTimestamp(startedDate);
|
|
1169
|
+
const randomBytes = options.randomBytes ?? nodeRandomBytes;
|
|
1170
|
+
try {
|
|
1171
|
+
await fs7.mkdir(domainRunsDir.value, { recursive: true });
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
return {
|
|
1174
|
+
ok: false,
|
|
1175
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED, toErrorMessage(error))
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1179
|
+
const runId = generateRunId(randomBytes);
|
|
1180
|
+
const runToken = `${startedAt}${SLUG_SEPARATOR}${runId}`;
|
|
1181
|
+
const name = runFileName(runToken);
|
|
1182
|
+
const path6 = join2(domainRunsDir.value, name);
|
|
1183
|
+
try {
|
|
1184
|
+
await fs7.writeFile(path6, EMPTY_STRING, { flag: EXCLUSIVE_CREATE_FLAG });
|
|
1185
|
+
return {
|
|
1186
|
+
ok: true,
|
|
1187
|
+
value: { runsDir: domainRunsDir.value, runFilePath: path6, runFileName: name, runToken, runId, startedAt }
|
|
1188
|
+
};
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
if (hasErrorCode(error, ERROR_CODE_FILE_EXISTS)) continue;
|
|
1191
|
+
return {
|
|
1192
|
+
ok: false,
|
|
1193
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED, toErrorMessage(error))
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return { ok: false, error: STATE_STORE_ERROR.RUN_FILE_COLLISION_LIMIT };
|
|
1198
|
+
}
|
|
1199
|
+
async function writeJsonlRunRecord(runFilePath, record, options = {}) {
|
|
1200
|
+
const fs7 = options.fs ?? defaultFileSystem;
|
|
1201
|
+
try {
|
|
1202
|
+
const existing = await fs7.readFile(runFilePath, "utf8");
|
|
1203
|
+
if (existing.trim().length > 0) return { ok: false, error: STATE_STORE_ERROR.RECORD_ALREADY_EXISTS };
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
if (!hasErrorCode(error, ERROR_CODE_NOT_FOUND)) {
|
|
1206
|
+
return {
|
|
1207
|
+
ok: false,
|
|
1208
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
await fs7.writeFile(runFilePath, serializeJsonlRecord(record), { flag: WRITE_EXISTING_FLAG });
|
|
1214
|
+
return { ok: true, value: runFilePath };
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
return {
|
|
1217
|
+
ok: false,
|
|
1218
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function appendJsonlRecord(filePath, record, options = {}) {
|
|
1223
|
+
const fs7 = options.fs ?? defaultFileSystem;
|
|
1224
|
+
try {
|
|
1225
|
+
await fs7.mkdir(dirname2(filePath), { recursive: true });
|
|
1226
|
+
await fs7.appendFile(filePath, serializeJsonlRecord(record));
|
|
1227
|
+
return { ok: true, value: filePath };
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
return {
|
|
1230
|
+
ok: false,
|
|
1231
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RECORD_WRITE_FAILED, toErrorMessage(error))
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
async function readLatestJsonlRecord(filePath, options = {}) {
|
|
1236
|
+
const fs7 = options.fs ?? defaultFileSystem;
|
|
1237
|
+
let content;
|
|
1238
|
+
try {
|
|
1239
|
+
content = await fs7.readFile(filePath, "utf8");
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return { ok: true, value: void 0 };
|
|
1242
|
+
return {
|
|
1243
|
+
ok: false,
|
|
1244
|
+
error: formatStateStoreError(STATE_STORE_ERROR.RECORD_READ_FAILED, toErrorMessage(error))
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
for (const line of nonEmptyJsonlLinesNewestFirst(content)) {
|
|
1248
|
+
try {
|
|
1249
|
+
return { ok: true, value: JSON.parse(line) };
|
|
1250
|
+
} catch {
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return { ok: true, value: void 0 };
|
|
1255
|
+
}
|
|
1256
|
+
function latestNonEmptyJsonlLine(content) {
|
|
1257
|
+
return nonEmptyJsonlLinesNewestFirst(content)[0];
|
|
1258
|
+
}
|
|
1259
|
+
function compareAsciiStrings(left, right) {
|
|
1260
|
+
if (left < right) return -1;
|
|
1261
|
+
if (left > right) return 1;
|
|
1262
|
+
return 0;
|
|
1263
|
+
}
|
|
1264
|
+
function formatStateStoreError(code, detail) {
|
|
1265
|
+
return detail === void 0 ? code : `${code}${ERROR_DETAIL_SEPARATOR}${detail}`;
|
|
1266
|
+
}
|
|
1267
|
+
function parseStateStoreError(error) {
|
|
1268
|
+
for (const code of Object.values(STATE_STORE_ERROR)) {
|
|
1269
|
+
if (error === code) return { code };
|
|
1270
|
+
const prefix = `${code}${ERROR_DETAIL_SEPARATOR}`;
|
|
1271
|
+
if (error.startsWith(prefix)) return { code, detail: error.slice(prefix.length) };
|
|
1272
|
+
}
|
|
1273
|
+
return void 0;
|
|
1274
|
+
}
|
|
1275
|
+
function serializeJsonlRecord(record) {
|
|
1276
|
+
return `${JSON.stringify(record)}${JSONL_LINE_SEPARATOR}`;
|
|
1277
|
+
}
|
|
1278
|
+
function nonEmptyJsonlLinesNewestFirst(content) {
|
|
1279
|
+
const lines = content.split(JSONL_LINE_SEPARATOR);
|
|
1280
|
+
const latestLines = [];
|
|
1281
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1282
|
+
const line = lines[index]?.trim() ?? EMPTY_STRING;
|
|
1283
|
+
if (line.length > 0) latestLines.push(line);
|
|
1284
|
+
}
|
|
1285
|
+
return latestLines;
|
|
1286
|
+
}
|
|
1287
|
+
function hasErrorCode(error, code) {
|
|
1288
|
+
return typeof error === "object" && error !== null && !Array.isArray(error) && "code" in error && error.code === code;
|
|
1289
|
+
}
|
|
1290
|
+
function toErrorMessage(error) {
|
|
1291
|
+
return error instanceof Error ? error.message : String(error);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/domains/compact/index.ts
|
|
1295
|
+
var COMPACT_STORE_PATH = {
|
|
1296
|
+
STASH_FILE: "stash.jsonl"
|
|
1297
|
+
};
|
|
1298
|
+
var COMPACT_RECORD_FIELDS = {
|
|
1299
|
+
ACTIVE_NODE: "active_node",
|
|
1300
|
+
HAS_FOUNDATION: "has_foundation"
|
|
1301
|
+
};
|
|
1302
|
+
var COMPACT_MARKER = {
|
|
1303
|
+
FOUNDATION: "SPEC_TREE_FOUNDATION",
|
|
1304
|
+
CONTEXT: "SPEC_TREE_CONTEXT",
|
|
1305
|
+
TARGET_ATTRIBUTE: "target",
|
|
1306
|
+
ESCAPED_TARGET_QUOTE: '\\"',
|
|
1307
|
+
UNESCAPED_TARGET_QUOTE: '"'
|
|
1308
|
+
};
|
|
1309
|
+
var COMPACT_ERROR = {
|
|
1310
|
+
RECORD_SHAPE_INVALID: "compact record shape invalid"
|
|
1311
|
+
};
|
|
1312
|
+
var EMPTY_ACTIVE_NODE = "";
|
|
1313
|
+
var JSONL_LINE_SEPARATOR2 = "\n";
|
|
1314
|
+
var ATTRIBUTE_ASSIGNMENT = "=";
|
|
1315
|
+
var ESCAPE_CHARACTER = "\\";
|
|
1316
|
+
var CONTEXT_TARGET_PREFIX = `${COMPACT_MARKER.TARGET_ATTRIBUTE}${ATTRIBUTE_ASSIGNMENT}`;
|
|
1317
|
+
var NODE_PATH_PREFIX = "spx/";
|
|
1318
|
+
function resolveCompactSessionToken(sessionId, env) {
|
|
1319
|
+
if (sessionId !== void 0 && sessionId.length > 0) {
|
|
1320
|
+
return normalizeAgentSessionToken(sessionId);
|
|
1321
|
+
}
|
|
1322
|
+
return resolveAgentSessionId(env);
|
|
1323
|
+
}
|
|
1324
|
+
function extractCompactRecord(transcript) {
|
|
1325
|
+
let hasFoundation = false;
|
|
1326
|
+
let activeNode = EMPTY_ACTIVE_NODE;
|
|
1327
|
+
for (const value of transcriptStringValues(transcript)) {
|
|
1328
|
+
if (value.includes(COMPACT_MARKER.FOUNDATION)) hasFoundation = true;
|
|
1329
|
+
activeNode = extractLastContextTarget(value) ?? activeNode;
|
|
1330
|
+
}
|
|
1331
|
+
if (!hasFoundation) return void 0;
|
|
1332
|
+
return {
|
|
1333
|
+
[COMPACT_RECORD_FIELDS.ACTIVE_NODE]: activeNode,
|
|
1334
|
+
[COMPACT_RECORD_FIELDS.HAS_FOUNDATION]: true
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
function compactStashPath(worktreeScopeDir2, sessionToken) {
|
|
1338
|
+
const compactScope = composeScopeDir(worktreeScopeDir2, sessionToken, STATE_STORE_DOMAIN.COMPACT);
|
|
1339
|
+
if (!compactScope.ok) return compactScope;
|
|
1340
|
+
return { ok: true, value: join3(compactScope.value, COMPACT_STORE_PATH.STASH_FILE) };
|
|
1341
|
+
}
|
|
1342
|
+
function parseCompactRecord(value) {
|
|
1343
|
+
if (typeof value === "object" && value !== null && COMPACT_RECORD_FIELDS.ACTIVE_NODE in value && COMPACT_RECORD_FIELDS.HAS_FOUNDATION in value && typeof value[COMPACT_RECORD_FIELDS.ACTIVE_NODE] === "string" && value[COMPACT_RECORD_FIELDS.HAS_FOUNDATION] === true) {
|
|
1344
|
+
return {
|
|
1345
|
+
ok: true,
|
|
1346
|
+
value: {
|
|
1347
|
+
active_node: value[COMPACT_RECORD_FIELDS.ACTIVE_NODE] ?? EMPTY_ACTIVE_NODE,
|
|
1348
|
+
has_foundation: true
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
return { ok: false, error: COMPACT_ERROR.RECORD_SHAPE_INVALID };
|
|
1353
|
+
}
|
|
1354
|
+
function transcriptStringValues(transcript) {
|
|
1355
|
+
const values = [];
|
|
1356
|
+
for (const rawLine of transcript.split(JSONL_LINE_SEPARATOR2)) {
|
|
1357
|
+
const line = rawLine.trim();
|
|
1358
|
+
if (line.length === 0) continue;
|
|
1359
|
+
try {
|
|
1360
|
+
collectStringValues(JSON.parse(line), values);
|
|
1361
|
+
} catch {
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return values;
|
|
895
1366
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1367
|
+
function collectStringValues(value, values) {
|
|
1368
|
+
if (typeof value === "string") {
|
|
1369
|
+
values.push(value);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (Array.isArray(value)) {
|
|
1373
|
+
for (const entry of value) collectStringValues(entry, values);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
if (typeof value === "object" && value !== null) {
|
|
1377
|
+
for (const entry of Object.values(value)) collectStringValues(entry, values);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function extractLastContextTarget(value) {
|
|
1381
|
+
let activeNode;
|
|
1382
|
+
let searchFrom = 0;
|
|
1383
|
+
while (searchFrom < value.length) {
|
|
1384
|
+
const contextIndex = value.indexOf(COMPACT_MARKER.CONTEXT, searchFrom);
|
|
1385
|
+
if (contextIndex < 0) return activeNode;
|
|
1386
|
+
const target = extractContextTargetAt(value, contextIndex + COMPACT_MARKER.CONTEXT.length);
|
|
1387
|
+
if (target !== void 0) activeNode = target;
|
|
1388
|
+
searchFrom = contextIndex + COMPACT_MARKER.CONTEXT.length;
|
|
1389
|
+
}
|
|
1390
|
+
return activeNode;
|
|
1391
|
+
}
|
|
1392
|
+
function extractContextTargetAt(value, searchFrom) {
|
|
1393
|
+
const targetIndex = value.indexOf(CONTEXT_TARGET_PREFIX, searchFrom);
|
|
1394
|
+
if (targetIndex < 0) return void 0;
|
|
1395
|
+
const quoteIndex = findOpeningTargetQuote(value, targetIndex + CONTEXT_TARGET_PREFIX.length);
|
|
1396
|
+
if (quoteIndex === void 0) return void 0;
|
|
1397
|
+
const target = readTargetPath(value, quoteIndex + COMPACT_MARKER.UNESCAPED_TARGET_QUOTE.length);
|
|
1398
|
+
return target.startsWith(NODE_PATH_PREFIX) ? target : void 0;
|
|
1399
|
+
}
|
|
1400
|
+
function findOpeningTargetQuote(value, startIndex) {
|
|
1401
|
+
let index = startIndex;
|
|
1402
|
+
while (value[index] === ESCAPE_CHARACTER) index += 1;
|
|
1403
|
+
return value[index] === COMPACT_MARKER.UNESCAPED_TARGET_QUOTE ? index : void 0;
|
|
1404
|
+
}
|
|
1405
|
+
function readTargetPath(value, startIndex) {
|
|
1406
|
+
let endIndex = startIndex;
|
|
1407
|
+
while (endIndex < value.length && isTargetPathCharacter(value[endIndex] ?? EMPTY_ACTIVE_NODE)) {
|
|
1408
|
+
endIndex += 1;
|
|
1409
|
+
}
|
|
1410
|
+
return value.slice(startIndex, endIndex);
|
|
1411
|
+
}
|
|
1412
|
+
function isTargetPathCharacter(character) {
|
|
1413
|
+
const code = character.charCodeAt(0);
|
|
1414
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || character === "." || character === "_" || character === "-" || character === "/";
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// src/commands/compact/retrieve.ts
|
|
1418
|
+
var EMPTY_OUTPUT = "";
|
|
1419
|
+
async function compactRetrieveCommand(options) {
|
|
1420
|
+
const sessionToken = resolveCompactSessionToken(options.sessionId, options.env ?? process.env);
|
|
1421
|
+
if (sessionToken === void 0) return { exitCode: 1, output: EMPTY_OUTPUT };
|
|
1422
|
+
const worktreeScope = await resolveWorktreeScopeDir({ cwd: options.cwd });
|
|
1423
|
+
if (!worktreeScope.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
|
|
1424
|
+
const stashPath = compactStashPath(worktreeScope.value, sessionToken);
|
|
1425
|
+
if (!stashPath.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
|
|
1426
|
+
const latest = await readLatestJsonlRecord(stashPath.value);
|
|
1427
|
+
if (!latest.ok || latest.value === void 0) return { exitCode: 1, output: EMPTY_OUTPUT };
|
|
1428
|
+
const record = parseCompactRecord(latest.value);
|
|
1429
|
+
if (!record.ok) return { exitCode: 1, output: EMPTY_OUTPUT };
|
|
1430
|
+
return { exitCode: 0, output: `${JSON.stringify(record.value)}
|
|
1431
|
+
` };
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/commands/compact/store.ts
|
|
1435
|
+
import { readFile } from "fs/promises";
|
|
1436
|
+
var UTF8_ENCODING = "utf8";
|
|
1437
|
+
async function compactStoreCommand(options) {
|
|
1438
|
+
const sessionToken = resolveCompactSessionToken(options.sessionId, options.env ?? process.env);
|
|
1439
|
+
if (sessionToken === void 0) return 1;
|
|
1440
|
+
let transcript;
|
|
1441
|
+
try {
|
|
1442
|
+
transcript = await readFile(options.transcript, UTF8_ENCODING);
|
|
1443
|
+
} catch {
|
|
1444
|
+
return 1;
|
|
1445
|
+
}
|
|
1446
|
+
const record = extractCompactRecord(transcript);
|
|
1447
|
+
if (record === void 0) return 0;
|
|
1448
|
+
const worktreeScope = await resolveWorktreeScopeDir({ cwd: options.cwd });
|
|
1449
|
+
if (!worktreeScope.ok) return 1;
|
|
1450
|
+
const stashPath = compactStashPath(worktreeScope.value, sessionToken);
|
|
1451
|
+
if (!stashPath.ok) return 1;
|
|
1452
|
+
const written = await appendJsonlRecord(stashPath.value, record);
|
|
1453
|
+
return written.ok ? 0 : 1;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/interfaces/cli/compact.ts
|
|
1457
|
+
var COMPACT_CLI = {
|
|
1458
|
+
commandName: "compact",
|
|
1459
|
+
storeCommandName: "store",
|
|
1460
|
+
retrieveCommandName: "retrieve",
|
|
1461
|
+
description: "Store and retrieve compact resume state",
|
|
1462
|
+
transcriptFlag: "--transcript",
|
|
1463
|
+
transcriptOption: "--transcript <path>",
|
|
1464
|
+
sessionIdFlag: "--session-id",
|
|
1465
|
+
sessionIdOption: "--session-id <id>",
|
|
1466
|
+
sessionIdDescription: "Agent session identity (overrides the agent-session environment)"
|
|
1467
|
+
};
|
|
1468
|
+
var compactDomain = {
|
|
1469
|
+
name: COMPACT_CLI.commandName,
|
|
1470
|
+
description: COMPACT_CLI.description,
|
|
899
1471
|
register: (program2) => {
|
|
900
|
-
const
|
|
901
|
-
|
|
1472
|
+
const compactCmd = program2.command(COMPACT_CLI.commandName).description(COMPACT_CLI.description);
|
|
1473
|
+
compactCmd.command(COMPACT_CLI.storeCommandName).description("Store compact resume state").requiredOption(COMPACT_CLI.transcriptOption, "Transcript JSONL path").option(COMPACT_CLI.sessionIdOption, COMPACT_CLI.sessionIdDescription).action(async (options) => {
|
|
1474
|
+
process.exit(
|
|
1475
|
+
await compactStoreCommand({
|
|
1476
|
+
transcript: options.transcript,
|
|
1477
|
+
sessionId: options.sessionId,
|
|
1478
|
+
cwd: process.cwd(),
|
|
1479
|
+
env: process.env
|
|
1480
|
+
})
|
|
1481
|
+
);
|
|
1482
|
+
});
|
|
1483
|
+
compactCmd.command(COMPACT_CLI.retrieveCommandName).description("Retrieve compact resume state").option(COMPACT_CLI.sessionIdOption, COMPACT_CLI.sessionIdDescription).action(async (options) => {
|
|
1484
|
+
const result = await compactRetrieveCommand({
|
|
1485
|
+
sessionId: options.sessionId,
|
|
1486
|
+
cwd: process.cwd(),
|
|
1487
|
+
env: process.env
|
|
1488
|
+
});
|
|
1489
|
+
if (result.output.length > 0) {
|
|
1490
|
+
process.stdout.write(result.output);
|
|
1491
|
+
}
|
|
1492
|
+
process.exitCode = result.exitCode;
|
|
1493
|
+
});
|
|
902
1494
|
}
|
|
903
1495
|
};
|
|
904
1496
|
|
|
905
1497
|
// src/config/index.ts
|
|
906
1498
|
import { readFile as readFile2 } from "fs/promises";
|
|
907
|
-
import { join as
|
|
1499
|
+
import { join as join5 } from "path";
|
|
908
1500
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
909
1501
|
import { parse as parseYaml, parseDocument as parseYamlDocument, stringify as stringifyYaml } from "yaml";
|
|
910
1502
|
|
|
@@ -1415,57 +2007,20 @@ function applyPathFilter(paths, config) {
|
|
|
1415
2007
|
// src/domains/audit/config.ts
|
|
1416
2008
|
var AUDIT_SECTION = "audit";
|
|
1417
2009
|
var AUDIT_CONFIG_FIELDS = {
|
|
1418
|
-
STORAGE: "storage",
|
|
1419
|
-
SPX_DIR: "spxDir",
|
|
1420
|
-
NODES_DIR: "nodesDir",
|
|
1421
|
-
AUDIT_DIR: "auditDir",
|
|
1422
|
-
RUNS_DIR: "runsDir",
|
|
1423
|
-
VERDICT_FILE: "verdictFile",
|
|
1424
|
-
VERDICT_FILE_SUFFIX: "verdictFileSuffix",
|
|
1425
|
-
STATE_FILE: "stateFile",
|
|
1426
2010
|
BASE_REF: "baseRef",
|
|
1427
|
-
BRANCH_SLUG: "branchSlug",
|
|
1428
|
-
MAX_BYTES: "maxBytes",
|
|
1429
2011
|
AUDITORS: "auditors",
|
|
1430
2012
|
TARGETS: "targets"
|
|
1431
2013
|
};
|
|
1432
|
-
var AUDIT_BRANCH_SLUG_MIN_MAX_BYTES = 10;
|
|
1433
2014
|
var DEFAULT_AUDIT_CONFIG = {
|
|
1434
|
-
storage: {
|
|
1435
|
-
spxDir: ".spx",
|
|
1436
|
-
nodesDir: "nodes",
|
|
1437
|
-
auditDir: "audit",
|
|
1438
|
-
runsDir: "runs",
|
|
1439
|
-
verdictFile: "verdict.audit.xml",
|
|
1440
|
-
verdictFileSuffix: ".audit.xml",
|
|
1441
|
-
stateFile: "state.json"
|
|
1442
|
-
},
|
|
1443
2015
|
baseRef: "main",
|
|
1444
|
-
branchSlug: {
|
|
1445
|
-
maxBytes: 120
|
|
1446
|
-
},
|
|
1447
2016
|
auditors: [],
|
|
1448
2017
|
targets: resolveDefaultTargets()
|
|
1449
2018
|
};
|
|
1450
2019
|
var AUDIT_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
|
|
1451
|
-
AUDIT_CONFIG_FIELDS.STORAGE,
|
|
1452
2020
|
AUDIT_CONFIG_FIELDS.BASE_REF,
|
|
1453
|
-
AUDIT_CONFIG_FIELDS.BRANCH_SLUG,
|
|
1454
2021
|
AUDIT_CONFIG_FIELDS.AUDITORS,
|
|
1455
2022
|
AUDIT_CONFIG_FIELDS.TARGETS
|
|
1456
2023
|
]);
|
|
1457
|
-
var AUDIT_STORAGE_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
|
|
1458
|
-
AUDIT_CONFIG_FIELDS.SPX_DIR,
|
|
1459
|
-
AUDIT_CONFIG_FIELDS.NODES_DIR,
|
|
1460
|
-
AUDIT_CONFIG_FIELDS.AUDIT_DIR,
|
|
1461
|
-
AUDIT_CONFIG_FIELDS.RUNS_DIR,
|
|
1462
|
-
AUDIT_CONFIG_FIELDS.VERDICT_FILE,
|
|
1463
|
-
AUDIT_CONFIG_FIELDS.VERDICT_FILE_SUFFIX,
|
|
1464
|
-
AUDIT_CONFIG_FIELDS.STATE_FILE
|
|
1465
|
-
]);
|
|
1466
|
-
var AUDIT_BRANCH_SLUG_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
|
|
1467
|
-
AUDIT_CONFIG_FIELDS.MAX_BYTES
|
|
1468
|
-
]);
|
|
1469
2024
|
var auditConfigDescriptor = {
|
|
1470
2025
|
section: AUDIT_SECTION,
|
|
1471
2026
|
defaults: DEFAULT_AUDIT_CONFIG,
|
|
@@ -1485,15 +2040,11 @@ function validateAuditConfig(raw) {
|
|
|
1485
2040
|
}
|
|
1486
2041
|
const unknown = rejectUnknownFields2(AUDIT_SECTION, raw, AUDIT_ALLOWED_FIELDS);
|
|
1487
2042
|
if (!unknown.ok) return unknown;
|
|
1488
|
-
const storage = raw[AUDIT_CONFIG_FIELDS.STORAGE] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.storage } : validateStorage(raw[AUDIT_CONFIG_FIELDS.STORAGE]);
|
|
1489
|
-
if (!storage.ok) return storage;
|
|
1490
2043
|
const baseRef = raw[AUDIT_CONFIG_FIELDS.BASE_REF] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.baseRef } : validateNonEmptyString2(
|
|
1491
2044
|
`${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BASE_REF}`,
|
|
1492
2045
|
raw[AUDIT_CONFIG_FIELDS.BASE_REF]
|
|
1493
2046
|
);
|
|
1494
2047
|
if (!baseRef.ok) return baseRef;
|
|
1495
|
-
const branchSlug = raw[AUDIT_CONFIG_FIELDS.BRANCH_SLUG] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.branchSlug } : validateBranchSlug(raw[AUDIT_CONFIG_FIELDS.BRANCH_SLUG]);
|
|
1496
|
-
if (!branchSlug.ok) return branchSlug;
|
|
1497
2048
|
const auditors = raw[AUDIT_CONFIG_FIELDS.AUDITORS] === void 0 ? { ok: true, value: DEFAULT_AUDIT_CONFIG.auditors } : validateArrayOfNonEmptyStrings(
|
|
1498
2049
|
`${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.AUDITORS}`,
|
|
1499
2050
|
raw[AUDIT_CONFIG_FIELDS.AUDITORS]
|
|
@@ -1507,61 +2058,12 @@ function validateAuditConfig(raw) {
|
|
|
1507
2058
|
return {
|
|
1508
2059
|
ok: true,
|
|
1509
2060
|
value: {
|
|
1510
|
-
storage: storage.value,
|
|
1511
2061
|
baseRef: baseRef.value,
|
|
1512
|
-
branchSlug: branchSlug.value,
|
|
1513
2062
|
auditors: auditors.value,
|
|
1514
2063
|
targets: targets.value
|
|
1515
2064
|
}
|
|
1516
2065
|
};
|
|
1517
2066
|
}
|
|
1518
|
-
function validateStorage(raw) {
|
|
1519
|
-
if (!isRecord2(raw)) {
|
|
1520
|
-
return { ok: false, error: `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.STORAGE} must be an object` };
|
|
1521
|
-
}
|
|
1522
|
-
const unknown = rejectUnknownFields2(
|
|
1523
|
-
`${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.STORAGE}`,
|
|
1524
|
-
raw,
|
|
1525
|
-
AUDIT_STORAGE_ALLOWED_FIELDS
|
|
1526
|
-
);
|
|
1527
|
-
if (!unknown.ok) return unknown;
|
|
1528
|
-
return validateStringRecord(
|
|
1529
|
-
`${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.STORAGE}`,
|
|
1530
|
-
DEFAULT_AUDIT_CONFIG.storage,
|
|
1531
|
-
raw
|
|
1532
|
-
);
|
|
1533
|
-
}
|
|
1534
|
-
function validateBranchSlug(raw) {
|
|
1535
|
-
if (!isRecord2(raw)) {
|
|
1536
|
-
return { ok: false, error: `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BRANCH_SLUG} must be an object` };
|
|
1537
|
-
}
|
|
1538
|
-
const unknown = rejectUnknownFields2(
|
|
1539
|
-
`${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BRANCH_SLUG}`,
|
|
1540
|
-
raw,
|
|
1541
|
-
AUDIT_BRANCH_SLUG_ALLOWED_FIELDS
|
|
1542
|
-
);
|
|
1543
|
-
if (!unknown.ok) return unknown;
|
|
1544
|
-
const maxBytesRaw = raw[AUDIT_CONFIG_FIELDS.MAX_BYTES];
|
|
1545
|
-
if (maxBytesRaw === void 0) return { ok: true, value: DEFAULT_AUDIT_CONFIG.branchSlug };
|
|
1546
|
-
if (typeof maxBytesRaw !== "number" || !Number.isInteger(maxBytesRaw) || maxBytesRaw < AUDIT_BRANCH_SLUG_MIN_MAX_BYTES) {
|
|
1547
|
-
return {
|
|
1548
|
-
ok: false,
|
|
1549
|
-
error: `${AUDIT_SECTION}.${AUDIT_CONFIG_FIELDS.BRANCH_SLUG}.${AUDIT_CONFIG_FIELDS.MAX_BYTES} must be an integer greater than or equal to ${AUDIT_BRANCH_SLUG_MIN_MAX_BYTES}`
|
|
1550
|
-
};
|
|
1551
|
-
}
|
|
1552
|
-
return { ok: true, value: { maxBytes: maxBytesRaw } };
|
|
1553
|
-
}
|
|
1554
|
-
function validateStringRecord(path6, defaults4, raw) {
|
|
1555
|
-
const next = { ...defaults4 };
|
|
1556
|
-
for (const field of Object.keys(defaults4)) {
|
|
1557
|
-
const value = raw[field];
|
|
1558
|
-
if (value === void 0) continue;
|
|
1559
|
-
const validated = validateNonEmptyString2(`${path6}.${field}`, value);
|
|
1560
|
-
if (!validated.ok) return validated;
|
|
1561
|
-
next[field] = validated.value;
|
|
1562
|
-
}
|
|
1563
|
-
return { ok: true, value: next };
|
|
1564
|
-
}
|
|
1565
2067
|
function validateNonEmptyString2(path6, value) {
|
|
1566
2068
|
if (typeof value !== "string" || value.length === 0) {
|
|
1567
2069
|
return { ok: false, error: `${path6} must be a non-empty string` };
|
|
@@ -1645,6 +2147,112 @@ var DECISION_KINDS = Object.keys(KIND_REGISTRY).filter(
|
|
|
1645
2147
|
);
|
|
1646
2148
|
var NODE_SUFFIXES = NODE_KINDS.map((k) => KIND_REGISTRY[k].suffix);
|
|
1647
2149
|
var DECISION_SUFFIXES = DECISION_KINDS.map((k) => KIND_REGISTRY[k].suffix);
|
|
2150
|
+
var SPEC_TREE_GRAMMAR = {
|
|
2151
|
+
PRODUCT_SUFFIX: SPEC_TREE_CONFIG.PRODUCT.SUFFIX,
|
|
2152
|
+
EVIDENCE: {
|
|
2153
|
+
DIRECTORY_NAME: "tests",
|
|
2154
|
+
MODES: ["scenario", "mapping", "conformance", "property", "compliance"],
|
|
2155
|
+
LEVELS: ["l1", "l2", "l3"],
|
|
2156
|
+
TAILS: {
|
|
2157
|
+
TYPESCRIPT: ["test", "ts"],
|
|
2158
|
+
PYTHON: ["py"],
|
|
2159
|
+
RUST: ["rs"]
|
|
2160
|
+
},
|
|
2161
|
+
SEGMENT_SEPARATOR: "."
|
|
2162
|
+
},
|
|
2163
|
+
RUNNERS: ["vitest", "playwright", "subprocess"],
|
|
2164
|
+
ORDER: {
|
|
2165
|
+
SEPARATOR: "-",
|
|
2166
|
+
PATTERN: /^\d+$/
|
|
2167
|
+
},
|
|
2168
|
+
PATH_SEPARATOR: "/",
|
|
2169
|
+
COORDINATION_NOTES: ["PLAN.md", "ISSUES.md"],
|
|
2170
|
+
EVAL_LANE: ["eval.toml", "cases.jsonl", "prompt.md", "history.jsonl", "runs"],
|
|
2171
|
+
PRIOR_NODE_SUFFIXES: [".capability", ".feature", ".story"]
|
|
2172
|
+
};
|
|
2173
|
+
var SPEC_TREE_EVIDENCE_FILE = SPEC_TREE_GRAMMAR.EVIDENCE;
|
|
2174
|
+
var NAMING_SCHEMA_VERSION_ID = {
|
|
2175
|
+
PRIOR: "1.0.0",
|
|
2176
|
+
CANONICAL: "2.0.0"
|
|
2177
|
+
};
|
|
2178
|
+
function namingSchemaVersionFromNodeSuffixes(version2, nodeSuffixes) {
|
|
2179
|
+
return {
|
|
2180
|
+
version: version2,
|
|
2181
|
+
nodeSuffixes,
|
|
2182
|
+
decisionSuffixes: DECISION_SUFFIXES,
|
|
2183
|
+
productSuffix: SPEC_TREE_GRAMMAR.PRODUCT_SUFFIX,
|
|
2184
|
+
evidence: SPEC_TREE_GRAMMAR.EVIDENCE,
|
|
2185
|
+
runners: SPEC_TREE_GRAMMAR.RUNNERS,
|
|
2186
|
+
order: SPEC_TREE_GRAMMAR.ORDER,
|
|
2187
|
+
pathSeparator: SPEC_TREE_GRAMMAR.PATH_SEPARATOR,
|
|
2188
|
+
coordinationNotes: SPEC_TREE_GRAMMAR.COORDINATION_NOTES,
|
|
2189
|
+
evalLane: SPEC_TREE_GRAMMAR.EVAL_LANE
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
var SPEC_TREE_NAMING_SCHEMA_VERSIONS = [
|
|
2193
|
+
namingSchemaVersionFromNodeSuffixes(NAMING_SCHEMA_VERSION_ID.PRIOR, SPEC_TREE_GRAMMAR.PRIOR_NODE_SUFFIXES),
|
|
2194
|
+
namingSchemaVersionFromNodeSuffixes(NAMING_SCHEMA_VERSION_ID.CANONICAL, NODE_SUFFIXES)
|
|
2195
|
+
];
|
|
2196
|
+
var VERSION_COMPONENT_SEPARATOR = ".";
|
|
2197
|
+
var VERSION_COMPONENT_RADIX = 10;
|
|
2198
|
+
var VERSION_MISSING_COMPONENT = 0;
|
|
2199
|
+
var VERSION_ORDER_EQUAL = 0;
|
|
2200
|
+
var VERSION_NUMERIC_COMPONENT = /^\d+$/;
|
|
2201
|
+
function parseVersionComponents(version2) {
|
|
2202
|
+
return version2.split(VERSION_COMPONENT_SEPARATOR).map((part) => {
|
|
2203
|
+
if (!VERSION_NUMERIC_COMPONENT.test(part)) {
|
|
2204
|
+
throw new Error(
|
|
2205
|
+
`Naming-schema version "${version2}" must use numeric dotted components; "${part}" is not numeric`
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
return Number.parseInt(part, VERSION_COMPONENT_RADIX);
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
function compareNumericVersionIdentifiers(left, right) {
|
|
2212
|
+
const leftComponents = parseVersionComponents(left);
|
|
2213
|
+
const rightComponents = parseVersionComponents(right);
|
|
2214
|
+
const length = Math.max(leftComponents.length, rightComponents.length);
|
|
2215
|
+
for (let index = 0; index < length; index += 1) {
|
|
2216
|
+
const difference = (leftComponents[index] ?? VERSION_MISSING_COMPONENT) - (rightComponents[index] ?? VERSION_MISSING_COMPONENT);
|
|
2217
|
+
if (difference !== VERSION_ORDER_EQUAL) {
|
|
2218
|
+
return difference;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return VERSION_ORDER_EQUAL;
|
|
2222
|
+
}
|
|
2223
|
+
function compareNamingSchemaVersions(left, right) {
|
|
2224
|
+
return compareNumericVersionIdentifiers(left.version, right.version);
|
|
2225
|
+
}
|
|
2226
|
+
function canonicalNamingSchemaVersion(versions) {
|
|
2227
|
+
const [first, ...rest] = versions;
|
|
2228
|
+
if (first === void 0) {
|
|
2229
|
+
throw new Error("Naming-schema version tuple must declare at least one version");
|
|
2230
|
+
}
|
|
2231
|
+
return rest.reduce(
|
|
2232
|
+
(max, version2) => compareNamingSchemaVersions(version2, max) > VERSION_ORDER_EQUAL ? version2 : max,
|
|
2233
|
+
first
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
function supersededNodeSuffixes(versions) {
|
|
2237
|
+
const canonical = canonicalNamingSchemaVersion(versions);
|
|
2238
|
+
const canonicalSuffixes = new Set(canonical.nodeSuffixes);
|
|
2239
|
+
const superseded = /* @__PURE__ */ new Set();
|
|
2240
|
+
for (const version2 of versions) {
|
|
2241
|
+
if (version2 === canonical) {
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
for (const suffix of version2.nodeSuffixes) {
|
|
2245
|
+
if (!canonicalSuffixes.has(suffix)) {
|
|
2246
|
+
superseded.add(suffix);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
return [...superseded];
|
|
2251
|
+
}
|
|
2252
|
+
var SPEC_TREE_NAMING_VERSION = canonicalNamingSchemaVersion(SPEC_TREE_NAMING_SCHEMA_VERSIONS).version;
|
|
2253
|
+
var SPEC_TREE_SUPERSEDED_NODE_SUFFIXES = supersededNodeSuffixes(
|
|
2254
|
+
SPEC_TREE_NAMING_SCHEMA_VERSIONS
|
|
2255
|
+
);
|
|
1648
2256
|
var SPEC_TREE_NODE_STATE = {
|
|
1649
2257
|
DECLARED: "declared",
|
|
1650
2258
|
SPECIFIED: "specified",
|
|
@@ -1822,7 +2430,7 @@ var REGISTERED_TOOL_NAMES = Object.keys(ADAPTER_MAP);
|
|
|
1822
2430
|
|
|
1823
2431
|
// src/lib/file-inclusion/ignore-source.ts
|
|
1824
2432
|
import { readFileSync } from "fs";
|
|
1825
|
-
import { join } from "path";
|
|
2433
|
+
import { join as join4 } from "path";
|
|
1826
2434
|
var IGNORE_SOURCE_FILENAME_DEFAULT = "EXCLUDE";
|
|
1827
2435
|
var EMPTY_IGNORE_READER = {
|
|
1828
2436
|
isUnderIgnoreSource() {
|
|
@@ -1856,7 +2464,7 @@ function validateEntry(entry, lineNumber) {
|
|
|
1856
2464
|
}
|
|
1857
2465
|
function createIgnoreSourceReader(projectRoot, config) {
|
|
1858
2466
|
const { ignoreSourceFilename, specTreeRootSegment } = config;
|
|
1859
|
-
const filePath =
|
|
2467
|
+
const filePath = join4(projectRoot, specTreeRootSegment, ignoreSourceFilename);
|
|
1860
2468
|
let content;
|
|
1861
2469
|
try {
|
|
1862
2470
|
content = readFileSync(filePath, "utf8");
|
|
@@ -1919,8 +2527,8 @@ var HIDDEN_PREFIX_LAYER = "hidden-prefix";
|
|
|
1919
2527
|
var LAYER2 = HIDDEN_PREFIX_LAYER;
|
|
1920
2528
|
function hiddenPrefixPredicate(path6, config) {
|
|
1921
2529
|
const segments = path6.split("/");
|
|
1922
|
-
const
|
|
1923
|
-
const matched =
|
|
2530
|
+
const basename5 = segments[segments.length - 1] ?? "";
|
|
2531
|
+
const matched = basename5.startsWith(config.hiddenPrefix);
|
|
1924
2532
|
return { matched, layer: LAYER2 };
|
|
1925
2533
|
}
|
|
1926
2534
|
|
|
@@ -2319,7 +2927,7 @@ var defaults3 = {
|
|
|
2319
2927
|
enabled: false
|
|
2320
2928
|
}
|
|
2321
2929
|
};
|
|
2322
|
-
function
|
|
2930
|
+
function validatePaths(raw) {
|
|
2323
2931
|
const basePath = `${VALIDATION_SECTION}.${VALIDATION_PATHS_SUBSECTION}`;
|
|
2324
2932
|
const baseResult = validatePathFilterConfig(raw, basePath);
|
|
2325
2933
|
if (!baseResult.ok) return baseResult;
|
|
@@ -2386,7 +2994,7 @@ function validate7(value) {
|
|
|
2386
2994
|
}
|
|
2387
2995
|
const candidate = value;
|
|
2388
2996
|
const pathsRaw = candidate[VALIDATION_PATHS_SUBSECTION] ?? {};
|
|
2389
|
-
const pathsResult =
|
|
2997
|
+
const pathsResult = validatePaths(pathsRaw);
|
|
2390
2998
|
if (!pathsResult.ok) return pathsResult;
|
|
2391
2999
|
const literalRaw = candidate[VALIDATION_LITERAL_SUBSECTION] ?? {};
|
|
2392
3000
|
const literalResult = validateLiteral(literalRaw);
|
|
@@ -2414,11 +3022,11 @@ var productionRegistry = [
|
|
|
2414
3022
|
];
|
|
2415
3023
|
|
|
2416
3024
|
// src/config/descriptor-digest.ts
|
|
2417
|
-
import { createHash } from "crypto";
|
|
3025
|
+
import { createHash as createHash3 } from "crypto";
|
|
2418
3026
|
var DEFAULT_DESCRIPTOR_PATH = "descriptor section";
|
|
2419
3027
|
var SHA256_ALGORITHM = "sha256";
|
|
2420
|
-
var
|
|
2421
|
-
var
|
|
3028
|
+
var UTF8_ENCODING2 = "utf8";
|
|
3029
|
+
var HEX_ENCODING2 = "hex";
|
|
2422
3030
|
function canonicalDescriptorJson(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
|
|
2423
3031
|
const normalized = normalizeDescriptorJsonValue(value, path6, /* @__PURE__ */ new WeakSet());
|
|
2424
3032
|
if (!normalized.ok) return normalized;
|
|
@@ -2427,7 +3035,7 @@ function canonicalDescriptorJson(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
|
|
|
2427
3035
|
function digestDescriptorSection(value, path6 = DEFAULT_DESCRIPTOR_PATH) {
|
|
2428
3036
|
const canonical = canonicalDescriptorJson(value, path6);
|
|
2429
3037
|
if (!canonical.ok) return canonical;
|
|
2430
|
-
const sha256 =
|
|
3038
|
+
const sha256 = createHash3(SHA256_ALGORITHM).update(Buffer.from(canonical.value, UTF8_ENCODING2)).digest(HEX_ENCODING2);
|
|
2431
3039
|
return {
|
|
2432
3040
|
ok: true,
|
|
2433
3041
|
value: {
|
|
@@ -2569,7 +3177,7 @@ async function readProductConfigFile(productDir) {
|
|
|
2569
3177
|
const detected = [];
|
|
2570
3178
|
for (const format2 of CONFIG_FILE_FORMAT_ORDER) {
|
|
2571
3179
|
const filename = CONFIG_FILE_DEFINITIONS[format2].filename;
|
|
2572
|
-
const path6 =
|
|
3180
|
+
const path6 = join5(productDir, filename);
|
|
2573
3181
|
let raw;
|
|
2574
3182
|
try {
|
|
2575
3183
|
raw = await readFile2(path6, "utf8");
|
|
@@ -2596,7 +3204,7 @@ function configFileForFormat(productDir, format2 = DEFAULT_CONFIG_FILE_FORMAT, r
|
|
|
2596
3204
|
return {
|
|
2597
3205
|
filename,
|
|
2598
3206
|
format: format2,
|
|
2599
|
-
path:
|
|
3207
|
+
path: join5(productDir, filename),
|
|
2600
3208
|
raw
|
|
2601
3209
|
};
|
|
2602
3210
|
}
|
|
@@ -2893,7 +3501,7 @@ var configDomain = {
|
|
|
2893
3501
|
|
|
2894
3502
|
// src/commands/session/archive.ts
|
|
2895
3503
|
import { mkdir, rename, stat } from "fs/promises";
|
|
2896
|
-
import { dirname as
|
|
3504
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
2897
3505
|
|
|
2898
3506
|
// src/domains/session/archive.ts
|
|
2899
3507
|
var SESSION_FILE_EXTENSION = ".md";
|
|
@@ -2974,24 +3582,24 @@ var HANDOFF_BASE_PREREQUISITE_LABEL = {
|
|
|
2974
3582
|
DETACHED_AT_DEFAULT_TIP: "HEAD is detached at the default-branch tip"
|
|
2975
3583
|
};
|
|
2976
3584
|
var HANDOFF_BASE_REMEDY = {
|
|
2977
|
-
/** Unclean working tree: commit, or hand off from the
|
|
2978
|
-
|
|
2979
|
-
/** Off the default-branch tip: detach to it, or hand off from the
|
|
2980
|
-
|
|
2981
|
-
/** Default branch unresolved: only the
|
|
2982
|
-
|
|
3585
|
+
/** Unclean working tree: commit, or hand off from the main checkout. */
|
|
3586
|
+
COMMIT_OR_MAIN_CHECKOUT: "commit the changes, or run handoff from the main checkout",
|
|
3587
|
+
/** Off the default-branch tip: detach to it, or hand off from the main checkout. */
|
|
3588
|
+
DETACH_TO_TIP_OR_MAIN_CHECKOUT: "detach HEAD to the default-branch tip, or run handoff from the main checkout",
|
|
3589
|
+
/** Default branch unresolved: only the main checkout can anchor the base. */
|
|
3590
|
+
MAIN_CHECKOUT_ONLY: "run handoff from the main checkout"
|
|
2983
3591
|
};
|
|
2984
3592
|
var HANDOFF_BASE_FACT_LABEL = {
|
|
2985
3593
|
DEFAULT_BRANCH: "default branch",
|
|
2986
3594
|
DEFAULT_TIP: "origin tip",
|
|
2987
3595
|
HEAD: "HEAD",
|
|
2988
3596
|
CURRENT_WORKTREE: "current worktree",
|
|
2989
|
-
|
|
3597
|
+
MAIN_CHECKOUT: "main checkout"
|
|
2990
3598
|
};
|
|
2991
3599
|
var HANDOFF_BASE_UNRESOLVED = "unresolved";
|
|
2992
3600
|
var CHECKLIST_INDENT = " ";
|
|
2993
3601
|
var REMEDY_SEPARATOR = " \u2014 ";
|
|
2994
|
-
var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this
|
|
3602
|
+
var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this worktree \u2014 it is not the main checkout.`;
|
|
2995
3603
|
function renderFactLine(label, value) {
|
|
2996
3604
|
return `${CHECKLIST_INDENT}${label}: ${value ?? HANDOFF_BASE_UNRESOLVED}`;
|
|
2997
3605
|
}
|
|
@@ -3007,7 +3615,7 @@ function renderHandoffBaseChecklist(checklist) {
|
|
|
3007
3615
|
renderFactLine(HANDOFF_BASE_FACT_LABEL.DEFAULT_TIP, checklist.defaultTipSha),
|
|
3008
3616
|
renderFactLine(HANDOFF_BASE_FACT_LABEL.HEAD, checklist.headSha),
|
|
3009
3617
|
renderFactLine(HANDOFF_BASE_FACT_LABEL.CURRENT_WORKTREE, checklist.currentWorktreePath),
|
|
3010
|
-
renderFactLine(HANDOFF_BASE_FACT_LABEL.
|
|
3618
|
+
renderFactLine(HANDOFF_BASE_FACT_LABEL.MAIN_CHECKOUT, checklist.mainCheckoutPath),
|
|
3011
3619
|
...checklist.prerequisites.map(renderPrerequisiteLine)
|
|
3012
3620
|
].join("\n");
|
|
3013
3621
|
}
|
|
@@ -3049,6 +3657,18 @@ var SessionInvalidGoalError = class extends SessionError {
|
|
|
3049
3657
|
this.name = "SessionInvalidGoalError";
|
|
3050
3658
|
}
|
|
3051
3659
|
};
|
|
3660
|
+
var SessionInvalidFieldError = class extends SessionError {
|
|
3661
|
+
/** The unknown field token the caller supplied. */
|
|
3662
|
+
field;
|
|
3663
|
+
/** The full set of valid field names. */
|
|
3664
|
+
validFields;
|
|
3665
|
+
constructor(field, validFields) {
|
|
3666
|
+
super(`Invalid field: "${field}". Valid fields: ${validFields.join(", ")}`);
|
|
3667
|
+
this.name = "SessionInvalidFieldError";
|
|
3668
|
+
this.field = field;
|
|
3669
|
+
this.validFields = validFields;
|
|
3670
|
+
}
|
|
3671
|
+
};
|
|
3052
3672
|
var SessionInvalidNextStepError = class extends SessionError {
|
|
3053
3673
|
constructor() {
|
|
3054
3674
|
super("Invalid session next_step: next_step must be a non-empty string.");
|
|
@@ -3062,7 +3682,7 @@ var SessionHandoffBaseError = class extends SessionError {
|
|
|
3062
3682
|
silent;
|
|
3063
3683
|
constructor(options = {}) {
|
|
3064
3684
|
super(
|
|
3065
|
-
"Cannot create a handoff session from this git work context. Run handoff from the
|
|
3685
|
+
"Cannot create a handoff session from this git work context. Run handoff from the main checkout, or from a non-main checkout with a clean working tree detached at the tip of the default branch."
|
|
3066
3686
|
);
|
|
3067
3687
|
this.name = SESSION_HANDOFF_BASE_ERROR_NAME;
|
|
3068
3688
|
this.checklist = options.checklist ?? null;
|
|
@@ -3093,252 +3713,62 @@ var SessionLegacyFrontmatterInputError = class extends SessionError {
|
|
|
3093
3713
|
);
|
|
3094
3714
|
this.name = "SessionLegacyFrontmatterInputError";
|
|
3095
3715
|
}
|
|
3096
|
-
};
|
|
3097
|
-
var
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
delete cleaned[key];
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
|
-
return cleaned;
|
|
3129
|
-
}
|
|
3130
|
-
|
|
3131
|
-
// src/git/root.ts
|
|
3132
|
-
var defaultDeps = {
|
|
3133
|
-
execa: async (command, args, options) => {
|
|
3134
|
-
const result = await execa2(command, args, {
|
|
3135
|
-
...options,
|
|
3136
|
-
env: withoutGitEnvironment(process.env),
|
|
3137
|
-
extendEnv: false
|
|
3138
|
-
});
|
|
3139
|
-
return {
|
|
3140
|
-
exitCode: result.exitCode ?? 0,
|
|
3141
|
-
stdout: typeof result.stdout === "string" ? result.stdout : String(result.stdout),
|
|
3142
|
-
stderr: typeof result.stderr === "string" ? result.stderr : String(result.stderr)
|
|
3143
|
-
};
|
|
3144
|
-
}
|
|
3145
|
-
};
|
|
3146
|
-
var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository; resolving session storage relative to the current directory.";
|
|
3147
|
-
var GIT_ROOT_COMMAND = {
|
|
3148
|
-
EXECUTABLE: "git",
|
|
3149
|
-
REV_PARSE: "rev-parse",
|
|
3150
|
-
ABBREV_REF: "--abbrev-ref",
|
|
3151
|
-
GIT_COMMON_DIR: "--git-common-dir",
|
|
3152
|
-
HEAD: "HEAD",
|
|
3153
|
-
SHOW_TOPLEVEL: "--show-toplevel",
|
|
3154
|
-
SYMBOLIC_REF: "symbolic-ref",
|
|
3155
|
-
SHORT: "--short",
|
|
3156
|
-
ORIGIN_HEAD_REF: "refs/remotes/origin/HEAD",
|
|
3157
|
-
STATUS: "status",
|
|
3158
|
-
PORCELAIN: "--porcelain",
|
|
3159
|
-
PATH_FORMAT_ABSOLUTE: "--path-format=absolute"
|
|
3160
|
-
};
|
|
3161
|
-
var ORIGIN_REF_PREFIX = "origin/";
|
|
3162
|
-
var GIT_SHOW_TOPLEVEL_ARGS = [
|
|
3163
|
-
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3164
|
-
GIT_ROOT_COMMAND.SHOW_TOPLEVEL
|
|
3165
|
-
];
|
|
3166
|
-
var GIT_COMMON_DIR_ARGS = [
|
|
3167
|
-
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3168
|
-
GIT_ROOT_COMMAND.PATH_FORMAT_ABSOLUTE,
|
|
3169
|
-
GIT_ROOT_COMMAND.GIT_COMMON_DIR
|
|
3170
|
-
];
|
|
3171
|
-
var GIT_CURRENT_BRANCH_ARGS = [
|
|
3172
|
-
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3173
|
-
GIT_ROOT_COMMAND.ABBREV_REF,
|
|
3174
|
-
GIT_ROOT_COMMAND.HEAD
|
|
3175
|
-
];
|
|
3176
|
-
var GIT_HEAD_SHA_ARGS = [
|
|
3177
|
-
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3178
|
-
GIT_ROOT_COMMAND.HEAD
|
|
3179
|
-
];
|
|
3180
|
-
var GIT_ORIGIN_HEAD_REF_ARGS = [
|
|
3181
|
-
GIT_ROOT_COMMAND.SYMBOLIC_REF,
|
|
3182
|
-
GIT_ROOT_COMMAND.SHORT,
|
|
3183
|
-
GIT_ROOT_COMMAND.ORIGIN_HEAD_REF
|
|
3184
|
-
];
|
|
3185
|
-
var GIT_STATUS_PORCELAIN_ARGS = [
|
|
3186
|
-
GIT_ROOT_COMMAND.STATUS,
|
|
3187
|
-
GIT_ROOT_COMMAND.PORCELAIN
|
|
3188
|
-
];
|
|
3189
|
-
async function detectWorktreeProductRoot(cwd = process.cwd(), deps = defaultDeps) {
|
|
3190
|
-
try {
|
|
3191
|
-
const result = await deps.execa(
|
|
3192
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3193
|
-
[...GIT_SHOW_TOPLEVEL_ARGS],
|
|
3194
|
-
{ cwd, reject: false }
|
|
3195
|
-
);
|
|
3196
|
-
if (result.exitCode === 0 && result.stdout) {
|
|
3197
|
-
return {
|
|
3198
|
-
productDir: extractStdout(result.stdout),
|
|
3199
|
-
isGitRepo: true
|
|
3200
|
-
};
|
|
3201
|
-
}
|
|
3202
|
-
return {
|
|
3203
|
-
productDir: cwd,
|
|
3204
|
-
isGitRepo: false,
|
|
3205
|
-
warning: NOT_GIT_REPO_WARNING
|
|
3206
|
-
};
|
|
3207
|
-
} catch {
|
|
3208
|
-
return {
|
|
3209
|
-
productDir: cwd,
|
|
3210
|
-
isGitRepo: false,
|
|
3211
|
-
warning: NOT_GIT_REPO_WARNING
|
|
3212
|
-
};
|
|
3213
|
-
}
|
|
3214
|
-
}
|
|
3215
|
-
function extractStdout(stdout) {
|
|
3216
|
-
if (!stdout) return "";
|
|
3217
|
-
const str = typeof stdout === "string" ? stdout : String(stdout);
|
|
3218
|
-
return str.trim().replace(/\/+$/, "");
|
|
3219
|
-
}
|
|
3220
|
-
async function detectGitCommonDirProductRoot(cwd = process.cwd(), deps = defaultDeps) {
|
|
3221
|
-
try {
|
|
3222
|
-
const toplevelResult = await deps.execa(
|
|
3223
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3224
|
-
[...GIT_SHOW_TOPLEVEL_ARGS],
|
|
3225
|
-
{ cwd, reject: false }
|
|
3226
|
-
);
|
|
3227
|
-
if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) {
|
|
3228
|
-
return {
|
|
3229
|
-
productDir: cwd,
|
|
3230
|
-
isGitRepo: false,
|
|
3231
|
-
warning: NOT_GIT_REPO_WARNING,
|
|
3232
|
-
worktreeRoot: cwd
|
|
3233
|
-
};
|
|
3234
|
-
}
|
|
3235
|
-
const toplevel = extractStdout(toplevelResult.stdout);
|
|
3236
|
-
const commonDirResult = await deps.execa(
|
|
3237
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3238
|
-
[...GIT_COMMON_DIR_ARGS],
|
|
3239
|
-
{ cwd, reject: false }
|
|
3240
|
-
);
|
|
3241
|
-
if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
|
|
3242
|
-
return {
|
|
3243
|
-
productDir: toplevel,
|
|
3244
|
-
isGitRepo: true,
|
|
3245
|
-
worktreeRoot: toplevel
|
|
3246
|
-
};
|
|
3716
|
+
};
|
|
3717
|
+
var SessionWorkBranchNotOnOriginError = class extends SessionError {
|
|
3718
|
+
/** The work-branch ref the caller supplied that does not resolve on `origin`. */
|
|
3719
|
+
workBranch;
|
|
3720
|
+
constructor(workBranch) {
|
|
3721
|
+
super(
|
|
3722
|
+
`Work-branch ref not found on origin: ${workBranch}. Push the branch to origin before recording it as the handoff base, or omit git_ref to record the git context.`
|
|
3723
|
+
);
|
|
3724
|
+
this.name = "SessionWorkBranchNotOnOriginError";
|
|
3725
|
+
this.workBranch = workBranch;
|
|
3726
|
+
}
|
|
3727
|
+
};
|
|
3728
|
+
var SessionInvalidJsonHeaderError = class extends SessionError {
|
|
3729
|
+
constructor(reason) {
|
|
3730
|
+
super(`Invalid JSON header for handoff: ${reason}`);
|
|
3731
|
+
this.name = "SessionInvalidJsonHeaderError";
|
|
3732
|
+
}
|
|
3733
|
+
};
|
|
3734
|
+
|
|
3735
|
+
// src/commands/session/resolve-config.ts
|
|
3736
|
+
import { join as join6 } from "path";
|
|
3737
|
+
|
|
3738
|
+
// src/config/defaults.ts
|
|
3739
|
+
var DEFAULT_CONFIG = {
|
|
3740
|
+
sessions: {
|
|
3741
|
+
statusDirs: {
|
|
3742
|
+
todo: "todo",
|
|
3743
|
+
doing: "doing",
|
|
3744
|
+
archive: "archive"
|
|
3247
3745
|
}
|
|
3248
|
-
const commonDir = extractStdout(commonDirResult.stdout);
|
|
3249
|
-
const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve3(toplevel, commonDir);
|
|
3250
|
-
const gitCommonDirProductRoot = dirname(absoluteCommonDir);
|
|
3251
|
-
return {
|
|
3252
|
-
productDir: gitCommonDirProductRoot,
|
|
3253
|
-
isGitRepo: true,
|
|
3254
|
-
worktreeRoot: toplevel
|
|
3255
|
-
};
|
|
3256
|
-
} catch {
|
|
3257
|
-
return {
|
|
3258
|
-
productDir: cwd,
|
|
3259
|
-
isGitRepo: false,
|
|
3260
|
-
warning: NOT_GIT_REPO_WARNING,
|
|
3261
|
-
worktreeRoot: cwd
|
|
3262
|
-
};
|
|
3263
3746
|
}
|
|
3264
|
-
}
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3268
|
-
[...GIT_ORIGIN_HEAD_REF_ARGS],
|
|
3269
|
-
{ cwd, reject: false }
|
|
3270
|
-
);
|
|
3271
|
-
if (result.exitCode !== 0) return null;
|
|
3272
|
-
const ref = extractStdout(result.stdout);
|
|
3273
|
-
if (!ref.startsWith(ORIGIN_REF_PREFIX)) return null;
|
|
3274
|
-
const branch = ref.slice(ORIGIN_REF_PREFIX.length);
|
|
3275
|
-
return branch.length === 0 ? null : branch;
|
|
3276
|
-
}
|
|
3277
|
-
async function getCurrentBranch(cwd = process.cwd(), deps = defaultDeps) {
|
|
3278
|
-
const result = await deps.execa(
|
|
3279
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3280
|
-
[...GIT_CURRENT_BRANCH_ARGS],
|
|
3281
|
-
{ cwd, reject: false }
|
|
3282
|
-
);
|
|
3283
|
-
if (result.exitCode !== 0) return null;
|
|
3284
|
-
const branch = extractStdout(result.stdout);
|
|
3285
|
-
if (branch.length === 0 || branch === GIT_ROOT_COMMAND.HEAD) return null;
|
|
3286
|
-
return branch;
|
|
3287
|
-
}
|
|
3288
|
-
async function getHeadSha(cwd = process.cwd(), deps = defaultDeps) {
|
|
3289
|
-
const result = await deps.execa(
|
|
3290
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3291
|
-
[...GIT_HEAD_SHA_ARGS],
|
|
3292
|
-
{ cwd, reject: false }
|
|
3293
|
-
);
|
|
3294
|
-
if (result.exitCode !== 0) return null;
|
|
3295
|
-
const sha = extractStdout(result.stdout);
|
|
3296
|
-
return sha.length === 0 ? null : sha;
|
|
3297
|
-
}
|
|
3298
|
-
async function resolveRefSha(ref, cwd = process.cwd(), deps = defaultDeps) {
|
|
3299
|
-
const result = await deps.execa(
|
|
3300
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3301
|
-
[GIT_ROOT_COMMAND.REV_PARSE, ref],
|
|
3302
|
-
{ cwd, reject: false }
|
|
3303
|
-
);
|
|
3304
|
-
if (result.exitCode !== 0) return null;
|
|
3305
|
-
const sha = extractStdout(result.stdout);
|
|
3306
|
-
return sha.length === 0 ? null : sha;
|
|
3307
|
-
}
|
|
3308
|
-
async function isWorkingTreeClean(cwd = process.cwd(), deps = defaultDeps) {
|
|
3309
|
-
const result = await deps.execa(
|
|
3310
|
-
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3311
|
-
[...GIT_STATUS_PORCELAIN_ARGS],
|
|
3312
|
-
{ cwd, reject: false }
|
|
3313
|
-
);
|
|
3314
|
-
if (result.exitCode !== 0) return false;
|
|
3315
|
-
return extractStdout(result.stdout).length === 0;
|
|
3316
|
-
}
|
|
3747
|
+
};
|
|
3748
|
+
|
|
3749
|
+
// src/commands/session/resolve-config.ts
|
|
3317
3750
|
async function resolveSessionConfig(options = {}) {
|
|
3318
3751
|
const { sessionsDir, cwd, deps } = options;
|
|
3319
3752
|
const { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
|
|
3320
3753
|
if (sessionsDir) {
|
|
3321
3754
|
return {
|
|
3322
3755
|
config: {
|
|
3323
|
-
todoDir:
|
|
3324
|
-
doingDir:
|
|
3325
|
-
archiveDir:
|
|
3756
|
+
todoDir: join6(sessionsDir, statusDirs2.todo),
|
|
3757
|
+
doingDir: join6(sessionsDir, statusDirs2.doing),
|
|
3758
|
+
archiveDir: join6(sessionsDir, statusDirs2.archive)
|
|
3326
3759
|
}
|
|
3327
3760
|
};
|
|
3328
3761
|
}
|
|
3329
|
-
const
|
|
3330
|
-
const baseDir = join3(gitResult.productDir, DEFAULT_CONFIG.sessions.dir);
|
|
3762
|
+
const { sessionsDir: baseDir, warning } = await resolveSessionsScopeDir({ cwd, deps });
|
|
3331
3763
|
return {
|
|
3332
3764
|
config: {
|
|
3333
|
-
todoDir:
|
|
3334
|
-
doingDir:
|
|
3335
|
-
archiveDir:
|
|
3765
|
+
todoDir: join6(baseDir, statusDirs2.todo),
|
|
3766
|
+
doingDir: join6(baseDir, statusDirs2.doing),
|
|
3767
|
+
archiveDir: join6(baseDir, statusDirs2.archive)
|
|
3336
3768
|
},
|
|
3337
|
-
warning
|
|
3769
|
+
warning
|
|
3338
3770
|
};
|
|
3339
3771
|
}
|
|
3340
|
-
|
|
3341
|
-
// src/commands/session/resolve-config.ts
|
|
3342
3772
|
async function resolveSessionConfigSurfacingWarning(sessionsDir, onWarning) {
|
|
3343
3773
|
const { config, warning } = await resolveSessionConfig({ sessionsDir });
|
|
3344
3774
|
if (warning !== void 0) {
|
|
@@ -3371,9 +3801,9 @@ async function fileExists(path6) {
|
|
|
3371
3801
|
}
|
|
3372
3802
|
async function probeSessionPaths(sessionId, config) {
|
|
3373
3803
|
const filename = `${sessionId}${SESSION_FILE_EXTENSION}`;
|
|
3374
|
-
const todoPath =
|
|
3375
|
-
const doingPath =
|
|
3376
|
-
const archivePath =
|
|
3804
|
+
const todoPath = join7(config.todoDir, filename);
|
|
3805
|
+
const doingPath = join7(config.doingDir, filename);
|
|
3806
|
+
const archivePath = join7(config.archiveDir, filename);
|
|
3377
3807
|
return {
|
|
3378
3808
|
todo: await fileExists(todoPath) ? todoPath : null,
|
|
3379
3809
|
doing: await fileExists(doingPath) ? doingPath : null,
|
|
@@ -3394,7 +3824,7 @@ async function resolveArchivePaths(sessionId, config) {
|
|
|
3394
3824
|
}
|
|
3395
3825
|
async function archiveSingle(sessionId, config) {
|
|
3396
3826
|
const { source, target } = await resolveArchivePaths(sessionId, config);
|
|
3397
|
-
await mkdir(
|
|
3827
|
+
await mkdir(dirname3(target), { recursive: true });
|
|
3398
3828
|
await rename(source, target);
|
|
3399
3829
|
return `${SESSION_ARCHIVE_OUTPUT.ARCHIVED}: ${sessionId}
|
|
3400
3830
|
${SESSION_ARCHIVE_OUTPUT.ARCHIVE_LOCATION}: ${target}`;
|
|
@@ -3417,7 +3847,7 @@ function resolveDeletePath(sessionId, existingPaths) {
|
|
|
3417
3847
|
}
|
|
3418
3848
|
|
|
3419
3849
|
// src/domains/session/show.ts
|
|
3420
|
-
import { join as
|
|
3850
|
+
import { join as join8 } from "path";
|
|
3421
3851
|
|
|
3422
3852
|
// src/domains/session/list.ts
|
|
3423
3853
|
import { parse as parseYaml2 } from "yaml";
|
|
@@ -3428,12 +3858,12 @@ var SESSION_ID_SEPARATOR = "_";
|
|
|
3428
3858
|
function generateSessionId(options = {}) {
|
|
3429
3859
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3430
3860
|
const date = now();
|
|
3431
|
-
const year = date.
|
|
3432
|
-
const month = String(date.
|
|
3433
|
-
const day = String(date.
|
|
3434
|
-
const hours = String(date.
|
|
3435
|
-
const minutes = String(date.
|
|
3436
|
-
const seconds = String(date.
|
|
3861
|
+
const year = date.getUTCFullYear();
|
|
3862
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
3863
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
3864
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
3865
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
3866
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
3437
3867
|
return `${year}-${month}-${day}${SESSION_ID_SEPARATOR}${hours}-${minutes}-${seconds}`;
|
|
3438
3868
|
}
|
|
3439
3869
|
function parseSessionId(id) {
|
|
@@ -3453,7 +3883,11 @@ function parseSessionId(id) {
|
|
|
3453
3883
|
if (hours < 0 || hours > 23) return null;
|
|
3454
3884
|
if (minutes < 0 || minutes > 59) return null;
|
|
3455
3885
|
if (seconds < 0 || seconds > 59) return null;
|
|
3456
|
-
|
|
3886
|
+
const date = new Date(Date.UTC(year, month, day, hours, minutes, seconds));
|
|
3887
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day || date.getUTCHours() !== hours || date.getUTCMinutes() !== minutes || date.getUTCSeconds() !== seconds) {
|
|
3888
|
+
return null;
|
|
3889
|
+
}
|
|
3890
|
+
return date;
|
|
3457
3891
|
}
|
|
3458
3892
|
|
|
3459
3893
|
// src/domains/session/types.ts
|
|
@@ -3556,6 +3990,67 @@ function sortSessions(sessions) {
|
|
|
3556
3990
|
return dateB.getTime() - dateA.getTime();
|
|
3557
3991
|
});
|
|
3558
3992
|
}
|
|
3993
|
+
var FIELD_SELECTION_SEPARATOR = ",";
|
|
3994
|
+
var SESSION_RECORD_FIELD = {
|
|
3995
|
+
ID: "id",
|
|
3996
|
+
STATUS: "status",
|
|
3997
|
+
PRIORITY: SESSION_FRONT_MATTER.PRIORITY,
|
|
3998
|
+
GIT_REF: SESSION_FRONT_MATTER.GIT_REF,
|
|
3999
|
+
GOAL: SESSION_FRONT_MATTER.GOAL,
|
|
4000
|
+
NEXT_STEP: SESSION_FRONT_MATTER.NEXT_STEP,
|
|
4001
|
+
SPECS: SESSION_FRONT_MATTER.SPECS,
|
|
4002
|
+
FILES: SESSION_FRONT_MATTER.FILES,
|
|
4003
|
+
CREATED_AT: SESSION_FRONT_MATTER.CREATED_AT,
|
|
4004
|
+
AGENT_SESSION_ID: SESSION_FRONT_MATTER.AGENT_SESSION_ID
|
|
4005
|
+
};
|
|
4006
|
+
var SESSION_RECORD_FIELDS = Object.values(SESSION_RECORD_FIELD);
|
|
4007
|
+
function toSessionRecord(session) {
|
|
4008
|
+
const { id, status, metadata } = session;
|
|
4009
|
+
const record = {
|
|
4010
|
+
id,
|
|
4011
|
+
status,
|
|
4012
|
+
priority: metadata.priority,
|
|
4013
|
+
git_ref: metadata.git_ref,
|
|
4014
|
+
goal: metadata.goal,
|
|
4015
|
+
next_step: metadata.next_step,
|
|
4016
|
+
specs: metadata.specs,
|
|
4017
|
+
files: metadata.files
|
|
4018
|
+
};
|
|
4019
|
+
if (metadata.created_at !== void 0) {
|
|
4020
|
+
record.created_at = metadata.created_at;
|
|
4021
|
+
}
|
|
4022
|
+
if (metadata.agent_session_id !== void 0) {
|
|
4023
|
+
record.agent_session_id = metadata.agent_session_id;
|
|
4024
|
+
}
|
|
4025
|
+
return record;
|
|
4026
|
+
}
|
|
4027
|
+
function projectSessionRecord(record, fields) {
|
|
4028
|
+
const projected = {};
|
|
4029
|
+
for (const field of fields) {
|
|
4030
|
+
const value = record[field];
|
|
4031
|
+
if (value !== void 0) {
|
|
4032
|
+
projected[field] = value;
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
return projected;
|
|
4036
|
+
}
|
|
4037
|
+
function isSessionRecordField(value) {
|
|
4038
|
+
return SESSION_RECORD_FIELDS.includes(value);
|
|
4039
|
+
}
|
|
4040
|
+
function parseFieldSelection(input) {
|
|
4041
|
+
const tokens = input.split(FIELD_SELECTION_SEPARATOR).map((token) => token.trim()).filter((token) => token.length > 0);
|
|
4042
|
+
if (tokens.length === 0) {
|
|
4043
|
+
throw new SessionInvalidFieldError(input, SESSION_RECORD_FIELDS);
|
|
4044
|
+
}
|
|
4045
|
+
const selection = [];
|
|
4046
|
+
for (const token of tokens) {
|
|
4047
|
+
if (!isSessionRecordField(token)) {
|
|
4048
|
+
throw new SessionInvalidFieldError(token, SESSION_RECORD_FIELDS);
|
|
4049
|
+
}
|
|
4050
|
+
selection.push(token);
|
|
4051
|
+
}
|
|
4052
|
+
return selection;
|
|
4053
|
+
}
|
|
3559
4054
|
|
|
3560
4055
|
// src/domains/session/show.ts
|
|
3561
4056
|
var SESSION_SHOW_LABEL = {
|
|
@@ -3569,11 +4064,12 @@ var SESSION_SHOW_LABEL = {
|
|
|
3569
4064
|
};
|
|
3570
4065
|
var SESSION_SHOW_SEPARATOR_CHAR = "\u2500";
|
|
3571
4066
|
var SESSION_SHOW_SEPARATOR_WIDTH = 40;
|
|
3572
|
-
var
|
|
4067
|
+
var sessionsBaseDir = join8(STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
|
|
4068
|
+
var { statusDirs } = DEFAULT_CONFIG.sessions;
|
|
3573
4069
|
var DEFAULT_SESSION_CONFIG = {
|
|
3574
|
-
todoDir:
|
|
3575
|
-
doingDir:
|
|
3576
|
-
archiveDir:
|
|
4070
|
+
todoDir: join8(sessionsBaseDir, statusDirs.todo),
|
|
4071
|
+
doingDir: join8(sessionsBaseDir, statusDirs.doing),
|
|
4072
|
+
archiveDir: join8(sessionsBaseDir, statusDirs.archive)
|
|
3577
4073
|
};
|
|
3578
4074
|
var SEARCH_ORDER = [...SESSION_STATUSES];
|
|
3579
4075
|
function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
|
|
@@ -3631,7 +4127,7 @@ async function deleteCommand(options) {
|
|
|
3631
4127
|
|
|
3632
4128
|
// src/commands/session/handoff.ts
|
|
3633
4129
|
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
3634
|
-
import { join as
|
|
4130
|
+
import { join as join9, resolve as resolve3 } from "path";
|
|
3635
4131
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
3636
4132
|
|
|
3637
4133
|
// src/domains/session/create.ts
|
|
@@ -3652,11 +4148,11 @@ function cleanPrerequisite(facts) {
|
|
|
3652
4148
|
return {
|
|
3653
4149
|
label: HANDOFF_BASE_PREREQUISITE_LABEL.CLEAN_WORKING_TREE,
|
|
3654
4150
|
met: facts.isClean,
|
|
3655
|
-
remedy: facts.isClean ? "" : HANDOFF_BASE_REMEDY.
|
|
4151
|
+
remedy: facts.isClean ? "" : HANDOFF_BASE_REMEDY.COMMIT_OR_MAIN_CHECKOUT
|
|
3656
4152
|
};
|
|
3657
4153
|
}
|
|
3658
4154
|
function detachedAtTipPrerequisite(facts, met) {
|
|
3659
|
-
const remedy = facts.defaultTipSha === null ? HANDOFF_BASE_REMEDY.
|
|
4155
|
+
const remedy = facts.defaultTipSha === null ? HANDOFF_BASE_REMEDY.MAIN_CHECKOUT_ONLY : HANDOFF_BASE_REMEDY.DETACH_TO_TIP_OR_MAIN_CHECKOUT;
|
|
3660
4156
|
return {
|
|
3661
4157
|
label: HANDOFF_BASE_PREREQUISITE_LABEL.DETACHED_AT_DEFAULT_TIP,
|
|
3662
4158
|
met,
|
|
@@ -3669,7 +4165,7 @@ function buildChecklist(facts, prerequisites) {
|
|
|
3669
4165
|
defaultTipSha: facts.defaultTipSha,
|
|
3670
4166
|
headSha: facts.headSha,
|
|
3671
4167
|
currentWorktreePath: facts.currentWorktreePath,
|
|
3672
|
-
|
|
4168
|
+
mainCheckoutPath: facts.mainCheckoutPath,
|
|
3673
4169
|
prerequisites
|
|
3674
4170
|
};
|
|
3675
4171
|
}
|
|
@@ -3677,7 +4173,7 @@ function resolveHandoffGitRef(facts) {
|
|
|
3677
4173
|
if (!facts.isGitRepo) {
|
|
3678
4174
|
throw new SessionHandoffBaseError({ silent: true });
|
|
3679
4175
|
}
|
|
3680
|
-
if (facts.
|
|
4176
|
+
if (facts.isMainCheckout) {
|
|
3681
4177
|
if (facts.branch !== null) return facts.branch;
|
|
3682
4178
|
if (facts.headSha !== null) return facts.headSha;
|
|
3683
4179
|
throw new SessionHandoffBaseError();
|
|
@@ -3692,6 +4188,12 @@ function resolveHandoffGitRef(facts) {
|
|
|
3692
4188
|
}
|
|
3693
4189
|
throw new SessionHandoffBaseError({ checklist: buildChecklist(facts, prerequisites) });
|
|
3694
4190
|
}
|
|
4191
|
+
function resolveWorkBranchGitRef(workBranch, existsOnOrigin) {
|
|
4192
|
+
if (!existsOnOrigin) {
|
|
4193
|
+
throw new SessionWorkBranchNotOnOriginError(workBranch);
|
|
4194
|
+
}
|
|
4195
|
+
return workBranch;
|
|
4196
|
+
}
|
|
3695
4197
|
|
|
3696
4198
|
// src/domains/session/parse-handoff-input.ts
|
|
3697
4199
|
var LEGACY_FRONTMATTER_PREFIX = /^---\r?\n/;
|
|
@@ -3782,12 +4284,14 @@ function validateHandoffHeader(parsed) {
|
|
|
3782
4284
|
const nextStep = ensureStringOrEmpty(obj[SESSION_FRONT_MATTER.NEXT_STEP], SESSION_FRONT_MATTER.NEXT_STEP);
|
|
3783
4285
|
const specs = ensureStringArrayOrDefault(obj[SESSION_FRONT_MATTER.SPECS], SESSION_FRONT_MATTER.SPECS);
|
|
3784
4286
|
const files = ensureStringArrayOrDefault(obj[SESSION_FRONT_MATTER.FILES], SESSION_FRONT_MATTER.FILES);
|
|
4287
|
+
const gitRef = ensureOptionalString(obj[SESSION_FRONT_MATTER.GIT_REF], SESSION_FRONT_MATTER.GIT_REF);
|
|
3785
4288
|
return {
|
|
3786
4289
|
priority,
|
|
3787
4290
|
goal,
|
|
3788
4291
|
next_step: nextStep,
|
|
3789
4292
|
specs,
|
|
3790
|
-
files
|
|
4293
|
+
files,
|
|
4294
|
+
...gitRef === void 0 ? {} : { git_ref: gitRef }
|
|
3791
4295
|
};
|
|
3792
4296
|
}
|
|
3793
4297
|
function ensurePriorityOrDefault(value) {
|
|
@@ -3806,6 +4310,13 @@ function ensureStringOrEmpty(value, fieldName) {
|
|
|
3806
4310
|
}
|
|
3807
4311
|
return value;
|
|
3808
4312
|
}
|
|
4313
|
+
function ensureOptionalString(value, fieldName) {
|
|
4314
|
+
if (value === void 0) return void 0;
|
|
4315
|
+
if (typeof value !== "string") {
|
|
4316
|
+
throw new SessionInvalidJsonHeaderError(`${fieldName} must be a string`);
|
|
4317
|
+
}
|
|
4318
|
+
return value;
|
|
4319
|
+
}
|
|
3809
4320
|
function ensureStringArrayOrDefault(value, fieldName) {
|
|
3810
4321
|
if (value === void 0) return [];
|
|
3811
4322
|
if (!Array.isArray(value)) {
|
|
@@ -3821,18 +4332,19 @@ function ensureStringArrayOrDefault(value, fieldName) {
|
|
|
3821
4332
|
|
|
3822
4333
|
// src/commands/session/handoff.ts
|
|
3823
4334
|
async function resolveSessionGitRef(cwd, deps) {
|
|
3824
|
-
const [branch, headSha,
|
|
4335
|
+
const [branch, headSha, facts] = await Promise.all([
|
|
3825
4336
|
getCurrentBranch(cwd, deps),
|
|
3826
4337
|
getHeadSha(cwd, deps),
|
|
3827
|
-
|
|
4338
|
+
gatherGitFacts(cwd, deps)
|
|
3828
4339
|
]);
|
|
3829
|
-
const
|
|
3830
|
-
const
|
|
3831
|
-
const
|
|
4340
|
+
const isGitRepo = facts !== null;
|
|
4341
|
+
const currentWorktreePath = facts?.worktreeRoot ?? cwd ?? process.cwd();
|
|
4342
|
+
const isMain = facts !== null && isMainCheckout(facts);
|
|
4343
|
+
const designatedMainCheckout = facts !== null ? mainCheckoutPath(facts) : null;
|
|
3832
4344
|
let isClean = false;
|
|
3833
4345
|
let defaultBranch = null;
|
|
3834
4346
|
let defaultTipSha = null;
|
|
3835
|
-
if (
|
|
4347
|
+
if (isGitRepo && !isMain) {
|
|
3836
4348
|
[isClean, defaultBranch] = await Promise.all([
|
|
3837
4349
|
isWorkingTreeClean(cwd, deps),
|
|
3838
4350
|
resolveDefaultBranch(cwd, deps)
|
|
@@ -3840,17 +4352,22 @@ async function resolveSessionGitRef(cwd, deps) {
|
|
|
3840
4352
|
defaultTipSha = defaultBranch === null ? null : await resolveRefSha(`${ORIGIN_REF_PREFIX}${defaultBranch}`, cwd, deps);
|
|
3841
4353
|
}
|
|
3842
4354
|
return resolveHandoffGitRef({
|
|
3843
|
-
isGitRepo
|
|
3844
|
-
|
|
4355
|
+
isGitRepo,
|
|
4356
|
+
isMainCheckout: isMain,
|
|
3845
4357
|
branch,
|
|
3846
4358
|
headSha,
|
|
3847
4359
|
isClean,
|
|
3848
4360
|
defaultBranch,
|
|
3849
4361
|
defaultTipSha,
|
|
3850
4362
|
currentWorktreePath,
|
|
3851
|
-
|
|
4363
|
+
mainCheckoutPath: designatedMainCheckout
|
|
3852
4364
|
});
|
|
3853
4365
|
}
|
|
4366
|
+
async function resolveRecordedGitRef(suppliedRef, gateRef, cwd, deps) {
|
|
4367
|
+
if (suppliedRef === void 0 || suppliedRef.length === 0) return gateRef;
|
|
4368
|
+
const existsOnOrigin = await originBranchExists(suppliedRef, cwd, deps);
|
|
4369
|
+
return resolveWorkBranchGitRef(suppliedRef, existsOnOrigin);
|
|
4370
|
+
}
|
|
3854
4371
|
async function handoffCommand(options) {
|
|
3855
4372
|
const { config } = await resolveSessionConfig({
|
|
3856
4373
|
sessionsDir: options.sessionsDir,
|
|
@@ -3868,9 +4385,10 @@ async function handoffCommand(options) {
|
|
|
3868
4385
|
if (header.next_step.length === 0) {
|
|
3869
4386
|
throw new SessionInvalidNextStepError();
|
|
3870
4387
|
}
|
|
3871
|
-
const
|
|
4388
|
+
const gateRef = await resolveSessionGitRef(options.cwd, options.deps);
|
|
4389
|
+
const gitRef = await resolveRecordedGitRef(header.git_ref, gateRef, options.cwd, options.deps);
|
|
3872
4390
|
const sessionId = generateSessionId();
|
|
3873
|
-
const agentSessionId =
|
|
4391
|
+
const agentSessionId = resolveAgentSessionId(options.env ?? process.env);
|
|
3874
4392
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3875
4393
|
const frontMatterObject = {
|
|
3876
4394
|
[SESSION_FRONT_MATTER.PRIORITY]: header.priority,
|
|
@@ -3887,8 +4405,8 @@ async function handoffCommand(options) {
|
|
|
3887
4405
|
const yaml = stringifyYaml3(frontMatterObject, { defaultStringType: "QUOTE_DOUBLE" }).trimEnd();
|
|
3888
4406
|
const fullContent = `${SESSION_FRONT_MATTER_OPEN}${yaml}${SESSION_FRONT_MATTER_CLOSE}${body}`;
|
|
3889
4407
|
const filename = `${sessionId}.md`;
|
|
3890
|
-
const sessionPath =
|
|
3891
|
-
const absolutePath =
|
|
4408
|
+
const sessionPath = join9(config.todoDir, filename);
|
|
4409
|
+
const absolutePath = resolve3(sessionPath);
|
|
3892
4410
|
await mkdir2(config.todoDir, { recursive: true });
|
|
3893
4411
|
await writeFile(sessionPath, fullContent, "utf-8");
|
|
3894
4412
|
const output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
|
|
@@ -3898,7 +4416,7 @@ async function handoffCommand(options) {
|
|
|
3898
4416
|
|
|
3899
4417
|
// src/commands/session/list.ts
|
|
3900
4418
|
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
3901
|
-
import { join as
|
|
4419
|
+
import { join as join10 } from "path";
|
|
3902
4420
|
var SESSION_LIST_FORMAT = {
|
|
3903
4421
|
TEXT: "text",
|
|
3904
4422
|
JSON: "json"
|
|
@@ -3912,7 +4430,7 @@ async function loadSessionsFromDir(dir, status) {
|
|
|
3912
4430
|
for (const file of files) {
|
|
3913
4431
|
if (!file.endsWith(".md")) continue;
|
|
3914
4432
|
const id = file.replace(".md", "");
|
|
3915
|
-
const filePath =
|
|
4433
|
+
const filePath = join10(dir, file);
|
|
3916
4434
|
const content = await readFile3(filePath, "utf-8");
|
|
3917
4435
|
const metadata = parseSessionMetadata(content);
|
|
3918
4436
|
sessions.push({
|
|
@@ -3954,6 +4472,7 @@ function validateStatus(input) {
|
|
|
3954
4472
|
);
|
|
3955
4473
|
}
|
|
3956
4474
|
async function listCommand(options) {
|
|
4475
|
+
const fieldSelection = options.fields !== void 0 ? parseFieldSelection(options.fields) : void 0;
|
|
3957
4476
|
const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
|
|
3958
4477
|
const statuses = options.status !== void 0 ? [validateStatus(options.status)] : DEFAULT_LIST_STATUSES;
|
|
3959
4478
|
const sessionsByStatus = {};
|
|
@@ -3962,8 +4481,16 @@ async function listCommand(options) {
|
|
|
3962
4481
|
const sessions = await loadSessionsFromDir(config[dirKey], status);
|
|
3963
4482
|
sessionsByStatus[status] = sortSessions(sessions);
|
|
3964
4483
|
}
|
|
3965
|
-
|
|
3966
|
-
|
|
4484
|
+
const emitJson = fieldSelection !== void 0 || options.format === SESSION_LIST_FORMAT.JSON;
|
|
4485
|
+
if (emitJson) {
|
|
4486
|
+
const recordsByStatus = {};
|
|
4487
|
+
for (const status of statuses) {
|
|
4488
|
+
recordsByStatus[status] = (sessionsByStatus[status] ?? []).map((session) => {
|
|
4489
|
+
const record = toSessionRecord(session);
|
|
4490
|
+
return fieldSelection !== void 0 ? projectSessionRecord(record, fieldSelection) : record;
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
return JSON.stringify(recordsByStatus, null, 2);
|
|
3967
4494
|
}
|
|
3968
4495
|
const lines = [];
|
|
3969
4496
|
for (const status of statuses) {
|
|
@@ -3976,7 +4503,7 @@ async function listCommand(options) {
|
|
|
3976
4503
|
|
|
3977
4504
|
// src/commands/session/pickup.ts
|
|
3978
4505
|
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rename as rename2 } from "fs/promises";
|
|
3979
|
-
import { join as
|
|
4506
|
+
import { join as join11 } from "path";
|
|
3980
4507
|
|
|
3981
4508
|
// src/domains/session/pickup.ts
|
|
3982
4509
|
function buildClaimPaths(sessionId, config) {
|
|
@@ -4021,7 +4548,7 @@ async function loadTodoSessions(config) {
|
|
|
4021
4548
|
for (const file of files) {
|
|
4022
4549
|
if (!file.endsWith(".md")) continue;
|
|
4023
4550
|
const id = file.replace(".md", "");
|
|
4024
|
-
const filePath =
|
|
4551
|
+
const filePath = join11(config.todoDir, file);
|
|
4025
4552
|
const content = await readFile4(filePath, "utf-8");
|
|
4026
4553
|
const metadata = parseSessionMetadata(content);
|
|
4027
4554
|
sessions.push({
|
|
@@ -4077,7 +4604,7 @@ async function pickupCommand(options) {
|
|
|
4077
4604
|
|
|
4078
4605
|
// src/commands/session/prune.ts
|
|
4079
4606
|
import { readdir as readdir3, readFile as readFile5, unlink as unlink2 } from "fs/promises";
|
|
4080
|
-
import { join as
|
|
4607
|
+
import { join as join12 } from "path";
|
|
4081
4608
|
|
|
4082
4609
|
// src/domains/session/prune.ts
|
|
4083
4610
|
var DEFAULT_KEEP_COUNT = 5;
|
|
@@ -4126,7 +4653,7 @@ async function loadArchiveSessions(config) {
|
|
|
4126
4653
|
for (const file of files) {
|
|
4127
4654
|
if (!file.endsWith(".md")) continue;
|
|
4128
4655
|
const id = file.replace(".md", "");
|
|
4129
|
-
const filePath =
|
|
4656
|
+
const filePath = join12(config.archiveDir, file);
|
|
4130
4657
|
const content = await readFile5(filePath, "utf-8");
|
|
4131
4658
|
const metadata = parseSessionMetadata(content);
|
|
4132
4659
|
sessions.push({
|
|
@@ -4316,9 +4843,9 @@ Prefilled by the CLI:
|
|
|
4316
4843
|
created_at, git_ref, and agent_session_id when an agent session ID is
|
|
4317
4844
|
available.
|
|
4318
4845
|
|
|
4319
|
-
git_ref records the branch name (
|
|
4320
|
-
(detached), or the origin/<default> tip SHA (clean detached
|
|
4321
|
-
Handoff is refused from any other
|
|
4846
|
+
git_ref records the branch name (main checkout on a branch), the HEAD SHA
|
|
4847
|
+
(detached), or the origin/<default> tip SHA (clean detached non-main checkout).
|
|
4848
|
+
Handoff is refused from any other non-main checkout state.
|
|
4322
4849
|
|
|
4323
4850
|
Output Tags (for automation):
|
|
4324
4851
|
<HANDOFF_ID>session-id</HANDOFF_ID> Session identifier
|
|
@@ -4368,11 +4895,12 @@ function handleError(error) {
|
|
|
4368
4895
|
process.exit(1);
|
|
4369
4896
|
}
|
|
4370
4897
|
function registerSessionCommands(sessionCmd) {
|
|
4371
|
-
sessionCmd.command("list").description("List active sessions (doing + todo by default)").option("--status <status>", "Filter by status (todo|doing|archive); defaults to doing + todo").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
4898
|
+
sessionCmd.command("list").description("List active sessions (doing + todo by default)").option("--status <status>", "Filter by status (todo|doing|archive); defaults to doing + todo").option("--json", "Output as JSON").option("--fields <fields>", "Comma-separated fields to emit as JSON (implies --json)").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
4372
4899
|
try {
|
|
4373
4900
|
const output = await listCommand({
|
|
4374
4901
|
status: options.status,
|
|
4375
|
-
format: options.json ?
|
|
4902
|
+
format: options.json ? SESSION_LIST_FORMAT.JSON : SESSION_LIST_FORMAT.TEXT,
|
|
4903
|
+
fields: options.fields,
|
|
4376
4904
|
sessionsDir: options.sessionsDir,
|
|
4377
4905
|
onWarning: writeWarning
|
|
4378
4906
|
});
|
|
@@ -4381,11 +4909,12 @@ function registerSessionCommands(sessionCmd) {
|
|
|
4381
4909
|
handleError(error);
|
|
4382
4910
|
}
|
|
4383
4911
|
});
|
|
4384
|
-
sessionCmd.command("todo").description("List todo sessions").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
4912
|
+
sessionCmd.command("todo").description("List todo sessions").option("--json", "Output as JSON").option("--fields <fields>", "Comma-separated fields to emit as JSON (implies --json)").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
4385
4913
|
try {
|
|
4386
4914
|
const output = await listCommand({
|
|
4387
4915
|
status: SESSION_STATUSES[0],
|
|
4388
|
-
format: options.json ?
|
|
4916
|
+
format: options.json ? SESSION_LIST_FORMAT.JSON : SESSION_LIST_FORMAT.TEXT,
|
|
4917
|
+
fields: options.fields,
|
|
4389
4918
|
sessionsDir: options.sessionsDir,
|
|
4390
4919
|
onWarning: writeWarning
|
|
4391
4920
|
});
|
|
@@ -4440,7 +4969,8 @@ function registerSessionCommands(sessionCmd) {
|
|
|
4440
4969
|
const content = await readStdin();
|
|
4441
4970
|
const result = await handoffCommand({
|
|
4442
4971
|
content,
|
|
4443
|
-
sessionsDir: options.sessionsDir
|
|
4972
|
+
sessionsDir: options.sessionsDir,
|
|
4973
|
+
env: process.env
|
|
4444
4974
|
});
|
|
4445
4975
|
console.log(result.output);
|
|
4446
4976
|
} catch (error) {
|
|
@@ -4513,7 +5043,7 @@ var sessionDomain = {
|
|
|
4513
5043
|
|
|
4514
5044
|
// src/lib/spec-tree/index.ts
|
|
4515
5045
|
import { readdir as readdir5, readFile as readFile7 } from "fs/promises";
|
|
4516
|
-
import { join as
|
|
5046
|
+
import { join as join13 } from "path";
|
|
4517
5047
|
var SPEC_TREE_FIELD_KEY = {
|
|
4518
5048
|
VERSION: "version",
|
|
4519
5049
|
PRODUCT: "product",
|
|
@@ -4531,7 +5061,9 @@ var SPEC_TREE_ENTRY_TYPE = {
|
|
|
4531
5061
|
PRODUCT: SPEC_TREE_FIELD_KEY.PRODUCT,
|
|
4532
5062
|
NODE: SPEC_TREE_KIND_CATEGORY.NODE,
|
|
4533
5063
|
DECISION: SPEC_TREE_KIND_CATEGORY.DECISION,
|
|
4534
|
-
EVIDENCE: "evidence"
|
|
5064
|
+
EVIDENCE: "evidence",
|
|
5065
|
+
SUPERSEDED: "superseded",
|
|
5066
|
+
INVALID: "invalid"
|
|
4535
5067
|
};
|
|
4536
5068
|
var SPEC_TREE_FILESYSTEM_RECORD_TYPE = {
|
|
4537
5069
|
DIRECTORY: "directory",
|
|
@@ -4542,17 +5074,6 @@ var SPEC_TREE_EVIDENCE_STATUS = {
|
|
|
4542
5074
|
FAILING: SPEC_TREE_NODE_STATE.FAILING,
|
|
4543
5075
|
PASSING: SPEC_TREE_NODE_STATE.PASSING
|
|
4544
5076
|
};
|
|
4545
|
-
var SPEC_TREE_EVIDENCE_FILE = {
|
|
4546
|
-
DIRECTORY_NAME: "tests",
|
|
4547
|
-
MODES: ["scenario", "mapping", "conformance", "property", "compliance"],
|
|
4548
|
-
LEVELS: ["l1", "l2", "l3"],
|
|
4549
|
-
TAILS: {
|
|
4550
|
-
TYPESCRIPT: ["test", "ts"],
|
|
4551
|
-
PYTHON: ["py"],
|
|
4552
|
-
RUST: ["rs"]
|
|
4553
|
-
},
|
|
4554
|
-
SEGMENT_SEPARATOR: "."
|
|
4555
|
-
};
|
|
4556
5077
|
var SPEC_TREE_EVIDENCE_FILE_TAILS = Object.values(SPEC_TREE_EVIDENCE_FILE.TAILS);
|
|
4557
5078
|
var SPEC_TREE_PROJECTION = {
|
|
4558
5079
|
VERSION: 1,
|
|
@@ -4586,30 +5107,33 @@ var SPEC_TREE_NODE_RELATION_KEYS = {
|
|
|
4586
5107
|
DECISIONS: SPEC_TREE_FIELD_KEY.DECISIONS
|
|
4587
5108
|
};
|
|
4588
5109
|
var ORDER_COMPARISON_EQUAL = 0;
|
|
4589
|
-
var SPEC_TREE_PATH_SEPARATOR =
|
|
4590
|
-
var SPEC_TREE_ORDER_SEPARATOR =
|
|
5110
|
+
var SPEC_TREE_PATH_SEPARATOR = SPEC_TREE_GRAMMAR.PATH_SEPARATOR;
|
|
5111
|
+
var SPEC_TREE_ORDER_SEPARATOR = SPEC_TREE_GRAMMAR.ORDER.SEPARATOR;
|
|
4591
5112
|
var SPEC_TREE_ORDER_RADIX = 10;
|
|
4592
5113
|
var SPEC_TREE_TEXT_ENCODING = "utf8";
|
|
4593
5114
|
var SPEC_TREE_EMPTY_RELATIVE_PATH = "";
|
|
4594
|
-
var SPEC_TREE_ORDER_PATTERN =
|
|
5115
|
+
var SPEC_TREE_ORDER_PATTERN = SPEC_TREE_GRAMMAR.ORDER.PATTERN;
|
|
4595
5116
|
var SPEC_TREE_MIN_EVIDENCE_PATH_SEGMENTS = 2;
|
|
4596
5117
|
var SPEC_TREE_PARENT_SEGMENT_OFFSET = 2;
|
|
4597
5118
|
var SPEC_TREE_FIRST_EVIDENCE_MARKER_INDEX = 1;
|
|
4598
5119
|
var SPEC_TREE_EXACTLY_ONE_EVIDENCE_MARKER = 1;
|
|
4599
5120
|
function createFilesystemSpecTreeSource(options) {
|
|
4600
5121
|
const registry = options.registry ?? KIND_REGISTRY;
|
|
5122
|
+
const schemaVersions = options.schemaVersions ?? SPEC_TREE_NAMING_SCHEMA_VERSIONS;
|
|
4601
5123
|
const includePath = options.includePath ?? includeEverySpecTreePath;
|
|
4602
5124
|
return {
|
|
4603
|
-
entries: () => readFilesystemSourceEntries(options.productDir, registry, includePath),
|
|
5125
|
+
entries: () => readFilesystemSourceEntries(options.productDir, registry, schemaVersions, includePath),
|
|
4604
5126
|
async readText(ref) {
|
|
4605
5127
|
if (ref.path === void 0) {
|
|
4606
5128
|
throw new Error("Filesystem source refs require a path");
|
|
4607
5129
|
}
|
|
4608
|
-
return readFile7(
|
|
5130
|
+
return readFile7(join13(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
|
|
4609
5131
|
}
|
|
4610
5132
|
};
|
|
4611
5133
|
}
|
|
4612
|
-
function recognizeSpecTreeFilesystemEntry(record,
|
|
5134
|
+
function recognizeSpecTreeFilesystemEntry(record, options = {}) {
|
|
5135
|
+
const registry = options.registry ?? KIND_REGISTRY;
|
|
5136
|
+
const schemaVersions = options.schemaVersions ?? SPEC_TREE_NAMING_SCHEMA_VERSIONS;
|
|
4613
5137
|
const name = readLastPathSegment(record.relativePath);
|
|
4614
5138
|
if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE && isProductFile(record.relativePath)) {
|
|
4615
5139
|
return {
|
|
@@ -4620,19 +5144,7 @@ function recognizeSpecTreeFilesystemEntry(record, registry = KIND_REGISTRY) {
|
|
|
4620
5144
|
};
|
|
4621
5145
|
}
|
|
4622
5146
|
if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.DIRECTORY) {
|
|
4623
|
-
|
|
4624
|
-
if (nodeMatch === null) return null;
|
|
4625
|
-
const parsed = parseOrderedSlug(stripSuffix(name, nodeMatch.definition.suffix));
|
|
4626
|
-
if (parsed === null) return null;
|
|
4627
|
-
return {
|
|
4628
|
-
type: SPEC_TREE_ENTRY_TYPE.NODE,
|
|
4629
|
-
kind: nodeMatch.kind,
|
|
4630
|
-
id: record.relativePath,
|
|
4631
|
-
order: parsed.order,
|
|
4632
|
-
slug: parsed.slug,
|
|
4633
|
-
parentId: record.parentId,
|
|
4634
|
-
ref: sourceRefForNode(record.relativePath, parsed.slug)
|
|
4635
|
-
};
|
|
5147
|
+
return recognizeDirectoryRecord(record, name, registry, schemaVersions);
|
|
4636
5148
|
}
|
|
4637
5149
|
if (record.type === SPEC_TREE_FILESYSTEM_RECORD_TYPE.FILE && record.parentId !== void 0 && isEvidenceFile(record.relativePath)) {
|
|
4638
5150
|
return {
|
|
@@ -4660,9 +5172,79 @@ function recognizeSpecTreeFilesystemEntry(record, registry = KIND_REGISTRY) {
|
|
|
4660
5172
|
}
|
|
4661
5173
|
return null;
|
|
4662
5174
|
}
|
|
5175
|
+
function recognizeDirectoryRecord(record, name, registry, schemaVersions) {
|
|
5176
|
+
const canonical = canonicalNamingSchemaVersion(schemaVersions);
|
|
5177
|
+
const canonicalMatch = matchNodeSuffixFromVersion(name, canonical);
|
|
5178
|
+
if (canonicalMatch !== null) {
|
|
5179
|
+
const kind = nodeKindForSuffix(canonicalMatch.suffix, registry);
|
|
5180
|
+
if (kind !== null) {
|
|
5181
|
+
return {
|
|
5182
|
+
type: SPEC_TREE_ENTRY_TYPE.NODE,
|
|
5183
|
+
kind,
|
|
5184
|
+
id: record.relativePath,
|
|
5185
|
+
order: canonicalMatch.parsed.order,
|
|
5186
|
+
slug: canonicalMatch.parsed.slug,
|
|
5187
|
+
parentId: record.parentId,
|
|
5188
|
+
ref: sourceRefForNode(record.relativePath, canonicalMatch.parsed.slug)
|
|
5189
|
+
};
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
const supersededVersion = matchSupersededNodeVersion(name, schemaVersions, canonical);
|
|
5193
|
+
if (supersededVersion !== null) {
|
|
5194
|
+
return {
|
|
5195
|
+
type: SPEC_TREE_ENTRY_TYPE.SUPERSEDED,
|
|
5196
|
+
id: record.relativePath,
|
|
5197
|
+
version: supersededVersion,
|
|
5198
|
+
parentId: record.parentId,
|
|
5199
|
+
ref: sourceRefForRelativePath(record.relativePath)
|
|
5200
|
+
};
|
|
5201
|
+
}
|
|
5202
|
+
if (parseOrderedSlug(name) !== null) {
|
|
5203
|
+
return {
|
|
5204
|
+
type: SPEC_TREE_ENTRY_TYPE.INVALID,
|
|
5205
|
+
id: record.relativePath,
|
|
5206
|
+
parentId: record.parentId,
|
|
5207
|
+
ref: sourceRefForRelativePath(record.relativePath)
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
5210
|
+
return null;
|
|
5211
|
+
}
|
|
5212
|
+
function matchNodeSuffixFromVersion(name, version2) {
|
|
5213
|
+
for (const suffix of version2.nodeSuffixes) {
|
|
5214
|
+
if (!name.endsWith(suffix)) continue;
|
|
5215
|
+
const parsed = parseOrderedSlug(stripSuffix(name, suffix));
|
|
5216
|
+
if (parsed !== null) {
|
|
5217
|
+
return { suffix, parsed };
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
return null;
|
|
5221
|
+
}
|
|
5222
|
+
function nodeKindForSuffix(suffix, registry) {
|
|
5223
|
+
for (const [kind, definition] of Object.entries(registry)) {
|
|
5224
|
+
if (definition.category === SPEC_TREE_KIND_CATEGORY.NODE && definition.suffix === suffix) {
|
|
5225
|
+
return kind;
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
return null;
|
|
5229
|
+
}
|
|
5230
|
+
function matchSupersededNodeVersion(name, schemaVersions, canonical) {
|
|
5231
|
+
const canonicalSuffixes = new Set(canonical.nodeSuffixes);
|
|
5232
|
+
const priorVersions = schemaVersions.filter((version2) => version2 !== canonical).sort((left, right) => compareNamingSchemaVersions(right, left));
|
|
5233
|
+
for (const version2 of priorVersions) {
|
|
5234
|
+
for (const suffix of version2.nodeSuffixes) {
|
|
5235
|
+
if (canonicalSuffixes.has(suffix)) continue;
|
|
5236
|
+
if (name.endsWith(suffix) && parseOrderedSlug(stripSuffix(name, suffix)) !== null) {
|
|
5237
|
+
return version2.version;
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
return null;
|
|
5242
|
+
}
|
|
4663
5243
|
async function readSpecTree(options) {
|
|
4664
5244
|
const entries = await collectSourceEntries(options.source);
|
|
4665
5245
|
const product = entries.find(isProductEntry) ?? null;
|
|
5246
|
+
const superseded = entries.filter(isSupersededEntry);
|
|
5247
|
+
const residual = entries.filter(isInvalidEntry);
|
|
4666
5248
|
const evidenceByParent = groupEvidence(entries.filter(isEvidenceEntry));
|
|
4667
5249
|
const decisions = entries.filter(isDecisionEntry).map(toDecision).sort(compareOrderedEntries);
|
|
4668
5250
|
const decisionsByParent = groupDecisions(decisions);
|
|
@@ -4702,6 +5284,8 @@ async function readSpecTree(options) {
|
|
|
4702
5284
|
nodes: roots,
|
|
4703
5285
|
allNodes,
|
|
4704
5286
|
decisions,
|
|
5287
|
+
superseded,
|
|
5288
|
+
residual,
|
|
4705
5289
|
entries
|
|
4706
5290
|
};
|
|
4707
5291
|
}
|
|
@@ -4743,6 +5327,12 @@ function isDecisionEntry(entry) {
|
|
|
4743
5327
|
function isEvidenceEntry(entry) {
|
|
4744
5328
|
return entry.type === SPEC_TREE_ENTRY_TYPE.EVIDENCE;
|
|
4745
5329
|
}
|
|
5330
|
+
function isSupersededEntry(entry) {
|
|
5331
|
+
return entry.type === SPEC_TREE_ENTRY_TYPE.SUPERSEDED;
|
|
5332
|
+
}
|
|
5333
|
+
function isInvalidEntry(entry) {
|
|
5334
|
+
return entry.type === SPEC_TREE_ENTRY_TYPE.INVALID;
|
|
5335
|
+
}
|
|
4746
5336
|
function toDecision(entry) {
|
|
4747
5337
|
return {
|
|
4748
5338
|
id: entry.id,
|
|
@@ -4817,11 +5407,12 @@ function compareOrderedEntries(left, right) {
|
|
|
4817
5407
|
if (orderComparison !== ORDER_COMPARISON_EQUAL) return orderComparison;
|
|
4818
5408
|
return left.id.localeCompare(right.id);
|
|
4819
5409
|
}
|
|
4820
|
-
async function* readFilesystemSourceEntries(productDir, registry, includePath) {
|
|
5410
|
+
async function* readFilesystemSourceEntries(productDir, registry, schemaVersions, includePath) {
|
|
4821
5411
|
yield* walkFilesystemDirectory({
|
|
4822
|
-
absolutePath:
|
|
5412
|
+
absolutePath: join13(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY),
|
|
4823
5413
|
relativePath: SPEC_TREE_EMPTY_RELATIVE_PATH,
|
|
4824
5414
|
registry,
|
|
5415
|
+
schemaVersions,
|
|
4825
5416
|
includePath
|
|
4826
5417
|
});
|
|
4827
5418
|
}
|
|
@@ -4842,14 +5433,15 @@ async function* walkFilesystemDirectory(context) {
|
|
|
4842
5433
|
if (recordType === void 0) continue;
|
|
4843
5434
|
const sourceEntry = recognizeSpecTreeFilesystemEntry(
|
|
4844
5435
|
{ type: recordType, relativePath, parentId: context.parentId },
|
|
4845
|
-
context.registry
|
|
5436
|
+
{ registry: context.registry, schemaVersions: context.schemaVersions }
|
|
4846
5437
|
);
|
|
4847
5438
|
if (sourceEntry !== null) yield sourceEntry;
|
|
4848
|
-
if (entry.isDirectory() && shouldDescendIntoDirectory(
|
|
5439
|
+
if (entry.isDirectory() && shouldDescendIntoDirectory(sourceEntry)) {
|
|
4849
5440
|
yield* walkFilesystemDirectory({
|
|
4850
|
-
absolutePath:
|
|
5441
|
+
absolutePath: join13(context.absolutePath, entry.name),
|
|
4851
5442
|
relativePath,
|
|
4852
5443
|
registry: context.registry,
|
|
5444
|
+
schemaVersions: context.schemaVersions,
|
|
4853
5445
|
includePath: context.includePath,
|
|
4854
5446
|
parentId: sourceEntry?.type === SPEC_TREE_ENTRY_TYPE.NODE ? sourceEntry.id : context.parentId
|
|
4855
5447
|
});
|
|
@@ -4907,14 +5499,8 @@ function segmentsEndWith(segments, suffix) {
|
|
|
4907
5499
|
const start = segments.length - suffix.length;
|
|
4908
5500
|
return suffix.every((value, index) => segments[start + index] === value);
|
|
4909
5501
|
}
|
|
4910
|
-
function shouldDescendIntoDirectory(
|
|
4911
|
-
|
|
4912
|
-
if (name === SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME) return true;
|
|
4913
|
-
return !isUnregisteredOrderedDirectory(name, registry);
|
|
4914
|
-
}
|
|
4915
|
-
function isUnregisteredOrderedDirectory(name, registry) {
|
|
4916
|
-
if (matchKindSuffix(name, registry, SPEC_TREE_KIND_CATEGORY.NODE) !== null) return false;
|
|
4917
|
-
return parseOrderedSlug(name) !== null;
|
|
5502
|
+
function shouldDescendIntoDirectory(sourceEntry) {
|
|
5503
|
+
return sourceEntry === null || sourceEntry.type === SPEC_TREE_ENTRY_TYPE.NODE;
|
|
4918
5504
|
}
|
|
4919
5505
|
function sourceRefForRelativePath(relativePath) {
|
|
4920
5506
|
const path6 = joinSpecTreePath(SPEC_TREE_CONFIG.ROOT_DIRECTORY, relativePath);
|
|
@@ -4998,14 +5584,14 @@ function formatNextNode(node) {
|
|
|
4998
5584
|
|
|
4999
5585
|
// src/commands/testing/discovery.ts
|
|
5000
5586
|
import { readdir as readdir6 } from "fs/promises";
|
|
5001
|
-
import { join as
|
|
5587
|
+
import { join as join14, relative, sep } from "path";
|
|
5002
5588
|
var TESTS_DIRECTORY_NAME = SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME;
|
|
5003
5589
|
var SPEC_ROOT_DIRECTORY = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
|
|
5004
5590
|
var POSIX_SEPARATOR = "/";
|
|
5005
|
-
var
|
|
5591
|
+
var ERROR_CODE_NOT_FOUND2 = "ENOENT";
|
|
5006
5592
|
async function discoverTestFiles(productDir) {
|
|
5007
5593
|
const found = [];
|
|
5008
|
-
await collectTestFiles(
|
|
5594
|
+
await collectTestFiles(join14(productDir, SPEC_ROOT_DIRECTORY), false, found);
|
|
5009
5595
|
return found.map((absolute) => toPosixRelative(productDir, absolute)).sort(compareAscii);
|
|
5010
5596
|
}
|
|
5011
5597
|
async function collectTestFiles(directory, insideTestsDir, found) {
|
|
@@ -5013,11 +5599,11 @@ async function collectTestFiles(directory, insideTestsDir, found) {
|
|
|
5013
5599
|
try {
|
|
5014
5600
|
entries = await readdir6(directory, { withFileTypes: true });
|
|
5015
5601
|
} catch (error) {
|
|
5016
|
-
if (
|
|
5602
|
+
if (hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) return;
|
|
5017
5603
|
throw error;
|
|
5018
5604
|
}
|
|
5019
5605
|
for (const entry of entries) {
|
|
5020
|
-
const childPath =
|
|
5606
|
+
const childPath = join14(directory, entry.name);
|
|
5021
5607
|
if (entry.isDirectory()) {
|
|
5022
5608
|
await collectTestFiles(childPath, insideTestsDir || entry.name === TESTS_DIRECTORY_NAME, found);
|
|
5023
5609
|
} else if (insideTestsDir && entry.isFile()) {
|
|
@@ -5026,14 +5612,14 @@ async function collectTestFiles(directory, insideTestsDir, found) {
|
|
|
5026
5612
|
}
|
|
5027
5613
|
}
|
|
5028
5614
|
function toPosixRelative(productDir, absolute) {
|
|
5029
|
-
return
|
|
5615
|
+
return relative(productDir, absolute).split(sep).join(POSIX_SEPARATOR);
|
|
5030
5616
|
}
|
|
5031
5617
|
function compareAscii(left, right) {
|
|
5032
5618
|
if (left < right) return -1;
|
|
5033
5619
|
if (left > right) return 1;
|
|
5034
5620
|
return 0;
|
|
5035
5621
|
}
|
|
5036
|
-
function
|
|
5622
|
+
function hasErrorCode2(error, code) {
|
|
5037
5623
|
return typeof error === "object" && error !== null && error.code === code;
|
|
5038
5624
|
}
|
|
5039
5625
|
|
|
@@ -5041,7 +5627,7 @@ function hasErrorCode(error, code) {
|
|
|
5041
5627
|
var SUCCESS_EXIT_CODE = 0;
|
|
5042
5628
|
function aggregateTestExitCode(invocations) {
|
|
5043
5629
|
for (const invocation of invocations) {
|
|
5044
|
-
if (invocation.invoked && invocation.exitCode !==
|
|
5630
|
+
if (invocation.invoked && invocation.exitCode !== SUCCESS_EXIT_CODE) {
|
|
5045
5631
|
return invocation.exitCode;
|
|
5046
5632
|
}
|
|
5047
5633
|
}
|
|
@@ -5091,7 +5677,7 @@ async function runTests(options, deps) {
|
|
|
5091
5677
|
outcomes.push({
|
|
5092
5678
|
runnerId: group.language.name,
|
|
5093
5679
|
testPaths: group.testPaths,
|
|
5094
|
-
exitCode: invocation.exitCode
|
|
5680
|
+
exitCode: invocation.exitCode
|
|
5095
5681
|
});
|
|
5096
5682
|
}
|
|
5097
5683
|
}
|
|
@@ -5099,55 +5685,11 @@ async function runTests(options, deps) {
|
|
|
5099
5685
|
}
|
|
5100
5686
|
|
|
5101
5687
|
// src/commands/testing/run-command.ts
|
|
5102
|
-
import { join as
|
|
5103
|
-
|
|
5104
|
-
// src/testing/run-state.ts
|
|
5105
|
-
import { createHash as createHash3, randomBytes as nodeRandomBytes2 } from "crypto";
|
|
5106
|
-
import {
|
|
5107
|
-
mkdir as nodeMkdir2,
|
|
5108
|
-
readdir as nodeReaddir2,
|
|
5109
|
-
readFile as nodeReadFile2,
|
|
5110
|
-
rename as nodeRename2,
|
|
5111
|
-
writeFile as nodeWriteFile2
|
|
5112
|
-
} from "fs/promises";
|
|
5113
|
-
import { join as join13 } from "path";
|
|
5114
|
-
|
|
5115
|
-
// src/domains/audit/run-state.ts
|
|
5116
|
-
import { createHash as createHash2, randomBytes as nodeRandomBytes } from "crypto";
|
|
5117
|
-
import {
|
|
5118
|
-
mkdir as nodeMkdir,
|
|
5119
|
-
readdir as nodeReaddir,
|
|
5120
|
-
readFile as nodeReadFile,
|
|
5121
|
-
rename as nodeRename,
|
|
5122
|
-
writeFile as nodeWriteFile
|
|
5123
|
-
} from "fs/promises";
|
|
5124
|
-
import { join as join12 } from "path";
|
|
5125
|
-
var AUDIT_RUN_STATE_STATUS = {
|
|
5126
|
-
APPROVED: "approved",
|
|
5127
|
-
REJECTED: "rejected",
|
|
5128
|
-
FAILED: "failed",
|
|
5129
|
-
INTERRUPTED: "interrupted"
|
|
5130
|
-
};
|
|
5131
|
-
var AUDIT_RUN_STATE_DISPLAY = {
|
|
5132
|
-
[AUDIT_RUN_STATE_STATUS.APPROVED]: "APPROVED",
|
|
5133
|
-
[AUDIT_RUN_STATE_STATUS.REJECTED]: AUDIT_VERDICT_VALUE.REJECT,
|
|
5134
|
-
[AUDIT_RUN_STATE_STATUS.FAILED]: "FAILED",
|
|
5135
|
-
[AUDIT_RUN_STATE_STATUS.INTERRUPTED]: "INTERRUPTED"
|
|
5136
|
-
};
|
|
5137
|
-
var AUDIT_RUN_TIMESTAMP_SEPARATOR = "_";
|
|
5138
|
-
var AUDIT_RUN_TIMESTAMP_MILLISECOND_DIGITS = 3;
|
|
5139
|
-
function formatAuditRunTimestamp(date) {
|
|
5140
|
-
const year = date.getUTCFullYear();
|
|
5141
|
-
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
5142
|
-
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
5143
|
-
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
5144
|
-
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
5145
|
-
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
5146
|
-
const milliseconds = String(date.getUTCMilliseconds()).padStart(AUDIT_RUN_TIMESTAMP_MILLISECOND_DIGITS, "0");
|
|
5147
|
-
return `${year}-${month}-${day}${AUDIT_RUN_TIMESTAMP_SEPARATOR}${hours}-${minutes}-${seconds}-${milliseconds}`;
|
|
5148
|
-
}
|
|
5688
|
+
import { join as join16 } from "path";
|
|
5149
5689
|
|
|
5150
5690
|
// src/testing/run-state.ts
|
|
5691
|
+
import { createHash as createHash4 } from "crypto";
|
|
5692
|
+
import { join as join15 } from "path";
|
|
5151
5693
|
var TEST_RUN_STATE_STATUS = {
|
|
5152
5694
|
PASSED: "passed",
|
|
5153
5695
|
FAILED: "failed",
|
|
@@ -5160,11 +5702,14 @@ var TESTING_RUN_STATE_INCOMPLETE_REASON = {
|
|
|
5160
5702
|
SHAPE_INVALID_STATE: "shape-invalid-state"
|
|
5161
5703
|
};
|
|
5162
5704
|
var TESTING_RUN_STATE_ERROR = {
|
|
5163
|
-
|
|
5164
|
-
|
|
5705
|
+
RUN_FILE_COLLISION_LIMIT: "testing run file collision limit exhausted",
|
|
5706
|
+
RUN_FILE_CREATE_FAILED: "testing run file create failed",
|
|
5165
5707
|
STATE_ALREADY_EXISTS: "testing run state already exists",
|
|
5166
5708
|
STATE_WRITE_FAILED: "testing run state write failed"
|
|
5167
5709
|
};
|
|
5710
|
+
var TESTING_RUN_STATE_ERROR_CODE = {
|
|
5711
|
+
NOT_FOUND: "ENOENT"
|
|
5712
|
+
};
|
|
5168
5713
|
var TEST_RUN_STATE_FIELDS = {
|
|
5169
5714
|
BRANCH_NAME: "branchName",
|
|
5170
5715
|
HEAD_SHA: "headSha",
|
|
@@ -5186,119 +5731,64 @@ var PRODUCT_INPUT_DIGEST_FIELDS = {
|
|
|
5186
5731
|
DESCRIPTOR_ID: "descriptorId",
|
|
5187
5732
|
DIGEST: "digest"
|
|
5188
5733
|
};
|
|
5189
|
-
var DEFAULT_TESTING_STORAGE = {
|
|
5190
|
-
spxDir: ".spx",
|
|
5191
|
-
localDir: "local",
|
|
5192
|
-
testingDir: "testing",
|
|
5193
|
-
runsDir: "runs",
|
|
5194
|
-
stateFile: "state.json"
|
|
5195
|
-
};
|
|
5196
5734
|
var SHA256_ALGORITHM2 = "sha256";
|
|
5197
|
-
var
|
|
5198
|
-
var UTF8_ENCODING2 = "utf8";
|
|
5199
|
-
var RUN_ID_BYTES = 6;
|
|
5200
|
-
var TEMP_STATE_ID_BYTES = 6;
|
|
5201
|
-
var RUN_DIRECTORY_CREATE_ATTEMPTS = 10;
|
|
5202
|
-
var TEMP_STATE_FILE_PREFIX = ".state";
|
|
5203
|
-
var TEMP_STATE_FILE_SUFFIX = ".tmp";
|
|
5204
|
-
var JSON_INDENT_SPACES = 2;
|
|
5735
|
+
var HEX_ENCODING3 = "hex";
|
|
5205
5736
|
var SEGMENT_SEPARATOR = "-";
|
|
5206
|
-
var
|
|
5207
|
-
var ERROR_CODE_FILE_EXISTS = "EEXIST";
|
|
5208
|
-
var ERROR_CODE_NOT_FOUND2 = "ENOENT";
|
|
5209
|
-
var RUN_DIRECTORY_NAME_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[a-f0-9]{12}$/;
|
|
5210
|
-
var defaultFileSystem = {
|
|
5211
|
-
mkdir: async (path6, options) => {
|
|
5212
|
-
await nodeMkdir2(path6, options);
|
|
5213
|
-
},
|
|
5214
|
-
writeFile: nodeWriteFile2,
|
|
5215
|
-
rename: nodeRename2,
|
|
5216
|
-
readFile: nodeReadFile2,
|
|
5217
|
-
readdir: nodeReaddir2
|
|
5218
|
-
};
|
|
5737
|
+
var defaultFileSystem2 = defaultFileSystem;
|
|
5219
5738
|
function formatTestRunTimestamp(date) {
|
|
5220
|
-
return
|
|
5739
|
+
return formatRunTimestamp(date);
|
|
5221
5740
|
}
|
|
5222
|
-
function testingRunsDir(productDir
|
|
5223
|
-
|
|
5741
|
+
function testingRunsDir(productDir) {
|
|
5742
|
+
const worktreeScope = worktreeScopeDir(productDir);
|
|
5743
|
+
if (!worktreeScope.ok) throw new Error(worktreeScope.error);
|
|
5744
|
+
const result = runsDir(worktreeScope.value, STATE_STORE_DOMAIN.TEST);
|
|
5745
|
+
if (!result.ok) throw new Error(result.error);
|
|
5746
|
+
return result.value;
|
|
5224
5747
|
}
|
|
5225
|
-
async function
|
|
5226
|
-
const
|
|
5227
|
-
|
|
5228
|
-
const
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
await fs7.mkdir(runsDir, { recursive: true });
|
|
5235
|
-
} catch (error) {
|
|
5236
|
-
return { ok: false, error: `${TESTING_RUN_STATE_ERROR.RUN_DIRECTORY_CREATE_FAILED}: ${toErrorMessage(error)}` };
|
|
5237
|
-
}
|
|
5238
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
5239
|
-
const runId = generateHexId(RUN_ID_BYTES, randomBytes);
|
|
5240
|
-
const runDirectoryName = `${startedAt}${SEGMENT_SEPARATOR}${runId}`;
|
|
5241
|
-
const runDir = join13(runsDir, runDirectoryName);
|
|
5242
|
-
try {
|
|
5243
|
-
await fs7.mkdir(runDir);
|
|
5244
|
-
return { ok: true, value: { runsDir, runDir, runDirectoryName, runId, startedAt } };
|
|
5245
|
-
} catch (error) {
|
|
5246
|
-
if (hasErrorCode2(error, ERROR_CODE_FILE_EXISTS)) continue;
|
|
5247
|
-
return { ok: false, error: `${TESTING_RUN_STATE_ERROR.RUN_DIRECTORY_CREATE_FAILED}: ${toErrorMessage(error)}` };
|
|
5248
|
-
}
|
|
5249
|
-
}
|
|
5250
|
-
return { ok: false, error: TESTING_RUN_STATE_ERROR.RUN_DIRECTORY_COLLISION_LIMIT };
|
|
5748
|
+
async function createTestRunFile(productDir, options = {}) {
|
|
5749
|
+
const worktreeScope = worktreeScopeDir(productDir);
|
|
5750
|
+
if (!worktreeScope.ok) return worktreeScope;
|
|
5751
|
+
const created = await createJsonlRunFile(worktreeScope.value, STATE_STORE_DOMAIN.TEST, options);
|
|
5752
|
+
if (!created.ok) return {
|
|
5753
|
+
ok: false,
|
|
5754
|
+
error: testingRunFileError(created.error)
|
|
5755
|
+
};
|
|
5756
|
+
return { ok: true, value: created.value };
|
|
5251
5757
|
}
|
|
5252
|
-
async function writeTerminalTestRunState(
|
|
5253
|
-
const
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
try {
|
|
5257
|
-
await fs7.readFile(statePath, UTF8_ENCODING2);
|
|
5758
|
+
async function writeTerminalTestRunState(runFilePath, state, options = {}) {
|
|
5759
|
+
const written = await writeJsonlRunRecord(runFilePath, testRunStateRecord(state), options);
|
|
5760
|
+
if (written.ok) return written;
|
|
5761
|
+
if (written.error === STATE_STORE_ERROR.RECORD_ALREADY_EXISTS) {
|
|
5258
5762
|
return { ok: false, error: TESTING_RUN_STATE_ERROR.STATE_ALREADY_EXISTS };
|
|
5259
|
-
} catch (error) {
|
|
5260
|
-
if (!hasErrorCode2(error, ERROR_CODE_NOT_FOUND2)) {
|
|
5261
|
-
return { ok: false, error: `${TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED}: ${toErrorMessage(error)}` };
|
|
5262
|
-
}
|
|
5263
|
-
}
|
|
5264
|
-
const tempId = generateHexId(TEMP_STATE_ID_BYTES, options.randomBytes ?? nodeRandomBytes2);
|
|
5265
|
-
const tempPath = join13(runDir, `${TEMP_STATE_FILE_PREFIX}-${tempId}${TEMP_STATE_FILE_SUFFIX}`);
|
|
5266
|
-
const serialized = `${JSON.stringify(state, null, JSON_INDENT_SPACES)}
|
|
5267
|
-
`;
|
|
5268
|
-
try {
|
|
5269
|
-
await fs7.writeFile(tempPath, serialized, { flag: EXCLUSIVE_CREATE_FLAG });
|
|
5270
|
-
await fs7.rename(tempPath, statePath);
|
|
5271
|
-
return { ok: true, value: statePath };
|
|
5272
|
-
} catch (error) {
|
|
5273
|
-
return { ok: false, error: `${TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED}: ${toErrorMessage(error)}` };
|
|
5274
5763
|
}
|
|
5764
|
+
return {
|
|
5765
|
+
ok: false,
|
|
5766
|
+
error: testingWriteError(written.error)
|
|
5767
|
+
};
|
|
5275
5768
|
}
|
|
5276
5769
|
async function readTestingRuns(productDir, options = {}) {
|
|
5277
|
-
const fs7 = options.fs ??
|
|
5278
|
-
const
|
|
5279
|
-
const runsDir = testingRunsDir(productDir, storage);
|
|
5770
|
+
const fs7 = options.fs ?? defaultFileSystem2;
|
|
5771
|
+
const runsDir2 = testingRunsDir(productDir);
|
|
5280
5772
|
let entries;
|
|
5281
5773
|
try {
|
|
5282
|
-
entries = await fs7.readdir(
|
|
5774
|
+
entries = await fs7.readdir(runsDir2, { withFileTypes: true });
|
|
5283
5775
|
} catch (error) {
|
|
5284
|
-
if (
|
|
5776
|
+
if (hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) {
|
|
5285
5777
|
return { ok: true, value: { terminalRuns: [], incompleteRuns: [] } };
|
|
5286
5778
|
}
|
|
5287
|
-
return { ok: false, error:
|
|
5779
|
+
return { ok: false, error: toErrorMessage2(error) };
|
|
5288
5780
|
}
|
|
5289
5781
|
const terminalRuns = [];
|
|
5290
5782
|
const incompleteRuns = [];
|
|
5291
|
-
for (const entry of entries.filter(
|
|
5292
|
-
const
|
|
5293
|
-
const
|
|
5294
|
-
const stateResult = await readTestRunStatePath(statePath, fs7);
|
|
5783
|
+
for (const entry of entries.filter(isTestRunFileEntry)) {
|
|
5784
|
+
const runFilePath = join15(runsDir2, entry.name);
|
|
5785
|
+
const stateResult = await readTestRunStatePath(runFilePath, fs7);
|
|
5295
5786
|
if (stateResult.ok) {
|
|
5296
|
-
terminalRuns.push({
|
|
5787
|
+
terminalRuns.push({ runFileName: entry.name, runFilePath, state: stateResult.value });
|
|
5297
5788
|
} else {
|
|
5298
5789
|
incompleteRuns.push({
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
statePath,
|
|
5790
|
+
runFileName: entry.name,
|
|
5791
|
+
runFilePath,
|
|
5302
5792
|
reason: stateResult.reason,
|
|
5303
5793
|
...stateResult.error === void 0 ? {} : { error: stateResult.error }
|
|
5304
5794
|
});
|
|
@@ -5331,29 +5821,55 @@ function digestTestContents(entries) {
|
|
|
5331
5821
|
function isStalenessMatch(recorded, current) {
|
|
5332
5822
|
return recorded.testingConfigDigest === current.testingConfigDigest && recorded.discoveredTestPathsDigest === current.discoveredTestPathsDigest && recorded.discoveredTestContentDigest === current.discoveredTestContentDigest && productInputDigestsEqual(recorded.productInputDigests, current.productInputDigests);
|
|
5333
5823
|
}
|
|
5334
|
-
function extractStalenessInputs(state) {
|
|
5824
|
+
function extractStalenessInputs(state) {
|
|
5825
|
+
return {
|
|
5826
|
+
testingConfigDigest: state.testingConfigDigest,
|
|
5827
|
+
discoveredTestPathsDigest: state.discoveredTestPathsDigest,
|
|
5828
|
+
discoveredTestContentDigest: state.discoveredTestContentDigest,
|
|
5829
|
+
productInputDigests: state.productInputDigests
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
function testRunStateRecord(state) {
|
|
5335
5833
|
return {
|
|
5834
|
+
branchName: state.branchName,
|
|
5835
|
+
headSha: state.headSha,
|
|
5336
5836
|
testingConfigDigest: state.testingConfigDigest,
|
|
5837
|
+
runnerOutcomes: state.runnerOutcomes.map((outcome) => ({
|
|
5838
|
+
runnerId: outcome.runnerId,
|
|
5839
|
+
testPaths: outcome.testPaths,
|
|
5840
|
+
exitCode: outcome.exitCode
|
|
5841
|
+
})),
|
|
5337
5842
|
discoveredTestPathsDigest: state.discoveredTestPathsDigest,
|
|
5338
5843
|
discoveredTestContentDigest: state.discoveredTestContentDigest,
|
|
5339
|
-
productInputDigests: state.productInputDigests
|
|
5844
|
+
productInputDigests: state.productInputDigests.map((digest) => ({
|
|
5845
|
+
descriptorId: digest.descriptorId,
|
|
5846
|
+
digest: digest.digest
|
|
5847
|
+
})),
|
|
5848
|
+
startedAt: state.startedAt,
|
|
5849
|
+
completedAt: state.completedAt,
|
|
5850
|
+
status: state.status
|
|
5340
5851
|
};
|
|
5341
5852
|
}
|
|
5342
|
-
async function readTestRunStatePath(
|
|
5343
|
-
let
|
|
5853
|
+
async function readTestRunStatePath(runFilePath, fs7) {
|
|
5854
|
+
let content;
|
|
5344
5855
|
try {
|
|
5345
|
-
|
|
5856
|
+
content = await fs7.readFile(runFilePath, "utf8");
|
|
5346
5857
|
} catch (error) {
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5858
|
+
return {
|
|
5859
|
+
ok: false,
|
|
5860
|
+
reason: hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND) ? TESTING_RUN_STATE_INCOMPLETE_REASON.MISSING_STATE : TESTING_RUN_STATE_INCOMPLETE_REASON.IO_ERROR,
|
|
5861
|
+
error: toErrorMessage2(error)
|
|
5862
|
+
};
|
|
5863
|
+
}
|
|
5864
|
+
const latest = latestNonEmptyJsonlLine(content);
|
|
5865
|
+
if (latest === void 0) {
|
|
5866
|
+
return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE };
|
|
5351
5867
|
}
|
|
5352
5868
|
let parsed;
|
|
5353
5869
|
try {
|
|
5354
|
-
parsed = JSON.parse(
|
|
5355
|
-
} catch
|
|
5356
|
-
return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE
|
|
5870
|
+
parsed = JSON.parse(latest);
|
|
5871
|
+
} catch {
|
|
5872
|
+
return { ok: false, reason: TESTING_RUN_STATE_INCOMPLETE_REASON.PARSE_INVALID_STATE };
|
|
5357
5873
|
}
|
|
5358
5874
|
const validated = validateTestRunState(parsed);
|
|
5359
5875
|
if (!validated.ok) {
|
|
@@ -5375,8 +5891,8 @@ function validateTestRunState(value) {
|
|
|
5375
5891
|
if (!discoveredTestPathsDigest.ok) return discoveredTestPathsDigest;
|
|
5376
5892
|
const discoveredTestContentDigest = readString(value, TEST_RUN_STATE_FIELDS.DISCOVERED_TEST_CONTENT_DIGEST);
|
|
5377
5893
|
if (!discoveredTestContentDigest.ok) return discoveredTestContentDigest;
|
|
5378
|
-
const
|
|
5379
|
-
if (!
|
|
5894
|
+
const productInputDigests2 = readProductInputDigests(value[TEST_RUN_STATE_FIELDS.PRODUCT_INPUT_DIGESTS]);
|
|
5895
|
+
if (!productInputDigests2.ok) return productInputDigests2;
|
|
5380
5896
|
const startedAt = readString(value, TEST_RUN_STATE_FIELDS.STARTED_AT);
|
|
5381
5897
|
if (!startedAt.ok) return startedAt;
|
|
5382
5898
|
const completedAt = readString(value, TEST_RUN_STATE_FIELDS.COMPLETED_AT);
|
|
@@ -5392,7 +5908,7 @@ function validateTestRunState(value) {
|
|
|
5392
5908
|
runnerOutcomes: runnerOutcomes.value,
|
|
5393
5909
|
discoveredTestPathsDigest: discoveredTestPathsDigest.value,
|
|
5394
5910
|
discoveredTestContentDigest: discoveredTestContentDigest.value,
|
|
5395
|
-
productInputDigests:
|
|
5911
|
+
productInputDigests: productInputDigests2.value,
|
|
5396
5912
|
startedAt: startedAt.value,
|
|
5397
5913
|
completedAt: completedAt.value,
|
|
5398
5914
|
status: status.value
|
|
@@ -5456,7 +5972,7 @@ function compareTerminalRuns(left, right) {
|
|
|
5456
5972
|
if (completed !== 0) return completed;
|
|
5457
5973
|
const started = compareAsciiStrings(left.state.startedAt, right.state.startedAt);
|
|
5458
5974
|
if (started !== 0) return started;
|
|
5459
|
-
return compareAsciiStrings(left.
|
|
5975
|
+
return compareAsciiStrings(left.runFileName, right.runFileName);
|
|
5460
5976
|
}
|
|
5461
5977
|
function productInputDigestsEqual(left, right) {
|
|
5462
5978
|
return canonicalProductInputDigests(left) === canonicalProductInputDigests(right);
|
|
@@ -5465,37 +5981,50 @@ function canonicalProductInputDigests(digests) {
|
|
|
5465
5981
|
const normalized = [...digests].map((digest) => [digest.descriptorId, digest.digest]).sort((left, right) => compareAsciiStrings(left.join(SEGMENT_SEPARATOR), right.join(SEGMENT_SEPARATOR)));
|
|
5466
5982
|
return JSON.stringify(normalized);
|
|
5467
5983
|
}
|
|
5468
|
-
function
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5984
|
+
function isTestRunFileEntry(entry) {
|
|
5985
|
+
return entry.isFile() && isRunFileName(entry.name);
|
|
5986
|
+
}
|
|
5987
|
+
function testingRunFileError(error) {
|
|
5988
|
+
const stateStoreError = parseStateStoreError(error);
|
|
5989
|
+
if (stateStoreError?.code === STATE_STORE_ERROR.RUN_FILE_COLLISION_LIMIT) {
|
|
5990
|
+
return TESTING_RUN_STATE_ERROR.RUN_FILE_COLLISION_LIMIT;
|
|
5991
|
+
}
|
|
5992
|
+
if (stateStoreError?.code === STATE_STORE_ERROR.RUN_FILE_CREATE_FAILED) {
|
|
5993
|
+
return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.RUN_FILE_CREATE_FAILED, stateStoreError.detail);
|
|
5994
|
+
}
|
|
5995
|
+
return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.RUN_FILE_CREATE_FAILED, error);
|
|
5996
|
+
}
|
|
5997
|
+
function testingWriteError(error) {
|
|
5998
|
+
const stateStoreError = parseStateStoreError(error);
|
|
5999
|
+
if (stateStoreError?.code === STATE_STORE_ERROR.RECORD_WRITE_FAILED) {
|
|
6000
|
+
return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED, stateStoreError.detail);
|
|
6001
|
+
}
|
|
6002
|
+
return withDomainErrorDetail(TESTING_RUN_STATE_ERROR.STATE_WRITE_FAILED, error);
|
|
5472
6003
|
}
|
|
5473
|
-
function
|
|
5474
|
-
return
|
|
6004
|
+
function withDomainErrorDetail(domainError, detail) {
|
|
6005
|
+
return detail === void 0 ? domainError : `${domainError}: ${detail}`;
|
|
5475
6006
|
}
|
|
5476
6007
|
function isRecord4(value) {
|
|
5477
6008
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5478
6009
|
}
|
|
5479
6010
|
function sha256Hex(value) {
|
|
5480
|
-
return
|
|
5481
|
-
}
|
|
5482
|
-
function generateHexId(size, randomBytes) {
|
|
5483
|
-
return randomBytes(size).toString(HEX_ENCODING2);
|
|
6011
|
+
return createHash4(SHA256_ALGORITHM2).update(value).digest(HEX_ENCODING3);
|
|
5484
6012
|
}
|
|
5485
|
-
function
|
|
5486
|
-
return isRecord4(error) && error.code === code;
|
|
5487
|
-
}
|
|
5488
|
-
function toErrorMessage(error) {
|
|
6013
|
+
function toErrorMessage2(error) {
|
|
5489
6014
|
return error instanceof Error ? error.message : String(error);
|
|
5490
6015
|
}
|
|
5491
6016
|
|
|
5492
6017
|
// src/commands/testing/run-command.ts
|
|
5493
6018
|
var NO_GIT_IDENTITY = "(unknown)";
|
|
5494
6019
|
var TEXT_ENCODING = "utf8";
|
|
5495
|
-
var
|
|
6020
|
+
var PRODUCT_INPUT_FIELDS = {
|
|
6021
|
+
PATH: "path",
|
|
6022
|
+
PRESENT: "present",
|
|
6023
|
+
CONTENT: "content"
|
|
6024
|
+
};
|
|
5496
6025
|
function resolveRecordingDependencies(deps) {
|
|
5497
6026
|
return {
|
|
5498
|
-
fs: deps.fs ??
|
|
6027
|
+
fs: deps.fs ?? defaultFileSystem2,
|
|
5499
6028
|
now: deps.now ?? (() => /* @__PURE__ */ new Date()),
|
|
5500
6029
|
git: deps.git
|
|
5501
6030
|
};
|
|
@@ -5518,28 +6047,74 @@ function coveredTestPaths(dispatch) {
|
|
|
5518
6047
|
async function readCoveredContents(productDir, paths, fs7) {
|
|
5519
6048
|
const entries = [];
|
|
5520
6049
|
for (const path6 of paths) {
|
|
5521
|
-
entries.push({ path: path6, content: await fs7.readFile(
|
|
6050
|
+
entries.push({ path: path6, content: await fs7.readFile(join16(productDir, path6), TEXT_ENCODING) });
|
|
6051
|
+
}
|
|
6052
|
+
return entries;
|
|
6053
|
+
}
|
|
6054
|
+
async function readProductInputEntries(productDir, paths, fs7) {
|
|
6055
|
+
const entries = [];
|
|
6056
|
+
for (const path6 of [...paths].sort(compareAsciiStrings)) {
|
|
6057
|
+
try {
|
|
6058
|
+
entries.push({
|
|
6059
|
+
[PRODUCT_INPUT_FIELDS.PATH]: path6,
|
|
6060
|
+
[PRODUCT_INPUT_FIELDS.PRESENT]: true,
|
|
6061
|
+
[PRODUCT_INPUT_FIELDS.CONTENT]: await fs7.readFile(join16(productDir, path6), TEXT_ENCODING)
|
|
6062
|
+
});
|
|
6063
|
+
} catch (error) {
|
|
6064
|
+
if (!hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) throw error;
|
|
6065
|
+
entries.push({
|
|
6066
|
+
[PRODUCT_INPUT_FIELDS.PATH]: path6,
|
|
6067
|
+
[PRODUCT_INPUT_FIELDS.PRESENT]: false
|
|
6068
|
+
});
|
|
6069
|
+
}
|
|
5522
6070
|
}
|
|
5523
6071
|
return entries;
|
|
5524
6072
|
}
|
|
6073
|
+
async function digestLanguageProductInputs(productDir, language, coveredPaths, fs7) {
|
|
6074
|
+
const entries = await readProductInputEntries(productDir, languageProductInputPaths(language, coveredPaths), fs7);
|
|
6075
|
+
const digest = digestDescriptorSection(entries, `${language.name} product inputs`);
|
|
6076
|
+
if (!digest.ok) {
|
|
6077
|
+
throw new Error(`failed to digest ${language.name} product inputs: ${digest.error}`);
|
|
6078
|
+
}
|
|
6079
|
+
return digest.value.sha256;
|
|
6080
|
+
}
|
|
6081
|
+
async function productInputDigests(productDir, registry, coveredPaths, fs7) {
|
|
6082
|
+
const digests = [];
|
|
6083
|
+
for (const language of registry.languages) {
|
|
6084
|
+
digests.push({
|
|
6085
|
+
descriptorId: language.name,
|
|
6086
|
+
digest: await digestLanguageProductInputs(productDir, language, coveredPaths, fs7)
|
|
6087
|
+
});
|
|
6088
|
+
}
|
|
6089
|
+
return digests;
|
|
6090
|
+
}
|
|
5525
6091
|
function deriveStatus(outcomes) {
|
|
5526
6092
|
const allPassed = outcomes.every((outcome) => outcome.exitCode === SUCCESS_EXIT_CODE);
|
|
5527
6093
|
return allPassed ? TEST_RUN_STATE_STATUS.PASSED : TEST_RUN_STATE_STATUS.FAILED;
|
|
5528
6094
|
}
|
|
5529
|
-
async function currentStalenessInputs(productDir, coveredPaths, deps
|
|
5530
|
-
const fs7 = deps.fs ??
|
|
6095
|
+
async function currentStalenessInputs(productDir, coveredPaths, deps) {
|
|
6096
|
+
const fs7 = deps.fs ?? defaultFileSystem2;
|
|
5531
6097
|
const testingConfigDigest = deps.testingConfigDigest ?? (await resolveTestingConfig(productDir)).digest;
|
|
5532
6098
|
const contents = await readCoveredContents(productDir, coveredPaths, fs7);
|
|
5533
6099
|
return {
|
|
5534
6100
|
testingConfigDigest,
|
|
5535
6101
|
discoveredTestPathsDigest: digestTestPaths(coveredPaths),
|
|
5536
6102
|
discoveredTestContentDigest: digestTestContents(contents),
|
|
5537
|
-
productInputDigests:
|
|
6103
|
+
productInputDigests: await productInputDigests(productDir, deps.registry, coveredPaths, fs7)
|
|
5538
6104
|
};
|
|
5539
6105
|
}
|
|
5540
|
-
|
|
6106
|
+
function languageProductInputPaths(language, coveredPaths) {
|
|
6107
|
+
return [
|
|
6108
|
+
.../* @__PURE__ */ new Set([
|
|
6109
|
+
...language.productInputPaths,
|
|
6110
|
+
...language.coveredProductInputPaths?.(coveredPaths) ?? []
|
|
6111
|
+
])
|
|
6112
|
+
].sort(compareAsciiStrings);
|
|
6113
|
+
}
|
|
6114
|
+
async function recordRun(runFile, productDir, dispatch, recording, registry, testingConfigDigest) {
|
|
5541
6115
|
const staleness = await currentStalenessInputs(productDir, coveredTestPaths(dispatch), {
|
|
5542
6116
|
fs: recording.fs,
|
|
6117
|
+
registry,
|
|
5543
6118
|
testingConfigDigest
|
|
5544
6119
|
});
|
|
5545
6120
|
const branchName = await getCurrentBranch(productDir, recording.git) ?? NO_GIT_IDENTITY;
|
|
@@ -5552,43 +6127,43 @@ async function recordRun(directory, productDir, dispatch, recording, testingConf
|
|
|
5552
6127
|
discoveredTestPathsDigest: staleness.discoveredTestPathsDigest,
|
|
5553
6128
|
discoveredTestContentDigest: staleness.discoveredTestContentDigest,
|
|
5554
6129
|
productInputDigests: staleness.productInputDigests,
|
|
5555
|
-
startedAt:
|
|
6130
|
+
startedAt: runFile.startedAt,
|
|
5556
6131
|
completedAt: formatTestRunTimestamp(recording.now()),
|
|
5557
6132
|
status: deriveStatus(dispatch.outcomes)
|
|
5558
6133
|
};
|
|
5559
|
-
const written = await writeTerminalTestRunState(
|
|
6134
|
+
const written = await writeTerminalTestRunState(runFile.runFilePath, state, { fs: recording.fs });
|
|
5560
6135
|
if (!written.ok) {
|
|
5561
6136
|
throw new Error(`failed to record test run: ${written.error}`);
|
|
5562
6137
|
}
|
|
5563
6138
|
return state;
|
|
5564
6139
|
}
|
|
5565
|
-
async function
|
|
5566
|
-
const
|
|
5567
|
-
if (!
|
|
5568
|
-
throw new Error(`failed to create test run
|
|
6140
|
+
async function reserveRunFile(productDir, recording) {
|
|
6141
|
+
const runFile = await createTestRunFile(productDir, { fs: recording.fs, now: recording.now });
|
|
6142
|
+
if (!runFile.ok) {
|
|
6143
|
+
throw new Error(`failed to create test run file: ${runFile.error}`);
|
|
5569
6144
|
}
|
|
5570
|
-
return
|
|
6145
|
+
return runFile.value;
|
|
5571
6146
|
}
|
|
5572
6147
|
async function runTestsCommand(options, deps) {
|
|
5573
6148
|
const { config, digest } = await resolveTestingConfig(options.productDir);
|
|
5574
6149
|
const passingScope = options.passing ? config.passingScope : void 0;
|
|
5575
6150
|
const recording = resolveRecordingDependencies(deps);
|
|
5576
|
-
const
|
|
6151
|
+
const runFile = await reserveRunFile(options.productDir, recording);
|
|
5577
6152
|
const dispatch = await runTests(
|
|
5578
6153
|
{ productDir: options.productDir, registry: deps.registry, passingScope },
|
|
5579
6154
|
{ runnerDepsFor: deps.runnerDepsFor }
|
|
5580
6155
|
);
|
|
5581
|
-
const recorded = await recordRun(
|
|
6156
|
+
const recorded = await recordRun(runFile, options.productDir, dispatch, recording, deps.registry, digest);
|
|
5582
6157
|
return { dispatch, recorded };
|
|
5583
6158
|
}
|
|
5584
6159
|
async function runNodeCommand(options, deps) {
|
|
5585
6160
|
const recording = resolveRecordingDependencies(deps);
|
|
5586
|
-
const
|
|
6161
|
+
const runFile = await reserveRunFile(options.productDir, recording);
|
|
5587
6162
|
const dispatch = await runTests(
|
|
5588
6163
|
{ productDir: options.productDir, registry: deps.registry, passingScope: { include: [options.nodePath] } },
|
|
5589
6164
|
{ runnerDepsFor: deps.runnerDepsFor }
|
|
5590
6165
|
);
|
|
5591
|
-
const recorded = await recordRun(
|
|
6166
|
+
const recorded = await recordRun(runFile, options.productDir, dispatch, recording, deps.registry);
|
|
5592
6167
|
return { dispatch, recorded };
|
|
5593
6168
|
}
|
|
5594
6169
|
|
|
@@ -5596,19 +6171,37 @@ async function runNodeCommand(options, deps) {
|
|
|
5596
6171
|
var PATH_SEPARATOR = "/";
|
|
5597
6172
|
function createNodeOutcomeResolver(deps) {
|
|
5598
6173
|
let evidence;
|
|
6174
|
+
const currentInputsByCoveredPaths = /* @__PURE__ */ new Map();
|
|
5599
6175
|
const sharedEvidence = () => evidence ??= loadResolverEvidence(deps);
|
|
6176
|
+
const refreshEvidence = () => {
|
|
6177
|
+
evidence = loadResolverEvidence(deps);
|
|
6178
|
+
};
|
|
6179
|
+
const currentInputsFor = (coveredPaths) => {
|
|
6180
|
+
const cacheKey = coveredPathCollectionKey(coveredPaths);
|
|
6181
|
+
const cached = currentInputsByCoveredPaths.get(cacheKey);
|
|
6182
|
+
if (cached !== void 0) {
|
|
6183
|
+
return cached;
|
|
6184
|
+
}
|
|
6185
|
+
const current = currentStalenessInputs(deps.productDir, coveredPaths, deps);
|
|
6186
|
+
currentInputsByCoveredPaths.set(cacheKey, current);
|
|
6187
|
+
return current;
|
|
6188
|
+
};
|
|
5600
6189
|
return async (nodeId) => {
|
|
5601
6190
|
const { discoveredTestPaths, terminalRuns } = await sharedEvidence();
|
|
5602
6191
|
const nodePath = `${SPEC_TREE_CONFIG.ROOT_DIRECTORY}${PATH_SEPARATOR}${nodeId}`;
|
|
5603
6192
|
const nodeTestPaths = filterNodeTestPaths(discoveredTestPaths, nodePath);
|
|
5604
|
-
const usable = await usableRecordedOutcome(
|
|
6193
|
+
const usable = await usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor);
|
|
5605
6194
|
if (usable !== void 0) {
|
|
5606
6195
|
return usable;
|
|
5607
6196
|
}
|
|
5608
6197
|
const { dispatch, recorded } = await runNodeCommand({ productDir: deps.productDir, nodePath }, deps);
|
|
6198
|
+
refreshEvidence();
|
|
5609
6199
|
return outcomesCoverPaths(dispatch.outcomes, nodeTestPaths) && recorded.status === TEST_RUN_STATE_STATUS.PASSED;
|
|
5610
6200
|
};
|
|
5611
6201
|
}
|
|
6202
|
+
function coveredPathCollectionKey(coveredPaths) {
|
|
6203
|
+
return JSON.stringify([...coveredPaths].sort());
|
|
6204
|
+
}
|
|
5612
6205
|
async function loadResolverEvidence(deps) {
|
|
5613
6206
|
const discoveredTestPaths = await discoverTestFiles(deps.productDir);
|
|
5614
6207
|
const runs = await readTestingRuns(deps.productDir, deps);
|
|
@@ -5618,7 +6211,7 @@ function filterNodeTestPaths(discoveredTestPaths, nodePath) {
|
|
|
5618
6211
|
const prefix = `${nodePath}${PATH_SEPARATOR}`;
|
|
5619
6212
|
return discoveredTestPaths.filter((path6) => path6.startsWith(prefix));
|
|
5620
6213
|
}
|
|
5621
|
-
async function usableRecordedOutcome(
|
|
6214
|
+
async function usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor) {
|
|
5622
6215
|
const latest = selectLatestTerminalTestRunForNode(terminalRuns, nodeTestPaths);
|
|
5623
6216
|
if (latest === void 0) {
|
|
5624
6217
|
return void 0;
|
|
@@ -5628,7 +6221,7 @@ async function usableRecordedOutcome(deps, discoveredTestPaths, terminalRuns, no
|
|
|
5628
6221
|
if (!runCoveredPaths.every((path6) => presentTestPaths.has(path6))) {
|
|
5629
6222
|
return void 0;
|
|
5630
6223
|
}
|
|
5631
|
-
const current = await
|
|
6224
|
+
const current = await currentInputsFor(runCoveredPaths);
|
|
5632
6225
|
const fresh = isStalenessMatch(extractStalenessInputs(latest.state), current);
|
|
5633
6226
|
return fresh && latest.state.status === TEST_RUN_STATE_STATUS.PASSED ? true : void 0;
|
|
5634
6227
|
}
|
|
@@ -5648,11 +6241,11 @@ function serializeNodeStatus(state) {
|
|
|
5648
6241
|
}
|
|
5649
6242
|
|
|
5650
6243
|
// src/lib/node-status/provider.ts
|
|
5651
|
-
import { join as
|
|
6244
|
+
import { join as join18 } from "path";
|
|
5652
6245
|
|
|
5653
6246
|
// src/lib/node-status/read.ts
|
|
5654
6247
|
import { readFileSync as readFileSync2 } from "fs";
|
|
5655
|
-
import { join as
|
|
6248
|
+
import { join as join17 } from "path";
|
|
5656
6249
|
var NODE_STATUS_FILENAME = "spx.status.json";
|
|
5657
6250
|
var NODE_STATUS_VALUES = new Set(Object.values(SPEC_TREE_NODE_STATE));
|
|
5658
6251
|
function isNodeError2(error) {
|
|
@@ -5662,7 +6255,7 @@ function isSpecTreeNodeState(value) {
|
|
|
5662
6255
|
return typeof value === "string" && NODE_STATUS_VALUES.has(value);
|
|
5663
6256
|
}
|
|
5664
6257
|
function readNodeStatus(nodeDir) {
|
|
5665
|
-
const filePath =
|
|
6258
|
+
const filePath = join17(nodeDir, NODE_STATUS_FILENAME);
|
|
5666
6259
|
let content;
|
|
5667
6260
|
try {
|
|
5668
6261
|
content = readFileSync2(filePath, "utf8");
|
|
@@ -5684,7 +6277,7 @@ function readNodeStatus(nodeDir) {
|
|
|
5684
6277
|
|
|
5685
6278
|
// src/lib/node-status/provider.ts
|
|
5686
6279
|
function nodeDirectory(productDir, node) {
|
|
5687
|
-
return
|
|
6280
|
+
return join18(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
|
|
5688
6281
|
}
|
|
5689
6282
|
function createNodeStatusProvider(productDir) {
|
|
5690
6283
|
return {
|
|
@@ -5696,7 +6289,7 @@ function createNodeStatusProvider(productDir) {
|
|
|
5696
6289
|
|
|
5697
6290
|
// src/lib/node-status/update.ts
|
|
5698
6291
|
import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
|
|
5699
|
-
import { dirname as
|
|
6292
|
+
import { dirname as dirname4, join as join19 } from "path";
|
|
5700
6293
|
var NODE_STATUS_TEXT_ENCODING = "utf8";
|
|
5701
6294
|
async function updateNodeStatus(options) {
|
|
5702
6295
|
const { productDir, resolveOutcome } = options;
|
|
@@ -5734,8 +6327,8 @@ function isNodeExcluded(ignoreReader, node) {
|
|
|
5734
6327
|
return ignoreReader.isUnderIgnoreSource(reference);
|
|
5735
6328
|
}
|
|
5736
6329
|
async function writeNodeStatus(productDir, nodeId, state) {
|
|
5737
|
-
const filePath =
|
|
5738
|
-
await mkdir4(
|
|
6330
|
+
const filePath = join19(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
|
|
6331
|
+
await mkdir4(dirname4(filePath), { recursive: true });
|
|
5739
6332
|
await writeFile2(filePath, serializeNodeStatus(state), NODE_STATUS_TEXT_ENCODING);
|
|
5740
6333
|
}
|
|
5741
6334
|
|
|
@@ -5849,26 +6442,95 @@ function formatNodeLabel(node) {
|
|
|
5849
6442
|
}
|
|
5850
6443
|
|
|
5851
6444
|
// src/testing/languages/python.ts
|
|
5852
|
-
import { basename } from "path";
|
|
6445
|
+
import { basename as basename2, dirname as dirname5, join as join20 } from "path/posix";
|
|
6446
|
+
|
|
6447
|
+
// src/validation/discovery/language-finder.ts
|
|
6448
|
+
import fs5 from "fs";
|
|
6449
|
+
import path4 from "path";
|
|
6450
|
+
var TYPESCRIPT_MARKER = "tsconfig.json";
|
|
6451
|
+
var PYTHON_MARKER = "pyproject.toml";
|
|
6452
|
+
var ESLINT_CONFIG_FILES = [
|
|
6453
|
+
"eslint.config.ts",
|
|
6454
|
+
"eslint.config.js",
|
|
6455
|
+
"eslint.config.mjs",
|
|
6456
|
+
"eslint.config.cjs"
|
|
6457
|
+
];
|
|
6458
|
+
var ESLINT_PRODUCTION_CONFIG_FILES = [
|
|
6459
|
+
"eslint.config.production.ts",
|
|
6460
|
+
"eslint.config.production.js",
|
|
6461
|
+
"eslint.config.production.mjs",
|
|
6462
|
+
"eslint.config.production.cjs"
|
|
6463
|
+
];
|
|
6464
|
+
var defaultLanguageDetectionDeps = {
|
|
6465
|
+
existsSync: fs5.existsSync
|
|
6466
|
+
};
|
|
6467
|
+
function detectTypeScript(projectRoot, deps = defaultLanguageDetectionDeps) {
|
|
6468
|
+
const present = deps.existsSync(path4.join(projectRoot, TYPESCRIPT_MARKER));
|
|
6469
|
+
if (!present) {
|
|
6470
|
+
return { present: false };
|
|
6471
|
+
}
|
|
6472
|
+
const eslintConfigFile = ESLINT_CONFIG_FILES.find(
|
|
6473
|
+
(configFile) => deps.existsSync(path4.join(projectRoot, configFile))
|
|
6474
|
+
);
|
|
6475
|
+
const productionEslintConfigFile = ESLINT_PRODUCTION_CONFIG_FILES.find(
|
|
6476
|
+
(configFile) => deps.existsSync(path4.join(projectRoot, configFile))
|
|
6477
|
+
);
|
|
6478
|
+
return { present: true, eslintConfigFile, productionEslintConfigFile };
|
|
6479
|
+
}
|
|
6480
|
+
function detectPython(projectRoot, deps = defaultLanguageDetectionDeps) {
|
|
6481
|
+
const present = deps.existsSync(path4.join(projectRoot, PYTHON_MARKER));
|
|
6482
|
+
return { present };
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
// src/testing/languages/python-pytest-contract.ts
|
|
6486
|
+
var PYTHON_PYTEST_IGNORE_FLAG_PREFIX = "--ignore=spx/";
|
|
6487
|
+
var PYTHON_PYTEST_IGNORE_FLAG_SUFFIX = "/";
|
|
6488
|
+
var UV_COMMAND = "uv";
|
|
6489
|
+
var PYTEST_INVOKE_ARGS = ["run", "--active", "pytest"];
|
|
6490
|
+
|
|
6491
|
+
// src/testing/languages/python.ts
|
|
5853
6492
|
var PYTHON_TESTING_LANGUAGE_NAME = "python";
|
|
6493
|
+
var PYTHON_PRODUCT_INPUT_PATH = {
|
|
6494
|
+
CONFTEST: "conftest.py",
|
|
6495
|
+
HIDDEN_PYTEST_INI: ".pytest.ini",
|
|
6496
|
+
HIDDEN_PYTEST_TOML: ".pytest.toml",
|
|
6497
|
+
PYPROJECT: "pyproject.toml",
|
|
6498
|
+
PYTEST_TOML: "pytest.toml",
|
|
6499
|
+
PYTEST_INI: "pytest.ini",
|
|
6500
|
+
SETUP_CFG: "setup.cfg",
|
|
6501
|
+
SETUP_PY: "setup.py",
|
|
6502
|
+
TOX_INI: "tox.ini",
|
|
6503
|
+
UV_LOCK: "uv.lock"
|
|
6504
|
+
};
|
|
6505
|
+
var PYTHON_PRODUCT_INPUT_PATHS = Object.values(PYTHON_PRODUCT_INPUT_PATH);
|
|
5854
6506
|
var PYTHON_TEST_FILE_PREFIX = "test_";
|
|
5855
6507
|
var PYTHON_TEST_FILE_EXTENSION = ".py";
|
|
5856
6508
|
var PYTHON_TEST_FILE_PATTERNS = [`${PYTHON_TEST_FILE_PREFIX}*${PYTHON_TEST_FILE_EXTENSION}`];
|
|
5857
|
-
var PYTHON_PYTEST_IGNORE_FLAG_PREFIX = "--ignore=spx/";
|
|
5858
|
-
var PYTHON_PYTEST_IGNORE_FLAG_SUFFIX = "/";
|
|
5859
|
-
var UV_COMMAND = "uv";
|
|
5860
|
-
var PYTEST_INVOKE_ARGS = ["run", "pytest"];
|
|
5861
6509
|
function matchesTestFile(filePath) {
|
|
5862
|
-
return
|
|
6510
|
+
return basename2(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
|
|
6511
|
+
}
|
|
6512
|
+
function coveredProductInputPaths(coveredTestPaths2) {
|
|
6513
|
+
const paths = /* @__PURE__ */ new Set();
|
|
6514
|
+
for (const testPath of coveredTestPaths2) {
|
|
6515
|
+
if (!matchesTestFile(testPath)) continue;
|
|
6516
|
+
let directory = dirname5(testPath);
|
|
6517
|
+
while (directory !== "." && directory.length > 0) {
|
|
6518
|
+
paths.add(join20(directory, PYTHON_PRODUCT_INPUT_PATH.CONFTEST));
|
|
6519
|
+
const parent = dirname5(directory);
|
|
6520
|
+
if (parent === directory) break;
|
|
6521
|
+
directory = parent;
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6524
|
+
return [...paths].sort(compareAsciiStrings);
|
|
5863
6525
|
}
|
|
5864
6526
|
function excludeFlag(nodePath) {
|
|
5865
6527
|
return `${PYTHON_PYTEST_IGNORE_FLAG_PREFIX}${nodePath}${PYTHON_PYTEST_IGNORE_FLAG_SUFFIX}`;
|
|
5866
6528
|
}
|
|
5867
6529
|
function detect(projectRoot, deps) {
|
|
5868
|
-
return deps
|
|
6530
|
+
return deps?.isLanguagePresent?.(projectRoot) ?? detectPython(projectRoot).present;
|
|
5869
6531
|
}
|
|
5870
6532
|
async function runTests2(request, deps) {
|
|
5871
|
-
if (!
|
|
6533
|
+
if (!detect(request.projectRoot, deps)) {
|
|
5872
6534
|
return { invoked: false };
|
|
5873
6535
|
}
|
|
5874
6536
|
const args = [
|
|
@@ -5882,6 +6544,8 @@ async function runTests2(request, deps) {
|
|
|
5882
6544
|
var pythonTestingLanguage = {
|
|
5883
6545
|
name: PYTHON_TESTING_LANGUAGE_NAME,
|
|
5884
6546
|
testFilePatterns: PYTHON_TEST_FILE_PATTERNS,
|
|
6547
|
+
productInputPaths: PYTHON_PRODUCT_INPUT_PATHS,
|
|
6548
|
+
coveredProductInputPaths,
|
|
5885
6549
|
matchesTestFile,
|
|
5886
6550
|
excludeFlag,
|
|
5887
6551
|
detect,
|
|
@@ -5892,6 +6556,15 @@ var pythonTestingLanguage = {
|
|
|
5892
6556
|
var TYPESCRIPT_TESTING_LANGUAGE_NAME = "typescript";
|
|
5893
6557
|
var TYPESCRIPT_TEST_FILE_PATTERNS = ["*.test.ts", "*.test.tsx"];
|
|
5894
6558
|
var TYPESCRIPT_TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx"];
|
|
6559
|
+
var TYPESCRIPT_PRODUCT_INPUT_PATHS = [
|
|
6560
|
+
"package.json",
|
|
6561
|
+
"pnpm-lock.yaml",
|
|
6562
|
+
"tsconfig.json",
|
|
6563
|
+
"vitest.config.js",
|
|
6564
|
+
"vitest.config.mjs",
|
|
6565
|
+
"vitest.config.ts",
|
|
6566
|
+
"vitest.config.mts"
|
|
6567
|
+
];
|
|
5895
6568
|
var TYPESCRIPT_VITEST_EXCLUDE_FLAG_PREFIX = "--exclude=spx/";
|
|
5896
6569
|
var TYPESCRIPT_VITEST_EXCLUDE_FLAG_SUFFIX = "/**";
|
|
5897
6570
|
var PACKAGE_MANAGER_COMMAND = "pnpm";
|
|
@@ -5904,10 +6577,10 @@ function excludeFlag2(nodePath) {
|
|
|
5904
6577
|
return `${TYPESCRIPT_VITEST_EXCLUDE_FLAG_PREFIX}${nodePath}${TYPESCRIPT_VITEST_EXCLUDE_FLAG_SUFFIX}`;
|
|
5905
6578
|
}
|
|
5906
6579
|
function detect2(projectRoot, deps) {
|
|
5907
|
-
return deps
|
|
6580
|
+
return deps?.isLanguagePresent?.(projectRoot) ?? detectTypeScript(projectRoot).present;
|
|
5908
6581
|
}
|
|
5909
6582
|
async function runTests3(request, deps) {
|
|
5910
|
-
if (!
|
|
6583
|
+
if (!detect2(request.projectRoot, deps)) {
|
|
5911
6584
|
return { invoked: false };
|
|
5912
6585
|
}
|
|
5913
6586
|
const args = [
|
|
@@ -5923,6 +6596,7 @@ async function runTests3(request, deps) {
|
|
|
5923
6596
|
var typescriptTestingLanguage = {
|
|
5924
6597
|
name: TYPESCRIPT_TESTING_LANGUAGE_NAME,
|
|
5925
6598
|
testFilePatterns: TYPESCRIPT_TEST_FILE_PATTERNS,
|
|
6599
|
+
productInputPaths: TYPESCRIPT_PRODUCT_INPUT_PATHS,
|
|
5926
6600
|
matchesTestFile: matchesTestFile2,
|
|
5927
6601
|
excludeFlag: excludeFlag2,
|
|
5928
6602
|
detect: detect2,
|
|
@@ -6078,51 +6752,8 @@ function spawnManagedSubprocess(runner, command, args, options) {
|
|
|
6078
6752
|
});
|
|
6079
6753
|
}
|
|
6080
6754
|
|
|
6081
|
-
// src/validation/discovery/language-finder.ts
|
|
6082
|
-
import fs5 from "fs";
|
|
6083
|
-
import path4 from "path";
|
|
6084
|
-
var TYPESCRIPT_MARKER = "tsconfig.json";
|
|
6085
|
-
var PYTHON_MARKER = "pyproject.toml";
|
|
6086
|
-
var ESLINT_CONFIG_FILES = [
|
|
6087
|
-
"eslint.config.ts",
|
|
6088
|
-
"eslint.config.js",
|
|
6089
|
-
"eslint.config.mjs",
|
|
6090
|
-
"eslint.config.cjs"
|
|
6091
|
-
];
|
|
6092
|
-
var ESLINT_PRODUCTION_CONFIG_FILES = [
|
|
6093
|
-
"eslint.config.production.ts",
|
|
6094
|
-
"eslint.config.production.js",
|
|
6095
|
-
"eslint.config.production.mjs",
|
|
6096
|
-
"eslint.config.production.cjs"
|
|
6097
|
-
];
|
|
6098
|
-
var defaultLanguageDetectionDeps = {
|
|
6099
|
-
existsSync: fs5.existsSync
|
|
6100
|
-
};
|
|
6101
|
-
function detectTypeScript(projectRoot, deps = defaultLanguageDetectionDeps) {
|
|
6102
|
-
const present = deps.existsSync(path4.join(projectRoot, TYPESCRIPT_MARKER));
|
|
6103
|
-
if (!present) {
|
|
6104
|
-
return { present: false };
|
|
6105
|
-
}
|
|
6106
|
-
const eslintConfigFile = ESLINT_CONFIG_FILES.find(
|
|
6107
|
-
(configFile) => deps.existsSync(path4.join(projectRoot, configFile))
|
|
6108
|
-
);
|
|
6109
|
-
const productionEslintConfigFile = ESLINT_PRODUCTION_CONFIG_FILES.find(
|
|
6110
|
-
(configFile) => deps.existsSync(path4.join(projectRoot, configFile))
|
|
6111
|
-
);
|
|
6112
|
-
return { present: true, eslintConfigFile, productionEslintConfigFile };
|
|
6113
|
-
}
|
|
6114
|
-
function detectPython(projectRoot, deps = defaultLanguageDetectionDeps) {
|
|
6115
|
-
const present = deps.existsSync(path4.join(projectRoot, PYTHON_MARKER));
|
|
6116
|
-
return { present };
|
|
6117
|
-
}
|
|
6118
|
-
|
|
6119
6755
|
// src/interfaces/cli/testing-runner-deps.ts
|
|
6120
6756
|
var PROCESS_FAILURE_EXIT_CODE = 1;
|
|
6121
|
-
var NO_PRESENCE_DETECTOR_ERROR = "no presence detector configured for testing language";
|
|
6122
|
-
var PRESENCE_BY_LANGUAGE_NAME = {
|
|
6123
|
-
[typescriptTestingLanguage.name]: (productDir) => detectTypeScript(productDir).present,
|
|
6124
|
-
[pythonTestingLanguage.name]: (productDir) => detectPython(productDir).present
|
|
6125
|
-
};
|
|
6126
6757
|
function createCommandRunner(productDir, outStream) {
|
|
6127
6758
|
return (command, args) => new Promise((resolveResult) => {
|
|
6128
6759
|
const child = spawnManagedSubprocess(lifecycleProcessRunner, command, args, {
|
|
@@ -6136,13 +6767,7 @@ function createCommandRunner(productDir, outStream) {
|
|
|
6136
6767
|
}
|
|
6137
6768
|
function createRunnerDepsFor(productDir, outStream = process.stdout) {
|
|
6138
6769
|
const runCommand = createCommandRunner(productDir, outStream);
|
|
6139
|
-
return (
|
|
6140
|
-
const isLanguagePresent = PRESENCE_BY_LANGUAGE_NAME[language.name];
|
|
6141
|
-
if (isLanguagePresent === void 0) {
|
|
6142
|
-
throw new Error(`${NO_PRESENCE_DETECTOR_ERROR}: ${language.name}`);
|
|
6143
|
-
}
|
|
6144
|
-
return { isLanguagePresent, runCommand };
|
|
6145
|
-
};
|
|
6770
|
+
return () => ({ runCommand });
|
|
6146
6771
|
}
|
|
6147
6772
|
|
|
6148
6773
|
// src/interfaces/cli/spec.ts
|
|
@@ -6273,10 +6898,10 @@ var testingDomain = {
|
|
|
6273
6898
|
};
|
|
6274
6899
|
|
|
6275
6900
|
// src/commands/validation/markdown.ts
|
|
6276
|
-
import { relative as
|
|
6901
|
+
import { relative as relative3 } from "path";
|
|
6277
6902
|
|
|
6278
6903
|
// src/validation/config/path-filter.ts
|
|
6279
|
-
import { isAbsolute as isAbsolute2, relative as
|
|
6904
|
+
import { isAbsolute as isAbsolute2, relative as relative2 } from "path";
|
|
6280
6905
|
var PATH_PREFIX_SEPARATOR = "/";
|
|
6281
6906
|
function hasEffectiveValidationPathMetadata(filter) {
|
|
6282
6907
|
return "hasIncludeFilter" in filter && typeof filter.hasIncludeFilter === "boolean" && "noMatchingIncludes" in filter && typeof filter.noMatchingIncludes === "boolean";
|
|
@@ -6296,7 +6921,7 @@ function nonEmpty(values) {
|
|
|
6296
6921
|
return values?.filter((value) => value.length > 0) ?? [];
|
|
6297
6922
|
}
|
|
6298
6923
|
function toProjectRelativeValidationPath(projectRoot, path6) {
|
|
6299
|
-
return isAbsolute2(path6) ?
|
|
6924
|
+
return isAbsolute2(path6) ? relative2(projectRoot, path6) : path6;
|
|
6300
6925
|
}
|
|
6301
6926
|
function intersectIncludes(baseInclude, toolInclude) {
|
|
6302
6927
|
const base = nonEmpty(baseInclude);
|
|
@@ -6371,8 +6996,8 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
|
|
|
6371
6996
|
}
|
|
6372
6997
|
|
|
6373
6998
|
// src/validation/steps/markdown.ts
|
|
6374
|
-
import { existsSync
|
|
6375
|
-
import { basename as
|
|
6999
|
+
import { existsSync, statSync } from "fs";
|
|
7000
|
+
import { basename as basename3, dirname as dirname6, join as join21, relative as pathRelative } from "path";
|
|
6376
7001
|
import { main as markdownlintMain } from "markdownlint-cli2";
|
|
6377
7002
|
import relativeLinksRule from "markdownlint-rule-relative-links";
|
|
6378
7003
|
var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
|
|
@@ -6411,7 +7036,7 @@ function buildMarkdownlintConfig(directoryName) {
|
|
|
6411
7036
|
};
|
|
6412
7037
|
}
|
|
6413
7038
|
function getDefaultDirectories(projectRoot) {
|
|
6414
|
-
return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) =>
|
|
7039
|
+
return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join21(projectRoot, name)).filter((dir) => existsSync(dir));
|
|
6415
7040
|
}
|
|
6416
7041
|
function resolveMarkdownValidationTarget(path6, deps = defaultMarkdownValidationTargetDeps) {
|
|
6417
7042
|
if (isExistingDirectory(path6, deps)) {
|
|
@@ -6470,7 +7095,7 @@ async function validateMarkdown(options) {
|
|
|
6470
7095
|
async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
6471
7096
|
const errors = [];
|
|
6472
7097
|
const directory = targetDirectory(target);
|
|
6473
|
-
const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [
|
|
7098
|
+
const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename3(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
|
|
6474
7099
|
const { customRules, ...markdownlintConfig } = config;
|
|
6475
7100
|
const optionsOverride = {
|
|
6476
7101
|
config: {
|
|
@@ -6494,7 +7119,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
|
6494
7119
|
if (parsed) {
|
|
6495
7120
|
errors.push({
|
|
6496
7121
|
...parsed,
|
|
6497
|
-
file:
|
|
7122
|
+
file: join21(directory, parsed.file)
|
|
6498
7123
|
});
|
|
6499
7124
|
}
|
|
6500
7125
|
}
|
|
@@ -6502,7 +7127,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
|
6502
7127
|
return errors;
|
|
6503
7128
|
}
|
|
6504
7129
|
function targetDirectory(target) {
|
|
6505
|
-
return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ?
|
|
7130
|
+
return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname6(target.path) : target.path;
|
|
6506
7131
|
}
|
|
6507
7132
|
function hasMarkdownExtension(path6) {
|
|
6508
7133
|
const lastDot = path6.lastIndexOf(".");
|
|
@@ -6530,7 +7155,7 @@ function markdownlintConfigDirectoryName(directory, projectRoot) {
|
|
|
6530
7155
|
return rootSegment;
|
|
6531
7156
|
}
|
|
6532
7157
|
}
|
|
6533
|
-
return
|
|
7158
|
+
return basename3(directory);
|
|
6534
7159
|
}
|
|
6535
7160
|
|
|
6536
7161
|
// src/commands/validation/messages.ts
|
|
@@ -6606,7 +7231,7 @@ async function markdownCommand(options) {
|
|
|
6606
7231
|
path: path6
|
|
6607
7232
|
}));
|
|
6608
7233
|
const targets = unfilteredTargets.filter(
|
|
6609
|
-
(target) => pathPassesValidationFilter(
|
|
7234
|
+
(target) => pathPassesValidationFilter(relative3(cwd, target.path), pathFilter)
|
|
6610
7235
|
);
|
|
6611
7236
|
const skippedTargets = targetResolutions === void 0 ? [] : targetResolutions.map((resolution) => resolution.skipped).filter((skipped) => skipped !== void 0);
|
|
6612
7237
|
const skippedOutput = quiet ? [] : skippedTargets.map(formatSkippedFileScope);
|
|
@@ -7199,39 +7824,39 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7199
7824
|
const token = _scanner.scan();
|
|
7200
7825
|
switch (_scanner.getTokenError()) {
|
|
7201
7826
|
case 4:
|
|
7202
|
-
|
|
7827
|
+
handleError3(
|
|
7203
7828
|
14
|
|
7204
7829
|
/* ParseErrorCode.InvalidUnicode */
|
|
7205
7830
|
);
|
|
7206
7831
|
break;
|
|
7207
7832
|
case 5:
|
|
7208
|
-
|
|
7833
|
+
handleError3(
|
|
7209
7834
|
15
|
|
7210
7835
|
/* ParseErrorCode.InvalidEscapeCharacter */
|
|
7211
7836
|
);
|
|
7212
7837
|
break;
|
|
7213
7838
|
case 3:
|
|
7214
|
-
|
|
7839
|
+
handleError3(
|
|
7215
7840
|
13
|
|
7216
7841
|
/* ParseErrorCode.UnexpectedEndOfNumber */
|
|
7217
7842
|
);
|
|
7218
7843
|
break;
|
|
7219
7844
|
case 1:
|
|
7220
7845
|
if (!disallowComments) {
|
|
7221
|
-
|
|
7846
|
+
handleError3(
|
|
7222
7847
|
11
|
|
7223
7848
|
/* ParseErrorCode.UnexpectedEndOfComment */
|
|
7224
7849
|
);
|
|
7225
7850
|
}
|
|
7226
7851
|
break;
|
|
7227
7852
|
case 2:
|
|
7228
|
-
|
|
7853
|
+
handleError3(
|
|
7229
7854
|
12
|
|
7230
7855
|
/* ParseErrorCode.UnexpectedEndOfString */
|
|
7231
7856
|
);
|
|
7232
7857
|
break;
|
|
7233
7858
|
case 6:
|
|
7234
|
-
|
|
7859
|
+
handleError3(
|
|
7235
7860
|
16
|
|
7236
7861
|
/* ParseErrorCode.InvalidCharacter */
|
|
7237
7862
|
);
|
|
@@ -7241,7 +7866,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7241
7866
|
case 12:
|
|
7242
7867
|
case 13:
|
|
7243
7868
|
if (disallowComments) {
|
|
7244
|
-
|
|
7869
|
+
handleError3(
|
|
7245
7870
|
10
|
|
7246
7871
|
/* ParseErrorCode.InvalidCommentToken */
|
|
7247
7872
|
);
|
|
@@ -7250,7 +7875,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7250
7875
|
}
|
|
7251
7876
|
break;
|
|
7252
7877
|
case 16:
|
|
7253
|
-
|
|
7878
|
+
handleError3(
|
|
7254
7879
|
1
|
|
7255
7880
|
/* ParseErrorCode.InvalidSymbol */
|
|
7256
7881
|
);
|
|
@@ -7263,7 +7888,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7263
7888
|
}
|
|
7264
7889
|
}
|
|
7265
7890
|
}
|
|
7266
|
-
function
|
|
7891
|
+
function handleError3(error, skipUntilAfter = [], skipUntil = []) {
|
|
7267
7892
|
onError(error);
|
|
7268
7893
|
if (skipUntilAfter.length + skipUntil.length > 0) {
|
|
7269
7894
|
let token = _scanner.getToken();
|
|
@@ -7295,7 +7920,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7295
7920
|
const tokenValue = _scanner.getTokenValue();
|
|
7296
7921
|
let value = Number(tokenValue);
|
|
7297
7922
|
if (isNaN(value)) {
|
|
7298
|
-
|
|
7923
|
+
handleError3(
|
|
7299
7924
|
2
|
|
7300
7925
|
/* ParseErrorCode.InvalidNumberFormat */
|
|
7301
7926
|
);
|
|
@@ -7320,7 +7945,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7320
7945
|
}
|
|
7321
7946
|
function parseProperty() {
|
|
7322
7947
|
if (_scanner.getToken() !== 10) {
|
|
7323
|
-
|
|
7948
|
+
handleError3(3, [], [
|
|
7324
7949
|
2,
|
|
7325
7950
|
5
|
|
7326
7951
|
/* SyntaxKind.CommaToken */
|
|
@@ -7332,14 +7957,14 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7332
7957
|
onSeparator(":");
|
|
7333
7958
|
scanNext();
|
|
7334
7959
|
if (!parseValue()) {
|
|
7335
|
-
|
|
7960
|
+
handleError3(4, [], [
|
|
7336
7961
|
2,
|
|
7337
7962
|
5
|
|
7338
7963
|
/* SyntaxKind.CommaToken */
|
|
7339
7964
|
]);
|
|
7340
7965
|
}
|
|
7341
7966
|
} else {
|
|
7342
|
-
|
|
7967
|
+
handleError3(5, [], [
|
|
7343
7968
|
2,
|
|
7344
7969
|
5
|
|
7345
7970
|
/* SyntaxKind.CommaToken */
|
|
@@ -7355,7 +7980,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7355
7980
|
while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
|
|
7356
7981
|
if (_scanner.getToken() === 5) {
|
|
7357
7982
|
if (!needsComma) {
|
|
7358
|
-
|
|
7983
|
+
handleError3(4, [], []);
|
|
7359
7984
|
}
|
|
7360
7985
|
onSeparator(",");
|
|
7361
7986
|
scanNext();
|
|
@@ -7363,10 +7988,10 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7363
7988
|
break;
|
|
7364
7989
|
}
|
|
7365
7990
|
} else if (needsComma) {
|
|
7366
|
-
|
|
7991
|
+
handleError3(6, [], []);
|
|
7367
7992
|
}
|
|
7368
7993
|
if (!parseProperty()) {
|
|
7369
|
-
|
|
7994
|
+
handleError3(4, [], [
|
|
7370
7995
|
2,
|
|
7371
7996
|
5
|
|
7372
7997
|
/* SyntaxKind.CommaToken */
|
|
@@ -7376,7 +8001,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7376
8001
|
}
|
|
7377
8002
|
onObjectEnd();
|
|
7378
8003
|
if (_scanner.getToken() !== 2) {
|
|
7379
|
-
|
|
8004
|
+
handleError3(7, [
|
|
7380
8005
|
2
|
|
7381
8006
|
/* SyntaxKind.CloseBraceToken */
|
|
7382
8007
|
], []);
|
|
@@ -7393,7 +8018,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7393
8018
|
while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
|
|
7394
8019
|
if (_scanner.getToken() === 5) {
|
|
7395
8020
|
if (!needsComma) {
|
|
7396
|
-
|
|
8021
|
+
handleError3(4, [], []);
|
|
7397
8022
|
}
|
|
7398
8023
|
onSeparator(",");
|
|
7399
8024
|
scanNext();
|
|
@@ -7401,7 +8026,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7401
8026
|
break;
|
|
7402
8027
|
}
|
|
7403
8028
|
} else if (needsComma) {
|
|
7404
|
-
|
|
8029
|
+
handleError3(6, [], []);
|
|
7405
8030
|
}
|
|
7406
8031
|
if (isFirstElement) {
|
|
7407
8032
|
_jsonPath.push(0);
|
|
@@ -7410,7 +8035,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7410
8035
|
_jsonPath[_jsonPath.length - 1]++;
|
|
7411
8036
|
}
|
|
7412
8037
|
if (!parseValue()) {
|
|
7413
|
-
|
|
8038
|
+
handleError3(4, [], [
|
|
7414
8039
|
4,
|
|
7415
8040
|
5
|
|
7416
8041
|
/* SyntaxKind.CommaToken */
|
|
@@ -7423,7 +8048,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7423
8048
|
_jsonPath.pop();
|
|
7424
8049
|
}
|
|
7425
8050
|
if (_scanner.getToken() !== 4) {
|
|
7426
|
-
|
|
8051
|
+
handleError3(8, [
|
|
7427
8052
|
4
|
|
7428
8053
|
/* SyntaxKind.CloseBracketToken */
|
|
7429
8054
|
], []);
|
|
@@ -7449,15 +8074,15 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7449
8074
|
if (options.allowEmptyContent) {
|
|
7450
8075
|
return true;
|
|
7451
8076
|
}
|
|
7452
|
-
|
|
8077
|
+
handleError3(4, [], []);
|
|
7453
8078
|
return false;
|
|
7454
8079
|
}
|
|
7455
8080
|
if (!parseValue()) {
|
|
7456
|
-
|
|
8081
|
+
handleError3(4, [], []);
|
|
7457
8082
|
return false;
|
|
7458
8083
|
}
|
|
7459
8084
|
if (_scanner.getToken() !== 17) {
|
|
7460
|
-
|
|
8085
|
+
handleError3(9, [], []);
|
|
7461
8086
|
}
|
|
7462
8087
|
return true;
|
|
7463
8088
|
}
|
|
@@ -7515,8 +8140,8 @@ var ParseErrorCode;
|
|
|
7515
8140
|
})(ParseErrorCode || (ParseErrorCode = {}));
|
|
7516
8141
|
|
|
7517
8142
|
// src/validation/config/scope.ts
|
|
7518
|
-
import { existsSync as
|
|
7519
|
-
import { isAbsolute as isAbsolute3, join as
|
|
8143
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
8144
|
+
import { isAbsolute as isAbsolute3, join as join22 } from "path";
|
|
7520
8145
|
var TSCONFIG_FILES = {
|
|
7521
8146
|
full: "tsconfig.json",
|
|
7522
8147
|
production: "tsconfig.production.json"
|
|
@@ -7526,11 +8151,11 @@ var GLOB_MARKER = "*";
|
|
|
7526
8151
|
var HIDDEN_PATH_PREFIX = ".";
|
|
7527
8152
|
var defaultScopeDeps = {
|
|
7528
8153
|
readFileSync: readFileSync3,
|
|
7529
|
-
existsSync:
|
|
8154
|
+
existsSync: existsSync2,
|
|
7530
8155
|
readdirSync
|
|
7531
8156
|
};
|
|
7532
8157
|
function resolveProjectPath(projectRoot, path6) {
|
|
7533
|
-
return isAbsolute3(path6) ? path6 :
|
|
8158
|
+
return isAbsolute3(path6) ? path6 : join22(projectRoot, path6);
|
|
7534
8159
|
}
|
|
7535
8160
|
function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
|
|
7536
8161
|
try {
|
|
@@ -7574,7 +8199,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
|
|
|
7574
8199
|
if (hasDirectTsFiles) return true;
|
|
7575
8200
|
const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
|
|
7576
8201
|
for (const subdir of subdirs.slice(0, 5)) {
|
|
7577
|
-
if (hasTypeScriptFilesRecursive(
|
|
8202
|
+
if (hasTypeScriptFilesRecursive(join22(dirPath, subdir.name), maxDepth - 1, deps)) {
|
|
7578
8203
|
return true;
|
|
7579
8204
|
}
|
|
7580
8205
|
}
|
|
@@ -7597,7 +8222,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
|
|
|
7597
8222
|
});
|
|
7598
8223
|
if (!isExcluded) {
|
|
7599
8224
|
try {
|
|
7600
|
-
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(
|
|
8225
|
+
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join22(projectRoot, dir), 2, deps);
|
|
7601
8226
|
if (hasTypeScriptFiles) {
|
|
7602
8227
|
directories.add(dir);
|
|
7603
8228
|
}
|
|
@@ -7628,13 +8253,13 @@ function getLiteralTopLevelPatternDirectory(pattern) {
|
|
|
7628
8253
|
function filterActiveIncludePatterns(patterns, excludePatterns, projectRoot, deps) {
|
|
7629
8254
|
return patterns.filter((pattern) => {
|
|
7630
8255
|
const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
|
|
7631
|
-
return topLevelDir === null || deps.existsSync(
|
|
8256
|
+
return topLevelDir === null || deps.existsSync(join22(projectRoot, topLevelDir));
|
|
7632
8257
|
}).filter((pattern) => pathPassesValidationFilter(pattern, { exclude: excludePatterns }));
|
|
7633
8258
|
}
|
|
7634
8259
|
function getValidationDirectories(scope, projectRoot, deps = defaultScopeDeps) {
|
|
7635
8260
|
const config = resolveTypeScriptConfig(scope, projectRoot, deps);
|
|
7636
8261
|
const configDirectories = getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps);
|
|
7637
|
-
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(
|
|
8262
|
+
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join22(projectRoot, dir)));
|
|
7638
8263
|
return existingDirectories;
|
|
7639
8264
|
}
|
|
7640
8265
|
function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
|
|
@@ -7758,7 +8383,7 @@ function formatSkipMessage(stepName, result) {
|
|
|
7758
8383
|
|
|
7759
8384
|
// src/validation/steps/circular.ts
|
|
7760
8385
|
import madge from "madge";
|
|
7761
|
-
import { join as
|
|
8386
|
+
import { join as join23 } from "path";
|
|
7762
8387
|
var CIRCULAR_DEPS_KEYS = {
|
|
7763
8388
|
MADGE: "madge"
|
|
7764
8389
|
};
|
|
@@ -7767,11 +8392,11 @@ var defaultCircularDeps = {
|
|
|
7767
8392
|
};
|
|
7768
8393
|
async function validateCircularDependencies(scope, typescriptScope, projectRoot, deps = defaultCircularDeps) {
|
|
7769
8394
|
try {
|
|
7770
|
-
const analyzeDirectories = typescriptScope.directories.map((directory) =>
|
|
8395
|
+
const analyzeDirectories = typescriptScope.directories.map((directory) => join23(projectRoot, directory));
|
|
7771
8396
|
if (analyzeDirectories.length === 0) {
|
|
7772
8397
|
return { success: true };
|
|
7773
8398
|
}
|
|
7774
|
-
const tsConfigFile =
|
|
8399
|
+
const tsConfigFile = join23(projectRoot, TSCONFIG_FILES[scope]);
|
|
7775
8400
|
const excludeRegExps = typescriptScope.excludePatterns.map((pattern) => {
|
|
7776
8401
|
const cleanPattern = pattern.replace(/\/\*\*?\/\*$/, "");
|
|
7777
8402
|
const escaped = cleanPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -7851,10 +8476,10 @@ ${cycles}`;
|
|
|
7851
8476
|
}
|
|
7852
8477
|
|
|
7853
8478
|
// src/validation/steps/knip.ts
|
|
7854
|
-
import { existsSync as
|
|
8479
|
+
import { existsSync as existsSync3 } from "fs";
|
|
7855
8480
|
import { mkdtemp, rm, writeFile as writeFile3 } from "fs/promises";
|
|
7856
8481
|
import { tmpdir } from "os";
|
|
7857
|
-
import { isAbsolute as isAbsolute4, join as
|
|
8482
|
+
import { isAbsolute as isAbsolute4, join as join24 } from "path";
|
|
7858
8483
|
var defaultKnipProcessRunner = lifecycleProcessRunner;
|
|
7859
8484
|
var KNIP_COMMAND_TOKENS = {
|
|
7860
8485
|
COMMAND: "knip",
|
|
@@ -7862,7 +8487,7 @@ var KNIP_COMMAND_TOKENS = {
|
|
|
7862
8487
|
USE_TSCONFIG_FILES_FLAG: "--use-tsconfig-files"
|
|
7863
8488
|
};
|
|
7864
8489
|
var defaultKnipDeps = {
|
|
7865
|
-
existsSync:
|
|
8490
|
+
existsSync: existsSync3,
|
|
7866
8491
|
mkdtemp,
|
|
7867
8492
|
rm,
|
|
7868
8493
|
writeFile: writeFile3
|
|
@@ -7882,7 +8507,7 @@ async function validateKnip2(context, runner = defaultKnipProcessRunner, deps =
|
|
|
7882
8507
|
}
|
|
7883
8508
|
async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
7884
8509
|
const scopedTsconfig = typescriptScope.filteredByValidationPaths ? await createScopedKnipTsconfig(projectRoot, typescriptScope, deps) : void 0;
|
|
7885
|
-
const localBin =
|
|
8510
|
+
const localBin = join24(projectRoot, "node_modules", ".bin", "knip");
|
|
7886
8511
|
const binary = deps.existsSync(localBin) ? localBin : "npx";
|
|
7887
8512
|
const baseArgs = scopedTsconfig === void 0 ? [] : [
|
|
7888
8513
|
KNIP_COMMAND_TOKENS.USE_TSCONFIG_FILES_FLAG,
|
|
@@ -7937,12 +8562,12 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
|
7937
8562
|
});
|
|
7938
8563
|
}
|
|
7939
8564
|
async function createScopedKnipTsconfig(projectRoot, typescriptScope, deps) {
|
|
7940
|
-
const tempDir = await deps.mkdtemp(
|
|
7941
|
-
const configPath =
|
|
7942
|
-
const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern :
|
|
8565
|
+
const tempDir = await deps.mkdtemp(join24(tmpdir(), "validate-knip-"));
|
|
8566
|
+
const configPath = join24(tempDir, TSCONFIG_FILES.full);
|
|
8567
|
+
const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join24(projectRoot, pattern);
|
|
7943
8568
|
const project = typescriptScope.filePatterns.length > 0 ? typescriptScope.filePatterns : typescriptScope.directories.map((directory) => `${directory}/**/*.{js,ts,tsx}`);
|
|
7944
8569
|
const config = {
|
|
7945
|
-
extends:
|
|
8570
|
+
extends: join24(projectRoot, TSCONFIG_FILES.full),
|
|
7946
8571
|
include: project.map(toProjectPathPattern),
|
|
7947
8572
|
exclude: typescriptScope.excludePatterns.map(toProjectPathPattern)
|
|
7948
8573
|
};
|
|
@@ -7991,13 +8616,13 @@ async function knipCommand(options) {
|
|
|
7991
8616
|
}
|
|
7992
8617
|
|
|
7993
8618
|
// src/validation/steps/eslint.ts
|
|
7994
|
-
import { existsSync as
|
|
7995
|
-
import { join as
|
|
8619
|
+
import { existsSync as existsSync5 } from "fs";
|
|
8620
|
+
import { join as join26 } from "path";
|
|
7996
8621
|
|
|
7997
8622
|
// src/validation/lint-policy.ts
|
|
7998
8623
|
import { execFileSync } from "child_process";
|
|
7999
|
-
import { existsSync as
|
|
8000
|
-
import { join as
|
|
8624
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
8625
|
+
import { join as join25 } from "path";
|
|
8001
8626
|
|
|
8002
8627
|
// src/validation/lint-policy-constants.ts
|
|
8003
8628
|
var LINT_POLICY_MANIFESTS = {
|
|
@@ -8038,29 +8663,30 @@ var TEST_LINT_DEBT_NODE_MANIFEST_FILE = LINT_POLICY_MANIFESTS.TEST_LINT_DEBT_NOD
|
|
|
8038
8663
|
var TEST_LINT_DEBT_NODE_MANIFEST_KEY = LINT_POLICY_MANIFESTS.TEST_LINT_DEBT_NODES.key;
|
|
8039
8664
|
var TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_FILE = LINT_POLICY_MANIFESTS.TEST_OWNED_CONSTANT_DEBT_NODES.file;
|
|
8040
8665
|
var TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_KEY = LINT_POLICY_MANIFESTS.TEST_OWNED_CONSTANT_DEBT_NODES.key;
|
|
8041
|
-
var SPEC_TREE_ROOT =
|
|
8042
|
-
var SPEC_TREE_NODE_SUFFIX_PATTERN = /\.(enabler|outcome)$/;
|
|
8043
|
-
var DEPRECATED_SPEC_NODE_SUFFIX_PATTERN = /\.(capability|feature|story)$/;
|
|
8666
|
+
var SPEC_TREE_ROOT = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
|
|
8044
8667
|
var BASE_BRANCH_REFS = [LINT_POLICY_BASE_REFS.REMOTE_MAIN, LINT_POLICY_BASE_REFS.LOCAL_MAIN];
|
|
8668
|
+
function isSpecTreeNodePath(entry) {
|
|
8669
|
+
return NODE_SUFFIXES.some((suffix) => entry.endsWith(suffix));
|
|
8670
|
+
}
|
|
8045
8671
|
function readManifest(productDir, file, key) {
|
|
8046
8672
|
return parseLintPolicyManifest(
|
|
8047
|
-
readFileSync4(
|
|
8673
|
+
readFileSync4(join25(productDir, file), "utf-8"),
|
|
8048
8674
|
file,
|
|
8049
8675
|
key
|
|
8050
8676
|
);
|
|
8051
8677
|
}
|
|
8052
8678
|
function manifestExists(productDir, file) {
|
|
8053
|
-
return
|
|
8679
|
+
return existsSync4(join25(productDir, file));
|
|
8054
8680
|
}
|
|
8055
8681
|
function findDeprecatedSpecNodePath(productDir) {
|
|
8056
8682
|
function visit2(relativeDirectory) {
|
|
8057
|
-
const absoluteDirectory =
|
|
8683
|
+
const absoluteDirectory = join25(productDir, relativeDirectory);
|
|
8058
8684
|
for (const entry of readdirSync2(absoluteDirectory, { withFileTypes: true })) {
|
|
8059
8685
|
if (!entry.isDirectory()) {
|
|
8060
8686
|
continue;
|
|
8061
8687
|
}
|
|
8062
8688
|
const childPath = `${relativeDirectory}/${entry.name}`;
|
|
8063
|
-
if (
|
|
8689
|
+
if (SPEC_TREE_SUPERSEDED_NODE_SUFFIXES.some((suffix) => entry.name.endsWith(suffix))) {
|
|
8064
8690
|
return childPath;
|
|
8065
8691
|
}
|
|
8066
8692
|
const nestedDeprecatedPath = visit2(childPath);
|
|
@@ -8070,13 +8696,13 @@ function findDeprecatedSpecNodePath(productDir) {
|
|
|
8070
8696
|
}
|
|
8071
8697
|
return void 0;
|
|
8072
8698
|
}
|
|
8073
|
-
const specTreeRootPath =
|
|
8074
|
-
if (!
|
|
8699
|
+
const specTreeRootPath = join25(productDir, SPEC_TREE_ROOT);
|
|
8700
|
+
if (!existsSync4(specTreeRootPath)) {
|
|
8075
8701
|
return void 0;
|
|
8076
8702
|
}
|
|
8077
8703
|
return visit2(SPEC_TREE_ROOT);
|
|
8078
8704
|
}
|
|
8079
|
-
function assertManifestEntries(productDir, file, entries,
|
|
8705
|
+
function assertManifestEntries(productDir, file, entries, suffixPredicate, suffixDescription) {
|
|
8080
8706
|
const duplicates = entries.filter((entry, index) => entries.indexOf(entry) !== index);
|
|
8081
8707
|
if (duplicates.length > 0) {
|
|
8082
8708
|
throw new Error(
|
|
@@ -8093,11 +8719,11 @@ function assertManifestEntries(productDir, file, entries, suffixPattern, suffixD
|
|
|
8093
8719
|
if (entry.includes("..")) {
|
|
8094
8720
|
throw new Error(`${file} entry must not contain '..': ${entry}`);
|
|
8095
8721
|
}
|
|
8096
|
-
if (!
|
|
8722
|
+
if (!suffixPredicate(entry)) {
|
|
8097
8723
|
throw new Error(`${file} entry must ${suffixDescription}: ${entry}`);
|
|
8098
8724
|
}
|
|
8099
|
-
const absoluteEntry =
|
|
8100
|
-
if (!
|
|
8725
|
+
const absoluteEntry = join25(productDir, entry);
|
|
8726
|
+
if (!existsSync4(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
|
|
8101
8727
|
throw new Error(`${file} entry does not exist as a directory: ${entry}`);
|
|
8102
8728
|
}
|
|
8103
8729
|
}
|
|
@@ -8186,7 +8812,7 @@ function validateTestLintDebtNodeManifest(productDir, entries) {
|
|
|
8186
8812
|
productDir,
|
|
8187
8813
|
TEST_LINT_DEBT_NODE_MANIFEST_FILE,
|
|
8188
8814
|
entries,
|
|
8189
|
-
|
|
8815
|
+
isSpecTreeNodePath,
|
|
8190
8816
|
"be a Spec Tree node path"
|
|
8191
8817
|
);
|
|
8192
8818
|
assertManifestDoesNotGrow(
|
|
@@ -8201,7 +8827,7 @@ function validateTestOwnedConstantDebtNodeManifest(productDir, entries) {
|
|
|
8201
8827
|
productDir,
|
|
8202
8828
|
TEST_OWNED_CONSTANT_DEBT_NODE_MANIFEST_FILE,
|
|
8203
8829
|
entries,
|
|
8204
|
-
|
|
8830
|
+
isSpecTreeNodePath,
|
|
8205
8831
|
"be a Spec Tree node path"
|
|
8206
8832
|
);
|
|
8207
8833
|
assertManifestDoesNotGrow(
|
|
@@ -8356,8 +8982,8 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
|
|
|
8356
8982
|
scopeConfig: context.scopeConfig
|
|
8357
8983
|
});
|
|
8358
8984
|
return new Promise((resolve6) => {
|
|
8359
|
-
const localBin =
|
|
8360
|
-
const binary =
|
|
8985
|
+
const localBin = join26(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
|
|
8986
|
+
const binary = existsSync5(localBin) ? localBin : "npx";
|
|
8361
8987
|
const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
|
|
8362
8988
|
const eslintProcess = spawnManagedSubprocess(runner, binary, spawnArgs, {
|
|
8363
8989
|
cwd: projectRoot
|
|
@@ -8458,7 +9084,7 @@ async function lintCommand(options) {
|
|
|
8458
9084
|
|
|
8459
9085
|
// src/validation/literal/index.ts
|
|
8460
9086
|
import { readFile as readFile8 } from "fs/promises";
|
|
8461
|
-
import { isAbsolute as isAbsolute5, relative as
|
|
9087
|
+
import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve4 } from "path";
|
|
8462
9088
|
|
|
8463
9089
|
// src/lib/file-inclusion/predicates/ignore-source.ts
|
|
8464
9090
|
var IGNORE_SOURCE_LAYER = "ignore-source";
|
|
@@ -8493,7 +9119,7 @@ var ignoreSourceLayer = makeLayer(
|
|
|
8493
9119
|
|
|
8494
9120
|
// src/lib/file-inclusion/pipeline.ts
|
|
8495
9121
|
import { readdir as readdir7 } from "fs/promises";
|
|
8496
|
-
import { join as
|
|
9122
|
+
import { join as join27, relative as relative4, sep as sep2 } from "path";
|
|
8497
9123
|
var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
|
|
8498
9124
|
function isNodeError3(err) {
|
|
8499
9125
|
return err instanceof Error && "code" in err;
|
|
@@ -8511,11 +9137,11 @@ async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
|
|
|
8511
9137
|
for (const entry of dirEntries) {
|
|
8512
9138
|
if (entry.isDirectory()) {
|
|
8513
9139
|
if (artifactDirs.has(entry.name)) continue;
|
|
8514
|
-
const absolutePath =
|
|
9140
|
+
const absolutePath = join27(absoluteDir, entry.name);
|
|
8515
9141
|
await collectPaths(absolutePath, projectRoot, result, artifactDirs);
|
|
8516
9142
|
} else if (entry.isFile()) {
|
|
8517
|
-
const absolutePath =
|
|
8518
|
-
const rel =
|
|
9143
|
+
const absolutePath = join27(absoluteDir, entry.name);
|
|
9144
|
+
const rel = relative4(projectRoot, absolutePath);
|
|
8519
9145
|
result.push(sep2 === "/" ? rel : rel.split(sep2).join("/"));
|
|
8520
9146
|
}
|
|
8521
9147
|
}
|
|
@@ -8921,8 +9547,8 @@ async function validateLiteralReuse(input) {
|
|
|
8921
9547
|
const config = input.config ?? literalConfigDescriptor.defaults;
|
|
8922
9548
|
const request = input.files ? {
|
|
8923
9549
|
explicit: input.files.map((f) => {
|
|
8924
|
-
const abs = isAbsolute5(f) ? f :
|
|
8925
|
-
return
|
|
9550
|
+
const abs = isAbsolute5(f) ? f : resolve4(input.productDir, f);
|
|
9551
|
+
return relative5(input.productDir, abs).split(/[\\/]/g).join("/");
|
|
8926
9552
|
})
|
|
8927
9553
|
} : { walkRoot: input.productDir };
|
|
8928
9554
|
const scope = await runPipeline(
|
|
@@ -8933,7 +9559,7 @@ async function validateLiteralReuse(input) {
|
|
|
8933
9559
|
EMPTY_IGNORE_READER
|
|
8934
9560
|
);
|
|
8935
9561
|
const filtered = applyPathFilter2(scope.included, input.pathConfig);
|
|
8936
|
-
const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) =>
|
|
9562
|
+
const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve4(input.productDir, entry.path));
|
|
8937
9563
|
const collectOptions = {
|
|
8938
9564
|
visitorKeys: defaultVisitorKeys,
|
|
8939
9565
|
minStringLength: config.minStringLength,
|
|
@@ -8943,7 +9569,7 @@ async function validateLiteralReuse(input) {
|
|
|
8943
9569
|
const testOccurrencesByFile = /* @__PURE__ */ new Map();
|
|
8944
9570
|
const indexedOccurrencesByFile = /* @__PURE__ */ new Map();
|
|
8945
9571
|
for (const abs of candidateFiles) {
|
|
8946
|
-
const rel =
|
|
9572
|
+
const rel = relative5(input.productDir, abs).split(/[\\/]/g).join("/");
|
|
8947
9573
|
const content = await readSafe(abs);
|
|
8948
9574
|
if (content === null) continue;
|
|
8949
9575
|
const occurrences = collectLiterals(content, rel, collectOptions);
|
|
@@ -9172,24 +9798,24 @@ function formatLoc(loc) {
|
|
|
9172
9798
|
}
|
|
9173
9799
|
|
|
9174
9800
|
// src/validation/steps/typescript.ts
|
|
9175
|
-
import { existsSync as
|
|
9801
|
+
import { existsSync as existsSync6, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
9176
9802
|
import { mkdtemp as mkdtemp2 } from "fs/promises";
|
|
9177
|
-
import { isAbsolute as isAbsolute6, join as
|
|
9803
|
+
import { isAbsolute as isAbsolute6, join as join28 } from "path";
|
|
9178
9804
|
var defaultTypeScriptProcessRunner = lifecycleProcessRunner;
|
|
9179
9805
|
var defaultTypeScriptDeps = {
|
|
9180
9806
|
mkdtemp: mkdtemp2,
|
|
9181
9807
|
mkdirSync,
|
|
9182
9808
|
writeFileSync,
|
|
9183
9809
|
rmSync,
|
|
9184
|
-
existsSync:
|
|
9810
|
+
existsSync: existsSync6
|
|
9185
9811
|
};
|
|
9186
9812
|
var TEMPORARY_TSCONFIG_COMPILER_OPTIONS = { noEmit: true };
|
|
9187
9813
|
var TEMPORARY_TSCONFIG_PARENT_SEGMENTS = ["node_modules", ".cache", "spx"];
|
|
9188
9814
|
var TEMPORARY_TSCONFIG_DIR_PREFIX = "validate-ts-";
|
|
9189
9815
|
async function createTemporaryTsconfigDir(projectRoot, deps) {
|
|
9190
|
-
const parent =
|
|
9816
|
+
const parent = join28(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
|
|
9191
9817
|
deps.mkdirSync(parent, { recursive: true });
|
|
9192
|
-
return deps.mkdtemp(
|
|
9818
|
+
return deps.mkdtemp(join28(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
|
|
9193
9819
|
}
|
|
9194
9820
|
function buildTypeScriptArgs(context) {
|
|
9195
9821
|
const { scope, configFile } = context;
|
|
@@ -9197,11 +9823,11 @@ function buildTypeScriptArgs(context) {
|
|
|
9197
9823
|
}
|
|
9198
9824
|
async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defaultTypeScriptDeps) {
|
|
9199
9825
|
const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
|
|
9200
|
-
const configPath =
|
|
9826
|
+
const configPath = join28(tempDir, "tsconfig.json");
|
|
9201
9827
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
9202
|
-
const absoluteFiles = files.map((file) => isAbsolute6(file) ? file :
|
|
9828
|
+
const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join28(projectRoot, file));
|
|
9203
9829
|
const tempConfig = {
|
|
9204
|
-
extends:
|
|
9830
|
+
extends: join28(projectRoot, baseConfigFile),
|
|
9205
9831
|
files: absoluteFiles,
|
|
9206
9832
|
include: [],
|
|
9207
9833
|
exclude: [],
|
|
@@ -9218,11 +9844,11 @@ async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defa
|
|
|
9218
9844
|
}
|
|
9219
9845
|
async function createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps = defaultTypeScriptDeps) {
|
|
9220
9846
|
const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
|
|
9221
|
-
const configPath =
|
|
9847
|
+
const configPath = join28(tempDir, "tsconfig.json");
|
|
9222
9848
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
9223
|
-
const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern :
|
|
9849
|
+
const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join28(projectRoot, pattern);
|
|
9224
9850
|
const tempConfig = {
|
|
9225
|
-
extends:
|
|
9851
|
+
extends: join28(projectRoot, baseConfigFile),
|
|
9226
9852
|
include: scopeConfig.filePatterns.map(toProjectPathPattern),
|
|
9227
9853
|
exclude: scopeConfig.excludePatterns.map(toProjectPathPattern),
|
|
9228
9854
|
compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
|
|
@@ -9250,7 +9876,7 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9250
9876
|
const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, projectRoot, deps);
|
|
9251
9877
|
try {
|
|
9252
9878
|
return await new Promise((resolve6) => {
|
|
9253
|
-
const tscBin =
|
|
9879
|
+
const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
|
|
9254
9880
|
const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9255
9881
|
const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
|
|
9256
9882
|
const tscProcess = spawnManagedSubprocess(runner, tscBinary, tscArgs2, {
|
|
@@ -9283,7 +9909,7 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9283
9909
|
return { success: true, skipped: true };
|
|
9284
9910
|
}
|
|
9285
9911
|
const { configPath, cleanup } = await createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps);
|
|
9286
|
-
const tscBin =
|
|
9912
|
+
const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
|
|
9287
9913
|
tool = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9288
9914
|
tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
|
|
9289
9915
|
return new Promise((resolve6) => {
|
|
@@ -9305,7 +9931,7 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9305
9931
|
});
|
|
9306
9932
|
});
|
|
9307
9933
|
} else {
|
|
9308
|
-
const tscBin =
|
|
9934
|
+
const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
|
|
9309
9935
|
tool = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9310
9936
|
const rawArgs = buildTypeScriptArgs({ scope, configFile });
|
|
9311
9937
|
tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
|
|
@@ -9565,7 +10191,7 @@ function truncate(value) {
|
|
|
9565
10191
|
|
|
9566
10192
|
// src/validation/literal/allowlist-existing.ts
|
|
9567
10193
|
import { rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
9568
|
-
import { dirname as
|
|
10194
|
+
import { dirname as dirname7, join as join29 } from "path";
|
|
9569
10195
|
var EXIT_OK = 0;
|
|
9570
10196
|
var EXIT_ERROR = 1;
|
|
9571
10197
|
var INCLUDE_FIELD = "include";
|
|
@@ -9585,9 +10211,9 @@ var productionReader = {
|
|
|
9585
10211
|
};
|
|
9586
10212
|
var productionWriter = {
|
|
9587
10213
|
async write(filePath, content) {
|
|
9588
|
-
const dir =
|
|
10214
|
+
const dir = dirname7(filePath);
|
|
9589
10215
|
const random = Math.random().toString(RANDOM_BASE).slice(RANDOM_PREFIX_SLICE, RANDOM_PREFIX_SLICE + RANDOM_TOKEN_LENGTH);
|
|
9590
|
-
const tmpPath =
|
|
10216
|
+
const tmpPath = join29(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
|
|
9591
10217
|
await writeFile4(tmpPath, content, "utf8");
|
|
9592
10218
|
await rename4(tmpPath, filePath);
|
|
9593
10219
|
}
|
|
@@ -9908,15 +10534,403 @@ var validationDomain = {
|
|
|
9908
10534
|
}
|
|
9909
10535
|
};
|
|
9910
10536
|
|
|
10537
|
+
// src/domains/worktree/controlling-process.ts
|
|
10538
|
+
var CONTROLLING_PID_ENV = "SPX_WORKTREE_CONTROLLING_PID";
|
|
10539
|
+
var AGENT_RUNTIME_NAMES = ["claude", "codex"];
|
|
10540
|
+
var AGENT_COMMAND_PATTERN = new RegExp(`\\b(?:${AGENT_RUNTIME_NAMES.join("|")})\\b`, "i");
|
|
10541
|
+
var CONTROLLING_PROCESS_ERROR = {
|
|
10542
|
+
UNRESOLVED: "worktree controlling process could not be resolved"
|
|
10543
|
+
};
|
|
10544
|
+
var MAX_ANCESTRY_DEPTH = 64;
|
|
10545
|
+
var PID_RADIX = 10;
|
|
10546
|
+
function resolveControllingProcess(selfPid, table, env) {
|
|
10547
|
+
const host = table.currentHost();
|
|
10548
|
+
for (const pid of controllingPidCandidates(selfPid, table, env)) {
|
|
10549
|
+
const startedAt = table.startTimeOf(pid);
|
|
10550
|
+
if (startedAt !== void 0) return { ok: true, value: { pid, startedAt, host } };
|
|
10551
|
+
}
|
|
10552
|
+
return { ok: false, error: CONTROLLING_PROCESS_ERROR.UNRESOLVED };
|
|
10553
|
+
}
|
|
10554
|
+
function* controllingPidCandidates(selfPid, table, env) {
|
|
10555
|
+
const override = parsePid(env[CONTROLLING_PID_ENV]);
|
|
10556
|
+
if (override !== void 0) yield override;
|
|
10557
|
+
const agent = findAgentAncestor(selfPid, table);
|
|
10558
|
+
if (agent !== void 0) yield agent;
|
|
10559
|
+
const parent = table.parentOf(selfPid);
|
|
10560
|
+
if (parent !== void 0) yield parent;
|
|
10561
|
+
}
|
|
10562
|
+
function findAgentAncestor(selfPid, table) {
|
|
10563
|
+
let pid = table.parentOf(selfPid);
|
|
10564
|
+
for (let depth = 0; pid !== void 0 && depth < MAX_ANCESTRY_DEPTH; depth += 1) {
|
|
10565
|
+
const command = table.commandOf(pid);
|
|
10566
|
+
if (command !== void 0 && AGENT_COMMAND_PATTERN.test(command)) return pid;
|
|
10567
|
+
pid = table.parentOf(pid);
|
|
10568
|
+
}
|
|
10569
|
+
return void 0;
|
|
10570
|
+
}
|
|
10571
|
+
function parsePid(value) {
|
|
10572
|
+
if (value === void 0 || value.length === 0) return void 0;
|
|
10573
|
+
const parsed = Number.parseInt(value, PID_RADIX);
|
|
10574
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
10575
|
+
}
|
|
10576
|
+
|
|
10577
|
+
// src/domains/worktree/occupancy-store.ts
|
|
10578
|
+
import {
|
|
10579
|
+
mkdir as nodeMkdir2,
|
|
10580
|
+
readFile as nodeReadFile2,
|
|
10581
|
+
rename as nodeRename,
|
|
10582
|
+
rm as nodeRm,
|
|
10583
|
+
writeFile as nodeWriteFile2
|
|
10584
|
+
} from "fs/promises";
|
|
10585
|
+
import { join as join30 } from "path";
|
|
10586
|
+
var OCCUPANCY_STATUS = {
|
|
10587
|
+
UNCLAIMED: "unclaimed",
|
|
10588
|
+
OCCUPIED: "occupied",
|
|
10589
|
+
STALE: "stale"
|
|
10590
|
+
};
|
|
10591
|
+
var OCCUPANCY_CLAIM = {
|
|
10592
|
+
FILE_EXTENSION: ".claim",
|
|
10593
|
+
TEMP_EXTENSION: ".tmp"
|
|
10594
|
+
};
|
|
10595
|
+
var OCCUPANCY_ERROR = {
|
|
10596
|
+
INVALID_NAME: "worktree occupancy claim name must be a safe path segment",
|
|
10597
|
+
CLAIM_WRITE_FAILED: "worktree occupancy claim write failed",
|
|
10598
|
+
CLAIM_READ_FAILED: "worktree occupancy claim read failed",
|
|
10599
|
+
CLAIM_REMOVE_FAILED: "worktree occupancy claim remove failed",
|
|
10600
|
+
CLAIM_MALFORMED: "worktree occupancy claim record is malformed"
|
|
10601
|
+
};
|
|
10602
|
+
var ERROR_DETAIL_SEPARATOR2 = ": ";
|
|
10603
|
+
var defaultOccupancyFileSystem = {
|
|
10604
|
+
mkdir: async (path6, options) => {
|
|
10605
|
+
await nodeMkdir2(path6, options);
|
|
10606
|
+
},
|
|
10607
|
+
writeFile: async (path6, data) => {
|
|
10608
|
+
await nodeWriteFile2(path6, data);
|
|
10609
|
+
},
|
|
10610
|
+
rename: nodeRename,
|
|
10611
|
+
readFile: nodeReadFile2,
|
|
10612
|
+
rm: async (path6, options) => {
|
|
10613
|
+
await nodeRm(path6, options);
|
|
10614
|
+
}
|
|
10615
|
+
};
|
|
10616
|
+
function claimFileName(name) {
|
|
10617
|
+
return `${name}${OCCUPANCY_CLAIM.FILE_EXTENSION}`;
|
|
10618
|
+
}
|
|
10619
|
+
function claimFilePath(worktreesDir, name) {
|
|
10620
|
+
const validated = validateScopeToken(name);
|
|
10621
|
+
if (!validated.ok) return { ok: false, error: OCCUPANCY_ERROR.INVALID_NAME };
|
|
10622
|
+
return { ok: true, value: join30(worktreesDir, claimFileName(validated.value)) };
|
|
10623
|
+
}
|
|
10624
|
+
function classifyOccupancy(claim, probe) {
|
|
10625
|
+
if (claim === void 0) return OCCUPANCY_STATUS.UNCLAIMED;
|
|
10626
|
+
if (claim.host !== probe.currentHost()) return OCCUPANCY_STATUS.STALE;
|
|
10627
|
+
if (!probe.isAlive(claim.pid)) return OCCUPANCY_STATUS.STALE;
|
|
10628
|
+
const liveStartTime = probe.startTimeOf(claim.pid);
|
|
10629
|
+
if (liveStartTime !== void 0 && liveStartTime !== claim.startedAt) return OCCUPANCY_STATUS.STALE;
|
|
10630
|
+
return OCCUPANCY_STATUS.OCCUPIED;
|
|
10631
|
+
}
|
|
10632
|
+
async function writeClaim(worktreesDir, name, record, options = {}) {
|
|
10633
|
+
const fs7 = options.fs ?? defaultOccupancyFileSystem;
|
|
10634
|
+
const pathResult = claimFilePath(worktreesDir, name);
|
|
10635
|
+
if (!pathResult.ok) return pathResult;
|
|
10636
|
+
const claimPath = pathResult.value;
|
|
10637
|
+
const tempPath = `${claimPath}${OCCUPANCY_CLAIM.TEMP_EXTENSION}`;
|
|
10638
|
+
try {
|
|
10639
|
+
await fs7.mkdir(worktreesDir, { recursive: true });
|
|
10640
|
+
await fs7.writeFile(tempPath, serializeClaim(record));
|
|
10641
|
+
await fs7.rename(tempPath, claimPath);
|
|
10642
|
+
return { ok: true, value: claimPath };
|
|
10643
|
+
} catch (error) {
|
|
10644
|
+
return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_WRITE_FAILED, toErrorMessage3(error)) };
|
|
10645
|
+
}
|
|
10646
|
+
}
|
|
10647
|
+
async function readClaim(worktreesDir, name, options = {}) {
|
|
10648
|
+
const fs7 = options.fs ?? defaultOccupancyFileSystem;
|
|
10649
|
+
const pathResult = claimFilePath(worktreesDir, name);
|
|
10650
|
+
if (!pathResult.ok) return pathResult;
|
|
10651
|
+
let content;
|
|
10652
|
+
try {
|
|
10653
|
+
content = await fs7.readFile(pathResult.value, "utf8");
|
|
10654
|
+
} catch (error) {
|
|
10655
|
+
if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return { ok: true, value: void 0 };
|
|
10656
|
+
return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_READ_FAILED, toErrorMessage3(error)) };
|
|
10657
|
+
}
|
|
10658
|
+
return parseClaim(content);
|
|
10659
|
+
}
|
|
10660
|
+
async function removeClaim(worktreesDir, name, options = {}) {
|
|
10661
|
+
const fs7 = options.fs ?? defaultOccupancyFileSystem;
|
|
10662
|
+
const pathResult = claimFilePath(worktreesDir, name);
|
|
10663
|
+
if (!pathResult.ok) return pathResult;
|
|
10664
|
+
try {
|
|
10665
|
+
await fs7.rm(pathResult.value, { force: true });
|
|
10666
|
+
return { ok: true, value: void 0 };
|
|
10667
|
+
} catch (error) {
|
|
10668
|
+
return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_REMOVE_FAILED, toErrorMessage3(error)) };
|
|
10669
|
+
}
|
|
10670
|
+
}
|
|
10671
|
+
async function readOccupancy(worktreesDir, name, probe, options = {}) {
|
|
10672
|
+
const claimResult = await readClaim(worktreesDir, name, options);
|
|
10673
|
+
if (!claimResult.ok) return claimResult;
|
|
10674
|
+
return { ok: true, value: classifyOccupancy(claimResult.value, probe) };
|
|
10675
|
+
}
|
|
10676
|
+
function serializeClaim(record) {
|
|
10677
|
+
return JSON.stringify({
|
|
10678
|
+
sessionId: record.sessionId,
|
|
10679
|
+
host: record.host,
|
|
10680
|
+
pid: record.pid,
|
|
10681
|
+
startedAt: record.startedAt
|
|
10682
|
+
});
|
|
10683
|
+
}
|
|
10684
|
+
function parseClaim(content) {
|
|
10685
|
+
let parsed;
|
|
10686
|
+
try {
|
|
10687
|
+
parsed = JSON.parse(content);
|
|
10688
|
+
} catch {
|
|
10689
|
+
return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
|
|
10690
|
+
}
|
|
10691
|
+
if (!isWorktreeClaimRecord(parsed)) return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
|
|
10692
|
+
return { ok: true, value: parsed };
|
|
10693
|
+
}
|
|
10694
|
+
function isWorktreeClaimRecord(value) {
|
|
10695
|
+
if (typeof value !== "object" || value === null) return false;
|
|
10696
|
+
const candidate = value;
|
|
10697
|
+
return typeof candidate.sessionId === "string" && typeof candidate.host === "string" && typeof candidate.pid === "number" && typeof candidate.startedAt === "string";
|
|
10698
|
+
}
|
|
10699
|
+
function formatOccupancyError(code, detail) {
|
|
10700
|
+
return `${code}${ERROR_DETAIL_SEPARATOR2}${detail}`;
|
|
10701
|
+
}
|
|
10702
|
+
function toErrorMessage3(error) {
|
|
10703
|
+
return error instanceof Error ? error.message : String(error);
|
|
10704
|
+
}
|
|
10705
|
+
|
|
10706
|
+
// src/domains/worktree/process-table.ts
|
|
10707
|
+
import { spawnSync } from "child_process";
|
|
10708
|
+
import { hostname } from "os";
|
|
10709
|
+
var PS_COMMAND = "/bin/ps";
|
|
10710
|
+
var PS_FIELD = {
|
|
10711
|
+
START_TIME: "lstart",
|
|
10712
|
+
PARENT_PID: "ppid",
|
|
10713
|
+
// The full command line, not `comm` — `comm` reports only the executable
|
|
10714
|
+
// basename, so a shebang-installed agent run through an interpreter reads as
|
|
10715
|
+
// the interpreter (e.g. `node`) and the agent-runtime match misses it. `args`
|
|
10716
|
+
// carries the script path, so both native and interpreted agents are matched.
|
|
10717
|
+
COMMAND: "args"
|
|
10718
|
+
};
|
|
10719
|
+
var SIGNAL_LIVENESS_PROBE = 0;
|
|
10720
|
+
var PROCESS_EXISTS_NO_PERMISSION = "EPERM";
|
|
10721
|
+
var PID_RADIX2 = 10;
|
|
10722
|
+
var STABLE_PS_ENV = { ...process.env, TZ: "UTC", LC_ALL: "C" };
|
|
10723
|
+
function psField(pid, field) {
|
|
10724
|
+
const result = spawnSync(PS_COMMAND, ["-o", `${field}=`, "-p", String(pid)], {
|
|
10725
|
+
encoding: "utf8",
|
|
10726
|
+
env: STABLE_PS_ENV
|
|
10727
|
+
});
|
|
10728
|
+
if (result.status !== 0 || typeof result.stdout !== "string") return void 0;
|
|
10729
|
+
const value = result.stdout.trim();
|
|
10730
|
+
return value.length > 0 ? value : void 0;
|
|
10731
|
+
}
|
|
10732
|
+
var defaultProcessTable = {
|
|
10733
|
+
currentHost: () => hostname(),
|
|
10734
|
+
isAlive: (pid) => {
|
|
10735
|
+
try {
|
|
10736
|
+
process.kill(pid, SIGNAL_LIVENESS_PROBE);
|
|
10737
|
+
return true;
|
|
10738
|
+
} catch (error) {
|
|
10739
|
+
return hasErrorCode(error, PROCESS_EXISTS_NO_PERMISSION);
|
|
10740
|
+
}
|
|
10741
|
+
},
|
|
10742
|
+
startTimeOf: (pid) => psField(pid, PS_FIELD.START_TIME),
|
|
10743
|
+
parentOf: (pid) => {
|
|
10744
|
+
const value = psField(pid, PS_FIELD.PARENT_PID);
|
|
10745
|
+
if (value === void 0) return void 0;
|
|
10746
|
+
const parsed = Number.parseInt(value, PID_RADIX2);
|
|
10747
|
+
return Number.isInteger(parsed) ? parsed : void 0;
|
|
10748
|
+
},
|
|
10749
|
+
commandOf: (pid) => psField(pid, PS_FIELD.COMMAND)
|
|
10750
|
+
};
|
|
10751
|
+
|
|
10752
|
+
// src/commands/worktree/resolve.ts
|
|
10753
|
+
import { stat as stat4 } from "fs/promises";
|
|
10754
|
+
import { dirname as dirname8, resolve as resolve5 } from "path";
|
|
10755
|
+
|
|
10756
|
+
// src/domains/worktree/worktree-name.ts
|
|
10757
|
+
import { basename as basename4 } from "path";
|
|
10758
|
+
var SEPARATOR_CHARACTER = /[^a-z0-9_]/;
|
|
10759
|
+
var NAME_SEPARATOR = "-";
|
|
10760
|
+
function worktreeClaimName(worktreeRoot) {
|
|
10761
|
+
return basename4(worktreeRoot).toLowerCase().split(SEPARATOR_CHARACTER).filter((segment) => segment.length > 0).join(NAME_SEPARATOR);
|
|
10762
|
+
}
|
|
10763
|
+
|
|
10764
|
+
// src/commands/worktree/resolve.ts
|
|
10765
|
+
var WORKTREE_RESOLVE_ERROR = {
|
|
10766
|
+
NOT_A_WORKTREE: "path resolves to no worktree"
|
|
10767
|
+
};
|
|
10768
|
+
async function resolveWorktreesDir(options) {
|
|
10769
|
+
if (options.worktreesDir !== void 0) return options.worktreesDir;
|
|
10770
|
+
const resolved = await resolveWorktreesScopeDir({ cwd: options.cwd, deps: options.gitDeps });
|
|
10771
|
+
options.onWarning?.(resolved.warning);
|
|
10772
|
+
return resolved.worktreesDir;
|
|
10773
|
+
}
|
|
10774
|
+
async function resolveCurrentWorktreeName(options) {
|
|
10775
|
+
const worktree = await detectWorktreeProductRoot(options.cwd, options.gitDeps);
|
|
10776
|
+
return worktreeClaimName(worktree.productDir);
|
|
10777
|
+
}
|
|
10778
|
+
async function resolveTargetWorktree(options) {
|
|
10779
|
+
const base = options.cwd ?? process.cwd();
|
|
10780
|
+
const targetPath = options.worktree === void 0 ? base : resolve5(base, options.worktree);
|
|
10781
|
+
const targetGitPath = await isExistingNonDirectory(targetPath) ? dirname8(targetPath) : targetPath;
|
|
10782
|
+
const worktree = await detectWorktreeProductRoot(targetGitPath, options.gitDeps);
|
|
10783
|
+
if (!worktree.isGitRepo) {
|
|
10784
|
+
return { ok: false, error: `${WORKTREE_RESOLVE_ERROR.NOT_A_WORKTREE}: ${options.worktree ?? base}` };
|
|
10785
|
+
}
|
|
10786
|
+
return { ok: true, value: { name: worktreeClaimName(worktree.productDir), worktreeRoot: worktree.productDir } };
|
|
10787
|
+
}
|
|
10788
|
+
async function isExistingNonDirectory(path6) {
|
|
10789
|
+
try {
|
|
10790
|
+
const pathStats = await stat4(path6);
|
|
10791
|
+
return !pathStats.isDirectory();
|
|
10792
|
+
} catch {
|
|
10793
|
+
return false;
|
|
10794
|
+
}
|
|
10795
|
+
}
|
|
10796
|
+
|
|
10797
|
+
// src/commands/worktree/claim.ts
|
|
10798
|
+
async function claimCommand(options) {
|
|
10799
|
+
const table = options.processTable ?? defaultProcessTable;
|
|
10800
|
+
const controlling = resolveControllingProcess(options.selfPid ?? process.pid, table, options.env ?? process.env);
|
|
10801
|
+
if (!controlling.ok) return controlling;
|
|
10802
|
+
const worktreesDir = await resolveWorktreesDir(options);
|
|
10803
|
+
const name = await resolveCurrentWorktreeName(options);
|
|
10804
|
+
return writeClaim(
|
|
10805
|
+
worktreesDir,
|
|
10806
|
+
name,
|
|
10807
|
+
{
|
|
10808
|
+
sessionId: options.sessionId,
|
|
10809
|
+
host: controlling.value.host,
|
|
10810
|
+
pid: controlling.value.pid,
|
|
10811
|
+
startedAt: controlling.value.startedAt
|
|
10812
|
+
},
|
|
10813
|
+
{ fs: options.fs }
|
|
10814
|
+
);
|
|
10815
|
+
}
|
|
10816
|
+
|
|
10817
|
+
// src/commands/worktree/release.ts
|
|
10818
|
+
async function releaseCommand2(options) {
|
|
10819
|
+
const worktreesDir = await resolveWorktreesDir(options);
|
|
10820
|
+
const name = await resolveCurrentWorktreeName(options);
|
|
10821
|
+
return removeClaim(worktreesDir, name, { fs: options.fs });
|
|
10822
|
+
}
|
|
10823
|
+
|
|
10824
|
+
// src/commands/worktree/status.ts
|
|
10825
|
+
var WORKTREE_STATUS_FORMAT = {
|
|
10826
|
+
JSON: "json",
|
|
10827
|
+
TEXT: "text"
|
|
10828
|
+
};
|
|
10829
|
+
async function statusCommand2(options) {
|
|
10830
|
+
const table = options.processTable ?? defaultProcessTable;
|
|
10831
|
+
const multiTargetRequest = options.worktrees !== void 0 && options.worktrees.length > 1;
|
|
10832
|
+
const targets = await resolveStatusTargets(options);
|
|
10833
|
+
if (!targets.ok) return targets;
|
|
10834
|
+
const records = [];
|
|
10835
|
+
for (const target of targets.value) {
|
|
10836
|
+
const worktreesDir = await resolveWorktreesDir({ ...options, cwd: target.worktreeRoot });
|
|
10837
|
+
const occupancy = await readOccupancy(worktreesDir, target.name, table, { fs: options.fs });
|
|
10838
|
+
if (!occupancy.ok) return occupancy;
|
|
10839
|
+
records.push({ worktree: target.name, status: occupancy.value });
|
|
10840
|
+
}
|
|
10841
|
+
return { ok: true, value: renderStatus(records, options.format, multiTargetRequest) };
|
|
10842
|
+
}
|
|
10843
|
+
async function resolveStatusTargets(options) {
|
|
10844
|
+
const requested = options.worktrees;
|
|
10845
|
+
if (requested === void 0 || requested.length === 0) {
|
|
10846
|
+
const target = await resolveTargetWorktree(options);
|
|
10847
|
+
if (!target.ok) return target;
|
|
10848
|
+
return { ok: true, value: [target.value] };
|
|
10849
|
+
}
|
|
10850
|
+
const targets = [];
|
|
10851
|
+
let firstError;
|
|
10852
|
+
for (const worktree of requested) {
|
|
10853
|
+
const target = await resolveTargetWorktree({ ...options, worktree });
|
|
10854
|
+
if (target.ok) {
|
|
10855
|
+
targets.push(target.value);
|
|
10856
|
+
} else {
|
|
10857
|
+
firstError ??= target.error;
|
|
10858
|
+
}
|
|
10859
|
+
}
|
|
10860
|
+
if (targets.length === 0) {
|
|
10861
|
+
return { ok: false, error: firstError ?? "no worktree status targets resolved" };
|
|
10862
|
+
}
|
|
10863
|
+
return { ok: true, value: targets };
|
|
10864
|
+
}
|
|
10865
|
+
function renderStatus(records, format2, multiTargetRequest) {
|
|
10866
|
+
if (format2 === WORKTREE_STATUS_FORMAT.JSON) {
|
|
10867
|
+
return JSON.stringify(multiTargetRequest ? records : records[0]);
|
|
10868
|
+
}
|
|
10869
|
+
return records.map(renderTextStatus).join("\n");
|
|
10870
|
+
}
|
|
10871
|
+
function renderTextStatus(record) {
|
|
10872
|
+
return `${record.worktree} ${record.status}`;
|
|
10873
|
+
}
|
|
10874
|
+
|
|
10875
|
+
// src/interfaces/cli/worktree.ts
|
|
10876
|
+
var WORKTREE_CLI = {
|
|
10877
|
+
COMMAND: "worktree",
|
|
10878
|
+
CLAIM: "claim",
|
|
10879
|
+
STATUS: "status",
|
|
10880
|
+
RELEASE: "release",
|
|
10881
|
+
WORKTREE_ARGUMENT: "[worktrees...]",
|
|
10882
|
+
SESSION_ID_FLAG: "--session-id",
|
|
10883
|
+
FORMAT_FLAG: "--format",
|
|
10884
|
+
WORKTREES_DIR_FLAG: "--worktrees-dir"
|
|
10885
|
+
};
|
|
10886
|
+
var WORKTREE_DOMAIN_DESCRIPTION = "Coordinate worktree occupancy across a bare-repository pool";
|
|
10887
|
+
function handleError2(error) {
|
|
10888
|
+
console.error("Error:", error);
|
|
10889
|
+
process.exit(1);
|
|
10890
|
+
}
|
|
10891
|
+
function registerWorktreeCommands(worktreeCmd) {
|
|
10892
|
+
worktreeCmd.command(WORKTREE_CLI.CLAIM).description("Record a worktree-occupancy claim for the running worktree").requiredOption(`${WORKTREE_CLI.SESSION_ID_FLAG} <id>`, "Claiming agent session id").option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (options) => {
|
|
10893
|
+
const result = await claimCommand({
|
|
10894
|
+
sessionId: options.sessionId,
|
|
10895
|
+
worktreesDir: options.worktreesDir,
|
|
10896
|
+
onWarning: writeWarning
|
|
10897
|
+
});
|
|
10898
|
+
if (!result.ok) handleError2(result.error);
|
|
10899
|
+
});
|
|
10900
|
+
worktreeCmd.command(`${WORKTREE_CLI.STATUS} ${WORKTREE_CLI.WORKTREE_ARGUMENT}`).description("Report a worktree's occupancy (occupied | unclaimed | stale)").option(`${WORKTREE_CLI.FORMAT_FLAG} <format>`, "Output format (text|json)", WORKTREE_STATUS_FORMAT.TEXT).option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (worktrees, options) => {
|
|
10901
|
+
const result = await statusCommand2({
|
|
10902
|
+
worktrees,
|
|
10903
|
+
format: options.format,
|
|
10904
|
+
worktreesDir: options.worktreesDir,
|
|
10905
|
+
onWarning: writeWarning
|
|
10906
|
+
});
|
|
10907
|
+
if (!result.ok) handleError2(result.error);
|
|
10908
|
+
console.log(result.value);
|
|
10909
|
+
});
|
|
10910
|
+
worktreeCmd.command(WORKTREE_CLI.RELEASE).description("Release the running worktree's occupancy claim").option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (options) => {
|
|
10911
|
+
const result = await releaseCommand2({ worktreesDir: options.worktreesDir, onWarning: writeWarning });
|
|
10912
|
+
if (!result.ok) handleError2(result.error);
|
|
10913
|
+
});
|
|
10914
|
+
}
|
|
10915
|
+
var worktreeDomain = {
|
|
10916
|
+
name: WORKTREE_CLI.COMMAND,
|
|
10917
|
+
description: WORKTREE_DOMAIN_DESCRIPTION,
|
|
10918
|
+
register: (program2) => {
|
|
10919
|
+
const worktreeCmd = program2.command(WORKTREE_CLI.COMMAND).description(WORKTREE_DOMAIN_DESCRIPTION);
|
|
10920
|
+
registerWorktreeCommands(worktreeCmd);
|
|
10921
|
+
}
|
|
10922
|
+
};
|
|
10923
|
+
|
|
9911
10924
|
// src/interfaces/cli/registry.ts
|
|
9912
10925
|
var CLI_DOMAINS = [
|
|
9913
|
-
auditDomain,
|
|
9914
10926
|
claudeDomain,
|
|
10927
|
+
compactDomain,
|
|
9915
10928
|
configDomain,
|
|
9916
10929
|
sessionDomain,
|
|
9917
10930
|
specDomain,
|
|
9918
10931
|
testingDomain,
|
|
9919
|
-
validationDomain
|
|
10932
|
+
validationDomain,
|
|
10933
|
+
worktreeDomain
|
|
9920
10934
|
];
|
|
9921
10935
|
|
|
9922
10936
|
// src/cli.ts
|