@outcomeeng/spx 0.5.5 → 0.5.6
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 +6 -5
- package/dist/cli.js +1416 -626
- package/dist/cli.js.map +1 -1
- package/package.json +12 -8
package/dist/cli.js
CHANGED
|
@@ -1066,11 +1066,13 @@ var STATE_STORE_ERROR = {
|
|
|
1066
1066
|
RECORD_WRITE_FAILED: "state-store record write failed",
|
|
1067
1067
|
RECORD_READ_FAILED: "state-store record read failed"
|
|
1068
1068
|
};
|
|
1069
|
+
var STATE_STORE_RUN_TOKEN = {
|
|
1070
|
+
ID_BYTES: 6,
|
|
1071
|
+
TIMESTAMP_MILLISECOND_DIGITS: 3
|
|
1072
|
+
};
|
|
1069
1073
|
var HEX_ENCODING = "hex";
|
|
1070
|
-
var RUN_ID_BYTES = 6;
|
|
1071
1074
|
var RUN_FILE_CREATE_ATTEMPTS = 10;
|
|
1072
1075
|
var RUN_TIMESTAMP_SEPARATOR = "_";
|
|
1073
|
-
var RUN_TIMESTAMP_MILLISECOND_DIGITS = 3;
|
|
1074
1076
|
var SLUG_SEPARATOR = "-";
|
|
1075
1077
|
var EMPTY_STRING = "";
|
|
1076
1078
|
var RUN_TOKEN_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[a-f0-9]{12}$/;
|
|
@@ -1144,11 +1146,19 @@ function formatRunTimestamp(date) {
|
|
|
1144
1146
|
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
1145
1147
|
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
1146
1148
|
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
1147
|
-
const milliseconds = String(date.getUTCMilliseconds()).padStart(
|
|
1149
|
+
const milliseconds = String(date.getUTCMilliseconds()).padStart(
|
|
1150
|
+
STATE_STORE_RUN_TOKEN.TIMESTAMP_MILLISECOND_DIGITS,
|
|
1151
|
+
"0"
|
|
1152
|
+
);
|
|
1148
1153
|
return `${year}-${month}-${day}${RUN_TIMESTAMP_SEPARATOR}${hours}-${minutes}-${seconds}-${milliseconds}`;
|
|
1149
1154
|
}
|
|
1150
1155
|
function generateRunId(randomBytes = nodeRandomBytes) {
|
|
1151
|
-
return randomBytes(
|
|
1156
|
+
return randomBytes(STATE_STORE_RUN_TOKEN.ID_BYTES).toString(HEX_ENCODING);
|
|
1157
|
+
}
|
|
1158
|
+
function createStateStoreRunToken(options) {
|
|
1159
|
+
const startedAt = formatRunTimestamp(options.date);
|
|
1160
|
+
const runId = generateRunId(options.randomBytes);
|
|
1161
|
+
return { runToken: `${startedAt}${SLUG_SEPARATOR}${runId}`, runId, startedAt };
|
|
1152
1162
|
}
|
|
1153
1163
|
function runFileName(runToken) {
|
|
1154
1164
|
return `${STATE_STORE_PATH.RUN_FILE_PREFIX}${runToken}${STATE_STORE_PATH.JSONL_EXTENSION}`;
|
|
@@ -1165,7 +1175,6 @@ async function createJsonlRunFile(scopeDir, domainName, options = {}) {
|
|
|
1165
1175
|
if (!domainRunsDir.ok) return domainRunsDir;
|
|
1166
1176
|
const maxAttempts = options.maxAttempts ?? RUN_FILE_CREATE_ATTEMPTS;
|
|
1167
1177
|
const startedDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1168
|
-
const startedAt = formatRunTimestamp(startedDate);
|
|
1169
1178
|
const randomBytes = options.randomBytes ?? nodeRandomBytes;
|
|
1170
1179
|
try {
|
|
1171
1180
|
await fs7.mkdir(domainRunsDir.value, { recursive: true });
|
|
@@ -1176,15 +1185,14 @@ async function createJsonlRunFile(scopeDir, domainName, options = {}) {
|
|
|
1176
1185
|
};
|
|
1177
1186
|
}
|
|
1178
1187
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1179
|
-
const
|
|
1180
|
-
const
|
|
1181
|
-
const name = runFileName(runToken);
|
|
1188
|
+
const token = createStateStoreRunToken({ date: startedDate, randomBytes });
|
|
1189
|
+
const name = runFileName(token.runToken);
|
|
1182
1190
|
const path6 = join2(domainRunsDir.value, name);
|
|
1183
1191
|
try {
|
|
1184
1192
|
await fs7.writeFile(path6, EMPTY_STRING, { flag: EXCLUSIVE_CREATE_FLAG });
|
|
1185
1193
|
return {
|
|
1186
1194
|
ok: true,
|
|
1187
|
-
value: { runsDir: domainRunsDir.value, runFilePath: path6, runFileName: name,
|
|
1195
|
+
value: { runsDir: domainRunsDir.value, runFilePath: path6, runFileName: name, ...token }
|
|
1188
1196
|
};
|
|
1189
1197
|
} catch (error) {
|
|
1190
1198
|
if (hasErrorCode(error, ERROR_CODE_FILE_EXISTS)) continue;
|
|
@@ -2382,11 +2390,6 @@ function knipAdapter(scope, config) {
|
|
|
2382
2390
|
return scope.excluded.flatMap((entry) => [config.ignoreFlag, entry.path]);
|
|
2383
2391
|
}
|
|
2384
2392
|
|
|
2385
|
-
// src/lib/file-inclusion/adapters/madge.ts
|
|
2386
|
-
function madgeAdapter(scope, config) {
|
|
2387
|
-
return scope.excluded.flatMap((entry) => [config.ignoreFlag, entry.path]);
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
2393
|
// src/lib/file-inclusion/adapters/markdownlint.ts
|
|
2391
2394
|
function markdownlintAdapter(scope, config) {
|
|
2392
2395
|
return scope.excluded.flatMap((entry) => [config.ignoreFlag, entry.path]);
|
|
@@ -2411,7 +2414,6 @@ function vitestAdapter(scope, config) {
|
|
|
2411
2414
|
var TOOL_DEFAULT_FLAGS = {
|
|
2412
2415
|
eslint: "--ignore-pattern",
|
|
2413
2416
|
tsc: "--exclude",
|
|
2414
|
-
madge: "--exclude",
|
|
2415
2417
|
knip: "--exclude",
|
|
2416
2418
|
markdownlint: "--ignore",
|
|
2417
2419
|
pytest: "--ignore",
|
|
@@ -2420,7 +2422,6 @@ var TOOL_DEFAULT_FLAGS = {
|
|
|
2420
2422
|
var ADAPTER_MAP = {
|
|
2421
2423
|
eslint: eslintAdapter,
|
|
2422
2424
|
tsc: tscAdapter,
|
|
2423
|
-
madge: madgeAdapter,
|
|
2424
2425
|
knip: knipAdapter,
|
|
2425
2426
|
markdownlint: markdownlintAdapter,
|
|
2426
2427
|
pytest: pytestAdapter,
|
|
@@ -3499,169 +3500,800 @@ var configDomain = {
|
|
|
3499
3500
|
}
|
|
3500
3501
|
};
|
|
3501
3502
|
|
|
3502
|
-
// src/
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
var
|
|
3508
|
-
var
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
doing: ARCHIVABLE_DIR_KEYS.DOING
|
|
3515
|
-
};
|
|
3516
|
-
function buildArchivePaths(sessionId, currentStatus, config) {
|
|
3517
|
-
const filename = `${sessionId}${SESSION_FILE_EXTENSION}`;
|
|
3518
|
-
const dirKey = ARCHIVABLE_DIR_KEY[currentStatus];
|
|
3519
|
-
const sourceDir = config[dirKey];
|
|
3520
|
-
return {
|
|
3521
|
-
source: `${sourceDir}/${filename}`,
|
|
3522
|
-
target: `${config.archiveDir}/${filename}`
|
|
3523
|
-
};
|
|
3503
|
+
// src/lib/sanitize-cli-argument.ts
|
|
3504
|
+
var MAX_CLI_ARGUMENT_DISPLAY_LENGTH = 120;
|
|
3505
|
+
var ELLIPSIS_TOKEN = "...";
|
|
3506
|
+
var SENTINEL_UNDEFINED = "<undefined>";
|
|
3507
|
+
var SENTINEL_NULL = "<null>";
|
|
3508
|
+
var SENTINEL_EMPTY = "<empty>";
|
|
3509
|
+
var CONTROL_CHAR_UPPER_BOUND = 31;
|
|
3510
|
+
var DEL_CHAR_CODE = 127;
|
|
3511
|
+
var HEX_RADIX = 16;
|
|
3512
|
+
var HEX_PAD = 2;
|
|
3513
|
+
function formatHexEscape(code) {
|
|
3514
|
+
return `\\x${code.toString(HEX_RADIX).padStart(HEX_PAD, "0")}`;
|
|
3524
3515
|
}
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
if (existingPaths.archive !== null) {
|
|
3528
|
-
return null;
|
|
3529
|
-
}
|
|
3530
|
-
for (const status of ARCHIVE_SEARCH_ORDER) {
|
|
3531
|
-
if (existingPaths[status] !== null) {
|
|
3532
|
-
return { status, path: existingPaths[status] };
|
|
3533
|
-
}
|
|
3534
|
-
}
|
|
3535
|
-
return null;
|
|
3516
|
+
function nonStringSentinel(type) {
|
|
3517
|
+
return `<non-string:${type}>`;
|
|
3536
3518
|
}
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
try {
|
|
3555
|
-
const output2 = await handler(id);
|
|
3556
|
-
results.push({ id, ok: true, message: output2 });
|
|
3557
|
-
} catch (error) {
|
|
3558
|
-
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
3559
|
-
results.push({ id, ok: false, message });
|
|
3519
|
+
function sanitizeCliArgument(input) {
|
|
3520
|
+
if (input === void 0) return SENTINEL_UNDEFINED;
|
|
3521
|
+
if (input === null) return SENTINEL_NULL;
|
|
3522
|
+
if (typeof input !== "string") return nonStringSentinel(typeof input);
|
|
3523
|
+
if (input.length === 0) return SENTINEL_EMPTY;
|
|
3524
|
+
const escaped = escapeControlCharacters(input);
|
|
3525
|
+
return truncate(escaped);
|
|
3526
|
+
}
|
|
3527
|
+
function escapeControlCharacters(value) {
|
|
3528
|
+
let out = "";
|
|
3529
|
+
for (const char of value) {
|
|
3530
|
+
const code = char.codePointAt(0);
|
|
3531
|
+
if (code === void 0) continue;
|
|
3532
|
+
if (code <= CONTROL_CHAR_UPPER_BOUND || code === DEL_CHAR_CODE) {
|
|
3533
|
+
out += formatHexEscape(code);
|
|
3534
|
+
} else {
|
|
3535
|
+
out += char;
|
|
3560
3536
|
}
|
|
3561
3537
|
}
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
${output}`;
|
|
3569
|
-
throw err;
|
|
3570
|
-
}
|
|
3571
|
-
return output;
|
|
3538
|
+
return out;
|
|
3539
|
+
}
|
|
3540
|
+
function truncate(value) {
|
|
3541
|
+
if (value.length <= MAX_CLI_ARGUMENT_DISPLAY_LENGTH) return value;
|
|
3542
|
+
return value.slice(0, MAX_CLI_ARGUMENT_DISPLAY_LENGTH - ELLIPSIS_TOKEN.length) + ELLIPSIS_TOKEN;
|
|
3572
3543
|
}
|
|
3573
3544
|
|
|
3574
|
-
// src/
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3545
|
+
// src/interfaces/hooks/session-start.ts
|
|
3546
|
+
import { appendFile as nodeAppendFile2 } from "fs/promises";
|
|
3547
|
+
|
|
3548
|
+
// src/domains/worktree/occupancy-store.ts
|
|
3549
|
+
import { join as join6 } from "path";
|
|
3550
|
+
var OCCUPANCY_STATUS = {
|
|
3551
|
+
UNCLAIMED: "unclaimed",
|
|
3552
|
+
OCCUPIED: "occupied",
|
|
3553
|
+
STALE: "stale"
|
|
3583
3554
|
};
|
|
3584
|
-
var
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
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"
|
|
3555
|
+
var OCCUPANCY_CLAIM = {
|
|
3556
|
+
FILE_EXTENSION: ".claim",
|
|
3557
|
+
TEMP_EXTENSION: ".tmp",
|
|
3558
|
+
UNREADABLE_STARTED_AT_PREFIX: "unreadable:"
|
|
3591
3559
|
};
|
|
3592
|
-
var
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3560
|
+
var OCCUPANCY_ERROR = {
|
|
3561
|
+
INVALID_NAME: "worktree occupancy claim name must be a safe path segment",
|
|
3562
|
+
INVALID_WRITE_TOKEN: "worktree occupancy claim write token must be a safe path segment",
|
|
3563
|
+
CLAIM_WRITE_FAILED: "worktree occupancy claim write failed",
|
|
3564
|
+
CLAIM_READ_FAILED: "worktree occupancy claim read failed",
|
|
3565
|
+
CLAIM_REMOVE_FAILED: "worktree occupancy claim remove failed",
|
|
3566
|
+
CLAIM_MALFORMED: "worktree occupancy claim record is malformed"
|
|
3598
3567
|
};
|
|
3599
|
-
var
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this worktree \u2014 it is not the main checkout.`;
|
|
3603
|
-
function renderFactLine(label, value) {
|
|
3604
|
-
return `${CHECKLIST_INDENT}${label}: ${value ?? HANDOFF_BASE_UNRESOLVED}`;
|
|
3568
|
+
var ERROR_DETAIL_SEPARATOR2 = ": ";
|
|
3569
|
+
function claimFileName(name) {
|
|
3570
|
+
return `${name}${OCCUPANCY_CLAIM.FILE_EXTENSION}`;
|
|
3605
3571
|
}
|
|
3606
|
-
function
|
|
3607
|
-
const
|
|
3608
|
-
|
|
3609
|
-
return
|
|
3572
|
+
function claimFilePath(worktreesDir, name) {
|
|
3573
|
+
const validated = validateScopeToken(name);
|
|
3574
|
+
if (!validated.ok) return { ok: false, error: OCCUPANCY_ERROR.INVALID_NAME };
|
|
3575
|
+
return { ok: true, value: join6(worktreesDir, claimFileName(validated.value)) };
|
|
3610
3576
|
}
|
|
3611
|
-
function
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
renderFactLine(HANDOFF_BASE_FACT_LABEL.DEFAULT_TIP, checklist.defaultTipSha),
|
|
3616
|
-
renderFactLine(HANDOFF_BASE_FACT_LABEL.HEAD, checklist.headSha),
|
|
3617
|
-
renderFactLine(HANDOFF_BASE_FACT_LABEL.CURRENT_WORKTREE, checklist.currentWorktreePath),
|
|
3618
|
-
renderFactLine(HANDOFF_BASE_FACT_LABEL.MAIN_CHECKOUT, checklist.mainCheckoutPath),
|
|
3619
|
-
...checklist.prerequisites.map(renderPrerequisiteLine)
|
|
3620
|
-
].join("\n");
|
|
3577
|
+
function claimTempFilePath(claimPath, writeToken) {
|
|
3578
|
+
const validated = validateScopeToken(writeToken);
|
|
3579
|
+
if (!validated.ok) return { ok: false, error: OCCUPANCY_ERROR.INVALID_WRITE_TOKEN };
|
|
3580
|
+
return { ok: true, value: `${claimPath}.${validated.value}${OCCUPANCY_CLAIM.TEMP_EXTENSION}` };
|
|
3621
3581
|
}
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3582
|
+
function classifyOccupancy(claim, probe) {
|
|
3583
|
+
if (claim === void 0) return OCCUPANCY_STATUS.UNCLAIMED;
|
|
3584
|
+
if (claim.host !== probe.currentHost()) return OCCUPANCY_STATUS.STALE;
|
|
3585
|
+
if (!probe.isAlive(claim.pid)) return OCCUPANCY_STATUS.STALE;
|
|
3586
|
+
const liveStartTime = probe.startTimeOf(claim.pid);
|
|
3587
|
+
if (claim.startedAt === unreadableStartedAt(claim.pid)) return OCCUPANCY_STATUS.OCCUPIED;
|
|
3588
|
+
if (liveStartTime !== void 0 && liveStartTime !== claim.startedAt) return OCCUPANCY_STATUS.STALE;
|
|
3589
|
+
return OCCUPANCY_STATUS.OCCUPIED;
|
|
3590
|
+
}
|
|
3591
|
+
function unreadableStartedAt(pid) {
|
|
3592
|
+
return `${OCCUPANCY_CLAIM.UNREADABLE_STARTED_AT_PREFIX}${pid}`;
|
|
3593
|
+
}
|
|
3594
|
+
async function writeClaim(worktreesDir, name, record, options) {
|
|
3595
|
+
const pathResult = claimFilePath(worktreesDir, name);
|
|
3596
|
+
if (!pathResult.ok) return pathResult;
|
|
3597
|
+
const claimPath = pathResult.value;
|
|
3598
|
+
const tempPath = claimTempFilePath(claimPath, options.writeToken);
|
|
3599
|
+
if (!tempPath.ok) return tempPath;
|
|
3600
|
+
try {
|
|
3601
|
+
await options.fs.mkdir(worktreesDir, { recursive: true });
|
|
3602
|
+
await options.fs.writeFile(tempPath.value, serializeClaim(record));
|
|
3603
|
+
await options.fs.rename(tempPath.value, claimPath);
|
|
3604
|
+
return { ok: true, value: claimPath };
|
|
3605
|
+
} catch (error) {
|
|
3606
|
+
return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_WRITE_FAILED, toErrorMessage2(error)) };
|
|
3637
3607
|
}
|
|
3638
|
-
}
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3608
|
+
}
|
|
3609
|
+
async function readClaim(worktreesDir, name, options) {
|
|
3610
|
+
const pathResult = claimFilePath(worktreesDir, name);
|
|
3611
|
+
if (!pathResult.ok) return pathResult;
|
|
3612
|
+
let content;
|
|
3613
|
+
try {
|
|
3614
|
+
content = await options.fs.readFile(pathResult.value, "utf8");
|
|
3615
|
+
} catch (error) {
|
|
3616
|
+
if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return { ok: true, value: void 0 };
|
|
3617
|
+
return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_READ_FAILED, toErrorMessage2(error)) };
|
|
3646
3618
|
}
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3619
|
+
return parseClaim(content);
|
|
3620
|
+
}
|
|
3621
|
+
async function removeClaim(worktreesDir, name, options) {
|
|
3622
|
+
const pathResult = claimFilePath(worktreesDir, name);
|
|
3623
|
+
if (!pathResult.ok) return pathResult;
|
|
3624
|
+
try {
|
|
3625
|
+
await options.fs.rm(pathResult.value, { force: true });
|
|
3626
|
+
return { ok: true, value: void 0 };
|
|
3627
|
+
} catch (error) {
|
|
3628
|
+
return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_REMOVE_FAILED, toErrorMessage2(error)) };
|
|
3652
3629
|
}
|
|
3653
|
-
}
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3630
|
+
}
|
|
3631
|
+
async function readOccupancy(worktreesDir, name, probe, options) {
|
|
3632
|
+
const claimResult = await readClaim(worktreesDir, name, options);
|
|
3633
|
+
if (!claimResult.ok) return claimResult;
|
|
3634
|
+
return { ok: true, value: classifyOccupancy(claimResult.value, probe) };
|
|
3635
|
+
}
|
|
3636
|
+
function serializeClaim(record) {
|
|
3637
|
+
return JSON.stringify({
|
|
3638
|
+
sessionId: record.sessionId,
|
|
3639
|
+
host: record.host,
|
|
3640
|
+
pid: record.pid,
|
|
3641
|
+
startedAt: record.startedAt
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
function parseClaim(content) {
|
|
3645
|
+
let parsed;
|
|
3646
|
+
try {
|
|
3647
|
+
parsed = JSON.parse(content);
|
|
3648
|
+
} catch {
|
|
3649
|
+
return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
|
|
3658
3650
|
}
|
|
3659
|
-
};
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3651
|
+
if (!isWorktreeClaimRecord(parsed)) return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
|
|
3652
|
+
return { ok: true, value: parsed };
|
|
3653
|
+
}
|
|
3654
|
+
function isWorktreeClaimRecord(value) {
|
|
3655
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3656
|
+
const candidate = value;
|
|
3657
|
+
return typeof candidate.sessionId === "string" && typeof candidate.host === "string" && typeof candidate.pid === "number" && typeof candidate.startedAt === "string";
|
|
3658
|
+
}
|
|
3659
|
+
function formatOccupancyError(code, detail) {
|
|
3660
|
+
return `${code}${ERROR_DETAIL_SEPARATOR2}${detail}`;
|
|
3661
|
+
}
|
|
3662
|
+
function toErrorMessage2(error) {
|
|
3663
|
+
return error instanceof Error ? error.message : String(error);
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
// src/domains/worktree/controlling-process.ts
|
|
3667
|
+
var CONTROLLING_PID_ENV = "SPX_WORKTREE_CONTROLLING_PID";
|
|
3668
|
+
var AGENT_RUNTIME_NAMES = ["claude", "codex"];
|
|
3669
|
+
var AGENT_COMMAND_PATTERN = new RegExp(`\\b(?:${AGENT_RUNTIME_NAMES.join("|")})\\b`, "i");
|
|
3670
|
+
var CONTROLLING_PROCESS_ERROR = {
|
|
3671
|
+
UNRESOLVED: "worktree controlling process could not be resolved"
|
|
3672
|
+
};
|
|
3673
|
+
var MAX_ANCESTRY_DEPTH = 64;
|
|
3674
|
+
var MIN_VALID_PID = 1;
|
|
3675
|
+
var PID_RADIX = 10;
|
|
3676
|
+
function resolveControllingProcess(selfPid, table, env) {
|
|
3677
|
+
const host = table.currentHost();
|
|
3678
|
+
for (const pid of controllingPidCandidates(selfPid, table, env)) {
|
|
3679
|
+
const startedAt = table.startTimeOf(pid);
|
|
3680
|
+
if (startedAt !== void 0) return { ok: true, value: { pid, startedAt, host } };
|
|
3681
|
+
if (table.isAlive(pid)) return { ok: true, value: { pid, startedAt: unreadableStartedAt(pid), host } };
|
|
3682
|
+
}
|
|
3683
|
+
return { ok: false, error: CONTROLLING_PROCESS_ERROR.UNRESOLVED };
|
|
3684
|
+
}
|
|
3685
|
+
function* controllingPidCandidates(selfPid, table, env) {
|
|
3686
|
+
const override = parsePid(env[CONTROLLING_PID_ENV]);
|
|
3687
|
+
if (override !== void 0) yield override;
|
|
3688
|
+
const agent = findAgentAncestor(selfPid, table);
|
|
3689
|
+
if (agent !== void 0) yield agent;
|
|
3690
|
+
const parent = table.parentOf(selfPid);
|
|
3691
|
+
if (parent !== void 0 && isValidPid(parent)) yield parent;
|
|
3692
|
+
}
|
|
3693
|
+
function findAgentAncestor(selfPid, table) {
|
|
3694
|
+
let pid = table.parentOf(selfPid);
|
|
3695
|
+
for (let depth = 0; pid !== void 0 && isValidPid(pid) && depth < MAX_ANCESTRY_DEPTH; depth += 1) {
|
|
3696
|
+
const command = table.commandOf(pid);
|
|
3697
|
+
if (command !== void 0 && AGENT_COMMAND_PATTERN.test(command)) return pid;
|
|
3698
|
+
pid = table.parentOf(pid);
|
|
3699
|
+
}
|
|
3700
|
+
return void 0;
|
|
3701
|
+
}
|
|
3702
|
+
function parsePid(value) {
|
|
3703
|
+
if (value === void 0 || value.length === 0) return void 0;
|
|
3704
|
+
const parsed = Number.parseInt(value, PID_RADIX);
|
|
3705
|
+
return isValidPid(parsed) ? parsed : void 0;
|
|
3706
|
+
}
|
|
3707
|
+
function isValidPid(pid) {
|
|
3708
|
+
return Number.isInteger(pid) && pid >= MIN_VALID_PID;
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
// src/domains/worktree/resolve.ts
|
|
3712
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
3713
|
+
|
|
3714
|
+
// src/domains/worktree/worktree-name.ts
|
|
3715
|
+
import { basename as basename2 } from "path";
|
|
3716
|
+
var SEPARATOR_CHARACTER = /[^a-z0-9_]/;
|
|
3717
|
+
var NAME_SEPARATOR = "-";
|
|
3718
|
+
function worktreeClaimName(worktreeRoot) {
|
|
3719
|
+
return basename2(worktreeRoot).toLowerCase().split(SEPARATOR_CHARACTER).filter((segment) => segment.length > 0).join(NAME_SEPARATOR);
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
// src/domains/worktree/resolve.ts
|
|
3723
|
+
var WORKTREE_RESOLVE_ERROR = {
|
|
3724
|
+
NOT_A_WORKTREE: "path resolves to no worktree"
|
|
3725
|
+
};
|
|
3726
|
+
async function resolveWorktreesDir(options) {
|
|
3727
|
+
if (options.worktreesDir !== void 0) return options.worktreesDir;
|
|
3728
|
+
const resolved = await resolveWorktreesScopeDir({ cwd: options.cwd, deps: options.gitDeps });
|
|
3729
|
+
options.onWarning?.(resolved.warning);
|
|
3730
|
+
return resolved.worktreesDir;
|
|
3731
|
+
}
|
|
3732
|
+
async function resolveCurrentWorktreeName(options) {
|
|
3733
|
+
const worktree = await detectWorktreeProductRoot(options.cwd, options.gitDeps);
|
|
3734
|
+
return worktreeClaimName(worktree.productDir);
|
|
3735
|
+
}
|
|
3736
|
+
async function resolveTargetWorktree(options) {
|
|
3737
|
+
const base = options.cwd;
|
|
3738
|
+
const targetPath = options.worktree === void 0 ? base : resolve3(base, options.worktree);
|
|
3739
|
+
const targetGitPath = await options.pathInfo.isExistingNonDirectory(targetPath) ? dirname3(targetPath) : targetPath;
|
|
3740
|
+
const worktree = await detectWorktreeProductRoot(targetGitPath, options.gitDeps);
|
|
3741
|
+
if (!worktree.isGitRepo) {
|
|
3742
|
+
return { ok: false, error: `${WORKTREE_RESOLVE_ERROR.NOT_A_WORKTREE}: ${options.worktree ?? base}` };
|
|
3743
|
+
}
|
|
3744
|
+
return { ok: true, value: { name: worktreeClaimName(worktree.productDir), worktreeRoot: worktree.productDir } };
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
// src/domains/worktree/claim.ts
|
|
3748
|
+
async function claimWorktreeOccupancy(options) {
|
|
3749
|
+
const controlling = resolveControllingProcess(options.selfPid, options.processTable, options.env);
|
|
3750
|
+
if (!controlling.ok) return controlling;
|
|
3751
|
+
const worktreesDir = await resolveWorktreesDir(options);
|
|
3752
|
+
const name = await resolveCurrentWorktreeName(options);
|
|
3753
|
+
return writeClaim(
|
|
3754
|
+
worktreesDir,
|
|
3755
|
+
name,
|
|
3756
|
+
{
|
|
3757
|
+
sessionId: options.sessionId,
|
|
3758
|
+
host: controlling.value.host,
|
|
3759
|
+
pid: controlling.value.pid,
|
|
3760
|
+
startedAt: controlling.value.startedAt
|
|
3761
|
+
},
|
|
3762
|
+
{ fs: options.fs, writeToken: options.claimWriteToken }
|
|
3763
|
+
);
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
// src/domains/hooks/session-start.ts
|
|
3767
|
+
var HOOK_SESSION_START_PAYLOAD = {
|
|
3768
|
+
CWD: "cwd",
|
|
3769
|
+
SESSION_ID: "session_id"
|
|
3770
|
+
};
|
|
3771
|
+
var HOOK_SESSION_START_ENV = {
|
|
3772
|
+
CLAUDE_ENV_FILE: "CLAUDE_ENV_FILE",
|
|
3773
|
+
CLAUDE_PROJECT_DIR: "CLAUDE_PROJECT_DIR",
|
|
3774
|
+
CLAUDE_SESSION_ID: "CLAUDE_SESSION_ID",
|
|
3775
|
+
CLAUDE_WORKTREE_CLAIMED: "CLAUDE_WORKTREE_CLAIMED",
|
|
3776
|
+
CODEX_THREAD_ID: "CODEX_THREAD_ID",
|
|
3777
|
+
PROJECT_DIR: "PROJECT_DIR"
|
|
3778
|
+
};
|
|
3779
|
+
var HOOK_SESSION_START_CLAIMED = {
|
|
3780
|
+
FALSE: "0",
|
|
3781
|
+
TRUE: "1"
|
|
3782
|
+
};
|
|
3783
|
+
var HOOK_ENV_FILE = {
|
|
3784
|
+
ENCODING: "utf8",
|
|
3785
|
+
EXPORT_PREFIX: "export "
|
|
3786
|
+
};
|
|
3787
|
+
var HOOK_SESSION_START_ERROR = {
|
|
3788
|
+
ENV_FILE_WRITE_FAILED: "hook session-start env file write failed",
|
|
3789
|
+
PAYLOAD_MALFORMED: "hook session-start payload must be a JSON object"
|
|
3790
|
+
};
|
|
3791
|
+
var ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3792
|
+
var SAFE_SHELL_VALUE_PATTERN = /^[A-Za-z0-9_@%+=:,.-]+$/;
|
|
3793
|
+
var SINGLE_QUOTE = "'";
|
|
3794
|
+
var SHELL_SINGLE_QUOTE_ESCAPE = `'"'"'`;
|
|
3795
|
+
var ENV_FILE_HEADER = "# Managed by spx hook run session-start";
|
|
3796
|
+
var LINE_SEPARATOR = "\n";
|
|
3797
|
+
function parseHookSessionStartPayload(content) {
|
|
3798
|
+
if (content === void 0 || content.trim().length === 0) return { ok: true, value: {} };
|
|
3799
|
+
let parsed;
|
|
3800
|
+
try {
|
|
3801
|
+
parsed = JSON.parse(content);
|
|
3802
|
+
} catch {
|
|
3803
|
+
return { ok: false, error: HOOK_SESSION_START_ERROR.PAYLOAD_MALFORMED };
|
|
3804
|
+
}
|
|
3805
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
3806
|
+
return { ok: false, error: HOOK_SESSION_START_ERROR.PAYLOAD_MALFORMED };
|
|
3807
|
+
}
|
|
3808
|
+
const record = parsed;
|
|
3809
|
+
return {
|
|
3810
|
+
ok: true,
|
|
3811
|
+
value: {
|
|
3812
|
+
cwd: nonEmptyString(record[HOOK_SESSION_START_PAYLOAD.CWD]),
|
|
3813
|
+
sessionId: nonEmptyString(record[HOOK_SESSION_START_PAYLOAD.SESSION_ID])
|
|
3814
|
+
}
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
function resolveHookSessionStartSessionId(payload, env) {
|
|
3818
|
+
return payload.sessionId ?? nonEmptyString(env[HOOK_SESSION_START_ENV.CLAUDE_SESSION_ID]) ?? nonEmptyString(env[HOOK_SESSION_START_ENV.CODEX_THREAD_ID]);
|
|
3819
|
+
}
|
|
3820
|
+
function resolveHookSessionStartProductDir(payload, cwd) {
|
|
3821
|
+
return payload.cwd ?? cwd;
|
|
3822
|
+
}
|
|
3823
|
+
function resolveHookSessionStartEnvFile(env, explicitEnvFile) {
|
|
3824
|
+
return nonEmptyString(explicitEnvFile) ?? nonEmptyString(env[HOOK_SESSION_START_ENV.CLAUDE_ENV_FILE]);
|
|
3825
|
+
}
|
|
3826
|
+
function renderHookSessionStartEnvFile(input) {
|
|
3827
|
+
return [
|
|
3828
|
+
"",
|
|
3829
|
+
ENV_FILE_HEADER,
|
|
3830
|
+
...input.sessionId === void 0 ? [] : [renderExportLine(HOOK_SESSION_START_ENV.CLAUDE_SESSION_ID, input.sessionId)],
|
|
3831
|
+
renderExportLine(HOOK_SESSION_START_ENV.CLAUDE_PROJECT_DIR, input.productDir),
|
|
3832
|
+
renderExportLine(HOOK_SESSION_START_ENV.PROJECT_DIR, input.productDir),
|
|
3833
|
+
renderExportLine(
|
|
3834
|
+
HOOK_SESSION_START_ENV.CLAUDE_WORKTREE_CLAIMED,
|
|
3835
|
+
input.claimed ? HOOK_SESSION_START_CLAIMED.TRUE : HOOK_SESSION_START_CLAIMED.FALSE
|
|
3836
|
+
),
|
|
3837
|
+
""
|
|
3838
|
+
].join(LINE_SEPARATOR);
|
|
3839
|
+
}
|
|
3840
|
+
function renderExportLine(name, value) {
|
|
3841
|
+
if (!ENV_NAME_PATTERN.test(name)) throw new Error(`invalid environment variable name: ${name}`);
|
|
3842
|
+
return `${HOOK_ENV_FILE.EXPORT_PREFIX}${name}=${shellQuote(value)}`;
|
|
3843
|
+
}
|
|
3844
|
+
function shellQuote(value) {
|
|
3845
|
+
if (SAFE_SHELL_VALUE_PATTERN.test(value)) return value;
|
|
3846
|
+
return `${SINGLE_QUOTE}${value.replaceAll(SINGLE_QUOTE, SHELL_SINGLE_QUOTE_ESCAPE)}${SINGLE_QUOTE}`;
|
|
3847
|
+
}
|
|
3848
|
+
function nonEmptyString(value) {
|
|
3849
|
+
if (typeof value !== "string") return void 0;
|
|
3850
|
+
const trimmed = value.trim();
|
|
3851
|
+
return trimmed.length === 0 ? void 0 : trimmed;
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
// src/interfaces/hooks/session-start.ts
|
|
3855
|
+
var defaultHookEnvFileSystem = {
|
|
3856
|
+
appendFile: async (path6, data, encoding) => {
|
|
3857
|
+
await nodeAppendFile2(path6, data, encoding);
|
|
3858
|
+
}
|
|
3859
|
+
};
|
|
3860
|
+
var ERROR_DETAIL_SEPARATOR3 = ": ";
|
|
3861
|
+
var EMPTY_HOOK_STDOUT = "";
|
|
3862
|
+
async function runSessionStartHook(options) {
|
|
3863
|
+
const diagnostics = [];
|
|
3864
|
+
const payloadResult = parseHookSessionStartPayload(options.content);
|
|
3865
|
+
if (!payloadResult.ok) {
|
|
3866
|
+
diagnostics.push(payloadResult.error);
|
|
3867
|
+
}
|
|
3868
|
+
const payload = payloadResult.ok ? payloadResult.value : {};
|
|
3869
|
+
const productDir = resolveHookSessionStartProductDir(payload, options.cwd);
|
|
3870
|
+
const sessionId = resolveHookSessionStartSessionId(payload, options.env);
|
|
3871
|
+
let claimed = false;
|
|
3872
|
+
if (sessionId !== void 0) {
|
|
3873
|
+
const claim = await claimWorktreeOccupancy({
|
|
3874
|
+
...options,
|
|
3875
|
+
cwd: productDir,
|
|
3876
|
+
sessionId
|
|
3877
|
+
});
|
|
3878
|
+
if (claim.ok) {
|
|
3879
|
+
claimed = true;
|
|
3880
|
+
} else {
|
|
3881
|
+
diagnostics.push(claim.error);
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
const envFile = resolveHookSessionStartEnvFile(options.env, options.envFile);
|
|
3885
|
+
const envFileWritten = await writeEnvFileIfConfigured({
|
|
3886
|
+
claimed,
|
|
3887
|
+
envFile,
|
|
3888
|
+
productDir,
|
|
3889
|
+
sessionId,
|
|
3890
|
+
envFileSystem: options.envFileSystem ?? defaultHookEnvFileSystem,
|
|
3891
|
+
diagnostics
|
|
3892
|
+
});
|
|
3893
|
+
return {
|
|
3894
|
+
ok: true,
|
|
3895
|
+
value: {
|
|
3896
|
+
claimed,
|
|
3897
|
+
diagnostics,
|
|
3898
|
+
envFileWritten,
|
|
3899
|
+
productDir,
|
|
3900
|
+
stdout: EMPTY_HOOK_STDOUT,
|
|
3901
|
+
...sessionId === void 0 ? {} : { sessionId }
|
|
3902
|
+
}
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
async function writeEnvFileIfConfigured(options) {
|
|
3906
|
+
if (options.envFile === void 0) return false;
|
|
3907
|
+
try {
|
|
3908
|
+
await options.envFileSystem.appendFile(
|
|
3909
|
+
options.envFile,
|
|
3910
|
+
renderHookSessionStartEnvFile({
|
|
3911
|
+
claimed: options.claimed,
|
|
3912
|
+
productDir: options.productDir,
|
|
3913
|
+
sessionId: options.sessionId
|
|
3914
|
+
}),
|
|
3915
|
+
HOOK_ENV_FILE.ENCODING
|
|
3916
|
+
);
|
|
3917
|
+
return true;
|
|
3918
|
+
} catch (error) {
|
|
3919
|
+
options.diagnostics.push(
|
|
3920
|
+
`${HOOK_SESSION_START_ERROR.ENV_FILE_WRITE_FAILED}${ERROR_DETAIL_SEPARATOR3}${error instanceof Error ? error.message : String(error)}`
|
|
3921
|
+
);
|
|
3922
|
+
return false;
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// src/interfaces/hooks/registry.ts
|
|
3927
|
+
var HOOK_EVENT = {
|
|
3928
|
+
SESSION_START: "session-start"
|
|
3929
|
+
};
|
|
3930
|
+
var HOOK_ERROR = {
|
|
3931
|
+
UNKNOWN_EVENT: "unknown hook event"
|
|
3932
|
+
};
|
|
3933
|
+
function isHookEvent(event) {
|
|
3934
|
+
return Object.values(HOOK_EVENT).includes(event);
|
|
3935
|
+
}
|
|
3936
|
+
async function runHookEvent(options) {
|
|
3937
|
+
if (options.event === HOOK_EVENT.SESSION_START) return runSessionStartHook(options);
|
|
3938
|
+
return { ok: false, error: `${HOOK_ERROR.UNKNOWN_EVENT}: ${options.event}` };
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
// src/interfaces/hooks/cli-runner.ts
|
|
3942
|
+
var HOOK_PROCESS_IO_EVENT = {
|
|
3943
|
+
DATA: "data",
|
|
3944
|
+
END: "end",
|
|
3945
|
+
ERROR: "error"
|
|
3946
|
+
};
|
|
3947
|
+
var LINE_SEPARATOR2 = "\n";
|
|
3948
|
+
var ERROR_DETAIL_SEPARATOR4 = ": ";
|
|
3949
|
+
var STDIN_READ_ERROR = "hook stdin read failed";
|
|
3950
|
+
async function runHookCli(options) {
|
|
3951
|
+
if (!isHookEvent(options.event)) {
|
|
3952
|
+
options.io.writeStderr(`${HOOK_ERROR.UNKNOWN_EVENT}: ${sanitizeCliArgument(options.event)}`);
|
|
3953
|
+
return { ok: false, error: HOOK_ERROR.UNKNOWN_EVENT };
|
|
3954
|
+
}
|
|
3955
|
+
const diagnostics = [];
|
|
3956
|
+
const stdin = await options.io.readStdin();
|
|
3957
|
+
const content = stdin.ok ? stdin.value : void 0;
|
|
3958
|
+
if (!stdin.ok) diagnostics.push(stdin.error);
|
|
3959
|
+
const runEvent = options.runEvent ?? runHookEvent;
|
|
3960
|
+
const result = await runEvent({
|
|
3961
|
+
claimWriteToken: options.claimWriteToken,
|
|
3962
|
+
content,
|
|
3963
|
+
cwd: options.cwd,
|
|
3964
|
+
env: options.env,
|
|
3965
|
+
envFile: options.envFile,
|
|
3966
|
+
event: options.event,
|
|
3967
|
+
fs: options.fs,
|
|
3968
|
+
gitDeps: options.gitDeps,
|
|
3969
|
+
onWarning: options.onWarning,
|
|
3970
|
+
processTable: options.processTable,
|
|
3971
|
+
selfPid: options.selfPid,
|
|
3972
|
+
worktreesDir: options.worktreesDir
|
|
3973
|
+
});
|
|
3974
|
+
for (const diagnostic of diagnostics) {
|
|
3975
|
+
options.io.writeStderr(diagnostic);
|
|
3976
|
+
}
|
|
3977
|
+
if (!result.ok) {
|
|
3978
|
+
options.io.writeStderr(result.error);
|
|
3979
|
+
return result;
|
|
3980
|
+
}
|
|
3981
|
+
for (const diagnostic of result.value.diagnostics) {
|
|
3982
|
+
options.io.writeStderr(diagnostic);
|
|
3983
|
+
}
|
|
3984
|
+
if (result.value.stdout.length > 0) {
|
|
3985
|
+
options.io.writeStdout(result.value.stdout);
|
|
3986
|
+
}
|
|
3987
|
+
return { ok: true, value: void 0 };
|
|
3988
|
+
}
|
|
3989
|
+
function createProcessHookIo(streams) {
|
|
3990
|
+
return {
|
|
3991
|
+
readStdin: async () => {
|
|
3992
|
+
if (streams.stdin.isTTY) return { ok: true, value: void 0 };
|
|
3993
|
+
return new Promise((resolve6) => {
|
|
3994
|
+
let data = "";
|
|
3995
|
+
streams.stdin.setEncoding("utf-8");
|
|
3996
|
+
streams.stdin.on(HOOK_PROCESS_IO_EVENT.DATA, (chunk) => {
|
|
3997
|
+
data += chunk;
|
|
3998
|
+
});
|
|
3999
|
+
streams.stdin.on(HOOK_PROCESS_IO_EVENT.END, () => {
|
|
4000
|
+
resolve6({ ok: true, value: data.length === 0 ? void 0 : data });
|
|
4001
|
+
});
|
|
4002
|
+
streams.stdin.on(HOOK_PROCESS_IO_EVENT.ERROR, (error) => {
|
|
4003
|
+
resolve6({ ok: false, error: formatStdinReadError(error) });
|
|
4004
|
+
});
|
|
4005
|
+
});
|
|
4006
|
+
},
|
|
4007
|
+
writeStdout: (content) => {
|
|
4008
|
+
streams.stdout.write(`${content}${LINE_SEPARATOR2}`);
|
|
4009
|
+
},
|
|
4010
|
+
writeStderr: (content) => {
|
|
4011
|
+
streams.stderr.write(`${content}${LINE_SEPARATOR2}`);
|
|
4012
|
+
}
|
|
4013
|
+
};
|
|
4014
|
+
}
|
|
4015
|
+
function formatStdinReadError(error) {
|
|
4016
|
+
return `${STDIN_READ_ERROR}${ERROR_DETAIL_SEPARATOR4}${error instanceof Error ? error.message : String(error)}`;
|
|
4017
|
+
}
|
|
4018
|
+
var processHookIo = createProcessHookIo({
|
|
4019
|
+
stdin: process.stdin,
|
|
4020
|
+
stdout: process.stdout,
|
|
4021
|
+
stderr: process.stderr
|
|
4022
|
+
});
|
|
4023
|
+
|
|
4024
|
+
// src/lib/worktree-claim-write-token.ts
|
|
4025
|
+
import { randomBytes as nodeRandomBytes2 } from "crypto";
|
|
4026
|
+
var CLAIM_WRITE_TOKEN_BYTES = 8;
|
|
4027
|
+
function createClaimWriteToken(randomBytes = nodeRandomBytes2) {
|
|
4028
|
+
return randomBytes(CLAIM_WRITE_TOKEN_BYTES).toString("hex");
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
// src/lib/worktree-occupancy-file-system.ts
|
|
4032
|
+
import {
|
|
4033
|
+
mkdir as nodeMkdir2,
|
|
4034
|
+
readFile as nodeReadFile2,
|
|
4035
|
+
rename as nodeRename,
|
|
4036
|
+
rm as nodeRm,
|
|
4037
|
+
writeFile as nodeWriteFile2
|
|
4038
|
+
} from "fs/promises";
|
|
4039
|
+
var defaultOccupancyFileSystem = {
|
|
4040
|
+
mkdir: async (path6, options) => {
|
|
4041
|
+
await nodeMkdir2(path6, options);
|
|
4042
|
+
},
|
|
4043
|
+
writeFile: async (path6, data) => {
|
|
4044
|
+
await nodeWriteFile2(path6, data);
|
|
4045
|
+
},
|
|
4046
|
+
rename: nodeRename,
|
|
4047
|
+
readFile: nodeReadFile2,
|
|
4048
|
+
rm: async (path6, options) => {
|
|
4049
|
+
await nodeRm(path6, options);
|
|
4050
|
+
}
|
|
4051
|
+
};
|
|
4052
|
+
|
|
4053
|
+
// src/lib/worktree-process-table.ts
|
|
4054
|
+
import { spawnSync } from "child_process";
|
|
4055
|
+
import { hostname } from "os";
|
|
4056
|
+
var PS_COMMAND = "/bin/ps";
|
|
4057
|
+
var PS_FIELD = {
|
|
4058
|
+
START_TIME: "lstart",
|
|
4059
|
+
PARENT_PID: "ppid",
|
|
4060
|
+
// The full command line, not `comm`: interpreted agent runtimes otherwise
|
|
4061
|
+
// report only the interpreter basename and miss the agent-command match.
|
|
4062
|
+
COMMAND: "args"
|
|
4063
|
+
};
|
|
4064
|
+
var SIGNAL_LIVENESS_PROBE = 0;
|
|
4065
|
+
var PROCESS_EXISTS_NO_PERMISSION = "EPERM";
|
|
4066
|
+
var PID_RADIX2 = 10;
|
|
4067
|
+
var STABLE_PS_ENV = { ...process.env, TZ: "UTC", LC_ALL: "C" };
|
|
4068
|
+
function psField(pid, field) {
|
|
4069
|
+
const result = spawnSync(PS_COMMAND, ["-o", `${field}=`, "-p", String(pid)], {
|
|
4070
|
+
encoding: "utf8",
|
|
4071
|
+
env: STABLE_PS_ENV
|
|
4072
|
+
});
|
|
4073
|
+
if (result.status !== 0 || typeof result.stdout !== "string") return void 0;
|
|
4074
|
+
const value = result.stdout.trim();
|
|
4075
|
+
return value.length > 0 ? value : void 0;
|
|
4076
|
+
}
|
|
4077
|
+
var defaultProcessTable = {
|
|
4078
|
+
currentHost: () => hostname(),
|
|
4079
|
+
isAlive: (pid) => {
|
|
4080
|
+
try {
|
|
4081
|
+
process.kill(pid, SIGNAL_LIVENESS_PROBE);
|
|
4082
|
+
return true;
|
|
4083
|
+
} catch (error) {
|
|
4084
|
+
return hasErrorCode(error, PROCESS_EXISTS_NO_PERMISSION);
|
|
4085
|
+
}
|
|
4086
|
+
},
|
|
4087
|
+
startTimeOf: (pid) => psField(pid, PS_FIELD.START_TIME),
|
|
4088
|
+
parentOf: (pid) => {
|
|
4089
|
+
const value = psField(pid, PS_FIELD.PARENT_PID);
|
|
4090
|
+
if (value === void 0) return void 0;
|
|
4091
|
+
const parsed = Number.parseInt(value, PID_RADIX2);
|
|
4092
|
+
return Number.isInteger(parsed) ? parsed : void 0;
|
|
4093
|
+
},
|
|
4094
|
+
commandOf: (pid) => psField(pid, PS_FIELD.COMMAND)
|
|
4095
|
+
};
|
|
4096
|
+
|
|
4097
|
+
// src/interfaces/cli/hook.ts
|
|
4098
|
+
var HOOK_CLI = {
|
|
4099
|
+
COMMAND: "hook",
|
|
4100
|
+
RUN: "run",
|
|
4101
|
+
EVENT_ARGUMENT: "<event>",
|
|
4102
|
+
ENV_FILE_FLAG: "--hook-env-file",
|
|
4103
|
+
WORKTREES_DIR_FLAG: "--worktrees-dir"
|
|
4104
|
+
};
|
|
4105
|
+
var HOOK_DOMAIN_DESCRIPTION = "Run host lifecycle hook events";
|
|
4106
|
+
function registerHookCommands(hookCmd) {
|
|
4107
|
+
hookCmd.command(`${HOOK_CLI.RUN} ${HOOK_CLI.EVENT_ARGUMENT}`).description("Run a hook lifecycle event").option(`${HOOK_CLI.ENV_FILE_FLAG} <path>`, "Hook env file to append; defaults to $CLAUDE_ENV_FILE").option(`${HOOK_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (event, options) => {
|
|
4108
|
+
const result = await runHookCli({
|
|
4109
|
+
claimWriteToken: createClaimWriteToken(),
|
|
4110
|
+
cwd: process.cwd(),
|
|
4111
|
+
env: process.env,
|
|
4112
|
+
envFile: options.hookEnvFile,
|
|
4113
|
+
event,
|
|
4114
|
+
fs: defaultOccupancyFileSystem,
|
|
4115
|
+
gitDeps: defaultGitDependencies,
|
|
4116
|
+
io: processHookIo,
|
|
4117
|
+
onWarning: writeWarning,
|
|
4118
|
+
processTable: defaultProcessTable,
|
|
4119
|
+
selfPid: process.pid,
|
|
4120
|
+
worktreesDir: options.worktreesDir
|
|
4121
|
+
});
|
|
4122
|
+
if (!result.ok) process.exit(1);
|
|
4123
|
+
});
|
|
4124
|
+
}
|
|
4125
|
+
var hookDomain = {
|
|
4126
|
+
name: HOOK_CLI.COMMAND,
|
|
4127
|
+
description: HOOK_DOMAIN_DESCRIPTION,
|
|
4128
|
+
register: (program2) => {
|
|
4129
|
+
const hookCmd = program2.command(HOOK_CLI.COMMAND).description(HOOK_DOMAIN_DESCRIPTION);
|
|
4130
|
+
registerHookCommands(hookCmd);
|
|
4131
|
+
}
|
|
4132
|
+
};
|
|
4133
|
+
|
|
4134
|
+
// src/commands/session/archive.ts
|
|
4135
|
+
import { mkdir, rename, stat } from "fs/promises";
|
|
4136
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
4137
|
+
|
|
4138
|
+
// src/domains/session/archive.ts
|
|
4139
|
+
var SESSION_FILE_EXTENSION = ".md";
|
|
4140
|
+
var ARCHIVABLE_DIR_KEYS = {
|
|
4141
|
+
TODO: "todoDir",
|
|
4142
|
+
DOING: "doingDir"
|
|
4143
|
+
};
|
|
4144
|
+
var ARCHIVABLE_DIR_KEY = {
|
|
4145
|
+
todo: ARCHIVABLE_DIR_KEYS.TODO,
|
|
4146
|
+
doing: ARCHIVABLE_DIR_KEYS.DOING
|
|
4147
|
+
};
|
|
4148
|
+
function buildArchivePaths(sessionId, currentStatus, config) {
|
|
4149
|
+
const filename = `${sessionId}${SESSION_FILE_EXTENSION}`;
|
|
4150
|
+
const dirKey = ARCHIVABLE_DIR_KEY[currentStatus];
|
|
4151
|
+
const sourceDir = config[dirKey];
|
|
4152
|
+
return {
|
|
4153
|
+
source: `${sourceDir}/${filename}`,
|
|
4154
|
+
target: `${config.archiveDir}/${filename}`
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
var ARCHIVE_SEARCH_ORDER = ["todo", "doing"];
|
|
4158
|
+
function findSessionForArchive(existingPaths) {
|
|
4159
|
+
if (existingPaths.archive !== null) {
|
|
4160
|
+
return null;
|
|
4161
|
+
}
|
|
4162
|
+
for (const status of ARCHIVE_SEARCH_ORDER) {
|
|
4163
|
+
if (existingPaths[status] !== null) {
|
|
4164
|
+
return { status, path: existingPaths[status] };
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
return null;
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
// src/domains/session/batch.ts
|
|
4171
|
+
var BatchError = class extends Error {
|
|
4172
|
+
results;
|
|
4173
|
+
constructor(results) {
|
|
4174
|
+
const failures = results.filter((r) => !r.ok);
|
|
4175
|
+
const successes = results.filter((r) => r.ok);
|
|
4176
|
+
super(
|
|
4177
|
+
`${failures.length} of ${results.length} operations failed. ${successes.length} succeeded.`
|
|
4178
|
+
);
|
|
4179
|
+
this.name = "BatchError";
|
|
4180
|
+
this.results = results;
|
|
4181
|
+
}
|
|
4182
|
+
};
|
|
4183
|
+
async function processBatch(ids, handler) {
|
|
4184
|
+
const results = [];
|
|
4185
|
+
for (const id of ids) {
|
|
4186
|
+
try {
|
|
4187
|
+
const output2 = await handler(id);
|
|
4188
|
+
results.push({ id, ok: true, message: output2 });
|
|
4189
|
+
} catch (error) {
|
|
4190
|
+
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
4191
|
+
results.push({ id, ok: false, message });
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
const output = results.map((r) => r.ok ? r.message : `Error (${r.id}): ${r.message}`).join("\n\n");
|
|
4195
|
+
const hasFailures = results.some((r) => !r.ok);
|
|
4196
|
+
if (hasFailures) {
|
|
4197
|
+
const err = new BatchError(results);
|
|
4198
|
+
err.message = `${err.message}
|
|
4199
|
+
|
|
4200
|
+
${output}`;
|
|
4201
|
+
throw err;
|
|
4202
|
+
}
|
|
4203
|
+
return output;
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
// src/domains/session/handoff-base-checklist.ts
|
|
4207
|
+
var SESSION_HANDOFF_BASE_ERROR_NAME = "SessionHandoffBaseError";
|
|
4208
|
+
var HANDOFF_BASE_MARK = {
|
|
4209
|
+
MET: "\u2713",
|
|
4210
|
+
UNMET: "\u2717"
|
|
4211
|
+
};
|
|
4212
|
+
var HANDOFF_BASE_PREREQUISITE_LABEL = {
|
|
4213
|
+
CLEAN_WORKING_TREE: "working tree is clean",
|
|
4214
|
+
DETACHED_AT_DEFAULT_TIP: "HEAD is detached at the default-branch tip"
|
|
4215
|
+
};
|
|
4216
|
+
var HANDOFF_BASE_REMEDY = {
|
|
4217
|
+
/** Unclean working tree: commit, or hand off from the main checkout. */
|
|
4218
|
+
COMMIT_OR_MAIN_CHECKOUT: "commit the changes, or run handoff from the main checkout",
|
|
4219
|
+
/** Off the default-branch tip: detach to it, or hand off from the main checkout. */
|
|
4220
|
+
DETACH_TO_TIP_OR_MAIN_CHECKOUT: "detach HEAD to the default-branch tip, or run handoff from the main checkout",
|
|
4221
|
+
/** Default branch unresolved: only the main checkout can anchor the base. */
|
|
4222
|
+
MAIN_CHECKOUT_ONLY: "run handoff from the main checkout"
|
|
4223
|
+
};
|
|
4224
|
+
var HANDOFF_BASE_FACT_LABEL = {
|
|
4225
|
+
DEFAULT_BRANCH: "default branch",
|
|
4226
|
+
DEFAULT_TIP: "origin tip",
|
|
4227
|
+
HEAD: "HEAD",
|
|
4228
|
+
CURRENT_WORKTREE: "current worktree",
|
|
4229
|
+
MAIN_CHECKOUT: "main checkout"
|
|
4230
|
+
};
|
|
4231
|
+
var HANDOFF_BASE_UNRESOLVED = "unresolved";
|
|
4232
|
+
var CHECKLIST_INDENT = " ";
|
|
4233
|
+
var REMEDY_SEPARATOR = " \u2014 ";
|
|
4234
|
+
var CHECKLIST_HEADER = `${SESSION_HANDOFF_BASE_ERROR_NAME}: cannot create a handoff session from this worktree \u2014 it is not the main checkout.`;
|
|
4235
|
+
function renderFactLine(label, value) {
|
|
4236
|
+
return `${CHECKLIST_INDENT}${label}: ${value ?? HANDOFF_BASE_UNRESOLVED}`;
|
|
4237
|
+
}
|
|
4238
|
+
function renderPrerequisiteLine(prerequisite) {
|
|
4239
|
+
const mark = prerequisite.met ? HANDOFF_BASE_MARK.MET : HANDOFF_BASE_MARK.UNMET;
|
|
4240
|
+
const base = `${CHECKLIST_INDENT}${mark} ${prerequisite.label}`;
|
|
4241
|
+
return prerequisite.met ? base : `${base}${REMEDY_SEPARATOR}${prerequisite.remedy}`;
|
|
4242
|
+
}
|
|
4243
|
+
function renderHandoffBaseChecklist(checklist) {
|
|
4244
|
+
return [
|
|
4245
|
+
CHECKLIST_HEADER,
|
|
4246
|
+
renderFactLine(HANDOFF_BASE_FACT_LABEL.DEFAULT_BRANCH, checklist.defaultBranch),
|
|
4247
|
+
renderFactLine(HANDOFF_BASE_FACT_LABEL.DEFAULT_TIP, checklist.defaultTipSha),
|
|
4248
|
+
renderFactLine(HANDOFF_BASE_FACT_LABEL.HEAD, checklist.headSha),
|
|
4249
|
+
renderFactLine(HANDOFF_BASE_FACT_LABEL.CURRENT_WORKTREE, checklist.currentWorktreePath),
|
|
4250
|
+
renderFactLine(HANDOFF_BASE_FACT_LABEL.MAIN_CHECKOUT, checklist.mainCheckoutPath),
|
|
4251
|
+
...checklist.prerequisites.map(renderPrerequisiteLine)
|
|
4252
|
+
].join("\n");
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
// src/domains/session/errors.ts
|
|
4256
|
+
var SessionError = class extends Error {
|
|
4257
|
+
constructor(message) {
|
|
4258
|
+
super(message);
|
|
4259
|
+
this.name = "SessionError";
|
|
4260
|
+
}
|
|
4261
|
+
};
|
|
4262
|
+
var SessionNotFoundError = class extends SessionError {
|
|
4263
|
+
/** The session ID that was not found */
|
|
4264
|
+
sessionId;
|
|
4265
|
+
constructor(sessionId) {
|
|
4266
|
+
super(`Session not found: ${sessionId}. Check the session ID and try again.`);
|
|
4267
|
+
this.name = "SessionNotFoundError";
|
|
4268
|
+
this.sessionId = sessionId;
|
|
4269
|
+
}
|
|
4270
|
+
};
|
|
4271
|
+
var SessionNotAvailableError = class extends SessionError {
|
|
4272
|
+
/** The session ID that is not available */
|
|
4273
|
+
sessionId;
|
|
4274
|
+
constructor(sessionId) {
|
|
4275
|
+
super(`Session not available: ${sessionId}. It may have been claimed by another agent.`);
|
|
4276
|
+
this.name = "SessionNotAvailableError";
|
|
4277
|
+
this.sessionId = sessionId;
|
|
4278
|
+
}
|
|
4279
|
+
};
|
|
4280
|
+
var SessionInvalidContentError = class extends SessionError {
|
|
4281
|
+
constructor(reason) {
|
|
4282
|
+
super(`Invalid session content: ${reason}`);
|
|
4283
|
+
this.name = "SessionInvalidContentError";
|
|
4284
|
+
}
|
|
4285
|
+
};
|
|
4286
|
+
var SessionInvalidGoalError = class extends SessionError {
|
|
4287
|
+
constructor() {
|
|
4288
|
+
super("Invalid session goal: goal must be a non-empty string.");
|
|
4289
|
+
this.name = "SessionInvalidGoalError";
|
|
4290
|
+
}
|
|
4291
|
+
};
|
|
4292
|
+
var SessionInvalidFieldError = class extends SessionError {
|
|
4293
|
+
/** The unknown field token the caller supplied. */
|
|
4294
|
+
field;
|
|
4295
|
+
/** The full set of valid field names. */
|
|
4296
|
+
validFields;
|
|
3665
4297
|
constructor(field, validFields) {
|
|
3666
4298
|
super(`Invalid field: "${field}". Valid fields: ${validFields.join(", ")}`);
|
|
3667
4299
|
this.name = "SessionInvalidFieldError";
|
|
@@ -3733,7 +4365,7 @@ var SessionInvalidJsonHeaderError = class extends SessionError {
|
|
|
3733
4365
|
};
|
|
3734
4366
|
|
|
3735
4367
|
// src/commands/session/resolve-config.ts
|
|
3736
|
-
import { join as
|
|
4368
|
+
import { join as join7 } from "path";
|
|
3737
4369
|
|
|
3738
4370
|
// src/config/defaults.ts
|
|
3739
4371
|
var DEFAULT_CONFIG = {
|
|
@@ -3753,18 +4385,18 @@ async function resolveSessionConfig(options = {}) {
|
|
|
3753
4385
|
if (sessionsDir) {
|
|
3754
4386
|
return {
|
|
3755
4387
|
config: {
|
|
3756
|
-
todoDir:
|
|
3757
|
-
doingDir:
|
|
3758
|
-
archiveDir:
|
|
4388
|
+
todoDir: join7(sessionsDir, statusDirs2.todo),
|
|
4389
|
+
doingDir: join7(sessionsDir, statusDirs2.doing),
|
|
4390
|
+
archiveDir: join7(sessionsDir, statusDirs2.archive)
|
|
3759
4391
|
}
|
|
3760
4392
|
};
|
|
3761
4393
|
}
|
|
3762
4394
|
const { sessionsDir: baseDir, warning } = await resolveSessionsScopeDir({ cwd, deps });
|
|
3763
4395
|
return {
|
|
3764
4396
|
config: {
|
|
3765
|
-
todoDir:
|
|
3766
|
-
doingDir:
|
|
3767
|
-
archiveDir:
|
|
4397
|
+
todoDir: join7(baseDir, statusDirs2.todo),
|
|
4398
|
+
doingDir: join7(baseDir, statusDirs2.doing),
|
|
4399
|
+
archiveDir: join7(baseDir, statusDirs2.archive)
|
|
3768
4400
|
},
|
|
3769
4401
|
warning
|
|
3770
4402
|
};
|
|
@@ -3801,9 +4433,9 @@ async function fileExists(path6) {
|
|
|
3801
4433
|
}
|
|
3802
4434
|
async function probeSessionPaths(sessionId, config) {
|
|
3803
4435
|
const filename = `${sessionId}${SESSION_FILE_EXTENSION}`;
|
|
3804
|
-
const todoPath =
|
|
3805
|
-
const doingPath =
|
|
3806
|
-
const archivePath =
|
|
4436
|
+
const todoPath = join8(config.todoDir, filename);
|
|
4437
|
+
const doingPath = join8(config.doingDir, filename);
|
|
4438
|
+
const archivePath = join8(config.archiveDir, filename);
|
|
3807
4439
|
return {
|
|
3808
4440
|
todo: await fileExists(todoPath) ? todoPath : null,
|
|
3809
4441
|
doing: await fileExists(doingPath) ? doingPath : null,
|
|
@@ -3824,7 +4456,7 @@ async function resolveArchivePaths(sessionId, config) {
|
|
|
3824
4456
|
}
|
|
3825
4457
|
async function archiveSingle(sessionId, config) {
|
|
3826
4458
|
const { source, target } = await resolveArchivePaths(sessionId, config);
|
|
3827
|
-
await mkdir(
|
|
4459
|
+
await mkdir(dirname4(target), { recursive: true });
|
|
3828
4460
|
await rename(source, target);
|
|
3829
4461
|
return `${SESSION_ARCHIVE_OUTPUT.ARCHIVED}: ${sessionId}
|
|
3830
4462
|
${SESSION_ARCHIVE_OUTPUT.ARCHIVE_LOCATION}: ${target}`;
|
|
@@ -3847,7 +4479,7 @@ function resolveDeletePath(sessionId, existingPaths) {
|
|
|
3847
4479
|
}
|
|
3848
4480
|
|
|
3849
4481
|
// src/domains/session/show.ts
|
|
3850
|
-
import { join as
|
|
4482
|
+
import { join as join9 } from "path";
|
|
3851
4483
|
|
|
3852
4484
|
// src/domains/session/list.ts
|
|
3853
4485
|
import { parse as parseYaml2 } from "yaml";
|
|
@@ -3898,6 +4530,7 @@ var SESSION_PRIORITY = {
|
|
|
3898
4530
|
};
|
|
3899
4531
|
var SESSION_STATUSES = ["todo", "doing", "archive"];
|
|
3900
4532
|
var DEFAULT_LIST_STATUSES = ["doing", "todo"];
|
|
4533
|
+
var CLAIMABLE_STATUS = SESSION_STATUSES[0];
|
|
3901
4534
|
var PRIORITY_ORDER = {
|
|
3902
4535
|
[SESSION_PRIORITY.HIGH]: 0,
|
|
3903
4536
|
[SESSION_PRIORITY.MEDIUM]: 1,
|
|
@@ -4064,12 +4697,12 @@ var SESSION_SHOW_LABEL = {
|
|
|
4064
4697
|
};
|
|
4065
4698
|
var SESSION_SHOW_SEPARATOR_CHAR = "\u2500";
|
|
4066
4699
|
var SESSION_SHOW_SEPARATOR_WIDTH = 40;
|
|
4067
|
-
var sessionsBaseDir =
|
|
4700
|
+
var sessionsBaseDir = join9(STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
|
|
4068
4701
|
var { statusDirs } = DEFAULT_CONFIG.sessions;
|
|
4069
4702
|
var DEFAULT_SESSION_CONFIG = {
|
|
4070
|
-
todoDir:
|
|
4071
|
-
doingDir:
|
|
4072
|
-
archiveDir:
|
|
4703
|
+
todoDir: join9(sessionsBaseDir, statusDirs.todo),
|
|
4704
|
+
doingDir: join9(sessionsBaseDir, statusDirs.doing),
|
|
4705
|
+
archiveDir: join9(sessionsBaseDir, statusDirs.archive)
|
|
4073
4706
|
};
|
|
4074
4707
|
var SEARCH_ORDER = [...SESSION_STATUSES];
|
|
4075
4708
|
function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
|
|
@@ -4127,7 +4760,7 @@ async function deleteCommand(options) {
|
|
|
4127
4760
|
|
|
4128
4761
|
// src/commands/session/handoff.ts
|
|
4129
4762
|
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
4130
|
-
import { join as
|
|
4763
|
+
import { join as join10, resolve as resolve4 } from "path";
|
|
4131
4764
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
4132
4765
|
|
|
4133
4766
|
// src/domains/session/create.ts
|
|
@@ -4405,8 +5038,8 @@ async function handoffCommand(options) {
|
|
|
4405
5038
|
const yaml = stringifyYaml3(frontMatterObject, { defaultStringType: "QUOTE_DOUBLE" }).trimEnd();
|
|
4406
5039
|
const fullContent = `${SESSION_FRONT_MATTER_OPEN}${yaml}${SESSION_FRONT_MATTER_CLOSE}${body}`;
|
|
4407
5040
|
const filename = `${sessionId}.md`;
|
|
4408
|
-
const sessionPath =
|
|
4409
|
-
const absolutePath =
|
|
5041
|
+
const sessionPath = join10(config.todoDir, filename);
|
|
5042
|
+
const absolutePath = resolve4(sessionPath);
|
|
4410
5043
|
await mkdir2(config.todoDir, { recursive: true });
|
|
4411
5044
|
await writeFile(sessionPath, fullContent, "utf-8");
|
|
4412
5045
|
const output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
|
|
@@ -4416,7 +5049,7 @@ async function handoffCommand(options) {
|
|
|
4416
5049
|
|
|
4417
5050
|
// src/commands/session/list.ts
|
|
4418
5051
|
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
4419
|
-
import { join as
|
|
5052
|
+
import { join as join11 } from "path";
|
|
4420
5053
|
var SESSION_LIST_FORMAT = {
|
|
4421
5054
|
TEXT: "text",
|
|
4422
5055
|
JSON: "json"
|
|
@@ -4430,7 +5063,7 @@ async function loadSessionsFromDir(dir, status) {
|
|
|
4430
5063
|
for (const file of files) {
|
|
4431
5064
|
if (!file.endsWith(".md")) continue;
|
|
4432
5065
|
const id = file.replace(".md", "");
|
|
4433
|
-
const filePath =
|
|
5066
|
+
const filePath = join11(dir, file);
|
|
4434
5067
|
const content = await readFile3(filePath, "utf-8");
|
|
4435
5068
|
const metadata = parseSessionMetadata(content);
|
|
4436
5069
|
sessions.push({
|
|
@@ -4503,7 +5136,7 @@ async function listCommand(options) {
|
|
|
4503
5136
|
|
|
4504
5137
|
// src/commands/session/pickup.ts
|
|
4505
5138
|
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rename as rename2 } from "fs/promises";
|
|
4506
|
-
import { join as
|
|
5139
|
+
import { join as join12 } from "path";
|
|
4507
5140
|
|
|
4508
5141
|
// src/domains/session/pickup.ts
|
|
4509
5142
|
function buildClaimPaths(sessionId, config) {
|
|
@@ -4539,7 +5172,6 @@ function selectBestSession(sessions) {
|
|
|
4539
5172
|
}
|
|
4540
5173
|
|
|
4541
5174
|
// src/commands/session/pickup.ts
|
|
4542
|
-
var PICKUP_SOURCE_STATUS = SESSION_STATUSES[0];
|
|
4543
5175
|
var PICKUP_TARGET_STATUS = SESSION_STATUSES[1];
|
|
4544
5176
|
async function loadTodoSessions(config) {
|
|
4545
5177
|
try {
|
|
@@ -4548,12 +5180,12 @@ async function loadTodoSessions(config) {
|
|
|
4548
5180
|
for (const file of files) {
|
|
4549
5181
|
if (!file.endsWith(".md")) continue;
|
|
4550
5182
|
const id = file.replace(".md", "");
|
|
4551
|
-
const filePath =
|
|
5183
|
+
const filePath = join12(config.todoDir, file);
|
|
4552
5184
|
const content = await readFile4(filePath, "utf-8");
|
|
4553
5185
|
const metadata = parseSessionMetadata(content);
|
|
4554
5186
|
sessions.push({
|
|
4555
5187
|
id,
|
|
4556
|
-
status:
|
|
5188
|
+
status: CLAIMABLE_STATUS,
|
|
4557
5189
|
path: filePath,
|
|
4558
5190
|
metadata
|
|
4559
5191
|
});
|
|
@@ -4602,9 +5234,15 @@ async function pickupCommand(options) {
|
|
|
4602
5234
|
return processBatch(options.sessionIds, (id) => pickupSingle(id, config));
|
|
4603
5235
|
}
|
|
4604
5236
|
|
|
5237
|
+
// src/commands/session/pick.ts
|
|
5238
|
+
async function loadPickCandidates(options) {
|
|
5239
|
+
const config = await resolveSessionConfigSurfacingWarning(options.sessionsDir, options.onWarning);
|
|
5240
|
+
return loadTodoSessions(config);
|
|
5241
|
+
}
|
|
5242
|
+
|
|
4605
5243
|
// src/commands/session/prune.ts
|
|
4606
5244
|
import { readdir as readdir3, readFile as readFile5, unlink as unlink2 } from "fs/promises";
|
|
4607
|
-
import { join as
|
|
5245
|
+
import { join as join13 } from "path";
|
|
4608
5246
|
|
|
4609
5247
|
// src/domains/session/prune.ts
|
|
4610
5248
|
var DEFAULT_KEEP_COUNT = 5;
|
|
@@ -4653,7 +5291,7 @@ async function loadArchiveSessions(config) {
|
|
|
4653
5291
|
for (const file of files) {
|
|
4654
5292
|
if (!file.endsWith(".md")) continue;
|
|
4655
5293
|
const id = file.replace(".md", "");
|
|
4656
|
-
const filePath =
|
|
5294
|
+
const filePath = join13(config.archiveDir, file);
|
|
4657
5295
|
const content = await readFile5(filePath, "utf-8");
|
|
4658
5296
|
const metadata = parseSessionMetadata(content);
|
|
4659
5297
|
sessions.push({
|
|
@@ -4871,6 +5509,190 @@ Output:
|
|
|
4871
5509
|
<PICKUP_ID>session-id</PICKUP_ID> tag for each claimed session
|
|
4872
5510
|
`;
|
|
4873
5511
|
|
|
5512
|
+
// src/interfaces/cli/session/pick/run-picker.tsx
|
|
5513
|
+
import { render } from "ink";
|
|
5514
|
+
|
|
5515
|
+
// src/interfaces/cli/session/pick/SessionPicker.tsx
|
|
5516
|
+
import { Box, Text, useInput } from "ink";
|
|
5517
|
+
import { useState } from "react";
|
|
5518
|
+
|
|
5519
|
+
// src/domains/session/pick-model.ts
|
|
5520
|
+
var PICKER_ACTION = {
|
|
5521
|
+
MOVE: "move",
|
|
5522
|
+
FILTER_APPEND: "filter-append",
|
|
5523
|
+
FILTER_DELETE: "filter-delete",
|
|
5524
|
+
CLAIM: "claim",
|
|
5525
|
+
CANCEL: "cancel"
|
|
5526
|
+
};
|
|
5527
|
+
function buildCandidates(sessions) {
|
|
5528
|
+
const claimable = sessions.filter((session) => session.status === CLAIMABLE_STATUS);
|
|
5529
|
+
return sortSessions(claimable);
|
|
5530
|
+
}
|
|
5531
|
+
function filterCandidates(candidates, query) {
|
|
5532
|
+
const needle = query.trim().toLowerCase();
|
|
5533
|
+
if (needle.length === 0) {
|
|
5534
|
+
return [...candidates];
|
|
5535
|
+
}
|
|
5536
|
+
return candidates.filter((session) => {
|
|
5537
|
+
const haystack = `${session.id}
|
|
5538
|
+
${session.metadata.goal}
|
|
5539
|
+
${session.metadata.next_step}`.toLowerCase();
|
|
5540
|
+
return haystack.includes(needle);
|
|
5541
|
+
});
|
|
5542
|
+
}
|
|
5543
|
+
function visibleCandidates(state) {
|
|
5544
|
+
return filterCandidates(state.candidates, state.query);
|
|
5545
|
+
}
|
|
5546
|
+
function selectedSession(state) {
|
|
5547
|
+
const visible = visibleCandidates(state);
|
|
5548
|
+
return visible[state.selectedIndex] ?? null;
|
|
5549
|
+
}
|
|
5550
|
+
function initialPickerState(sessions) {
|
|
5551
|
+
return { candidates: buildCandidates(sessions), query: "", selectedIndex: 0 };
|
|
5552
|
+
}
|
|
5553
|
+
function keyToAction(key) {
|
|
5554
|
+
if (key.downArrow) return { type: PICKER_ACTION.MOVE, delta: 1 };
|
|
5555
|
+
if (key.upArrow) return { type: PICKER_ACTION.MOVE, delta: -1 };
|
|
5556
|
+
if (key.return) return { type: PICKER_ACTION.CLAIM };
|
|
5557
|
+
if (key.escape) return { type: PICKER_ACTION.CANCEL };
|
|
5558
|
+
if (key.backspace || key.delete) return { type: PICKER_ACTION.FILTER_DELETE };
|
|
5559
|
+
if (key.input.length > 0) return { type: PICKER_ACTION.FILTER_APPEND, char: key.input };
|
|
5560
|
+
return null;
|
|
5561
|
+
}
|
|
5562
|
+
function clampIndex(index, count) {
|
|
5563
|
+
const upper = Math.max(0, count - 1);
|
|
5564
|
+
if (index < 0) return 0;
|
|
5565
|
+
if (index > upper) return upper;
|
|
5566
|
+
return index;
|
|
5567
|
+
}
|
|
5568
|
+
function reducePicker(state, action) {
|
|
5569
|
+
switch (action.type) {
|
|
5570
|
+
case PICKER_ACTION.MOVE: {
|
|
5571
|
+
const count = visibleCandidates(state).length;
|
|
5572
|
+
return { ...state, selectedIndex: clampIndex(state.selectedIndex + action.delta, count) };
|
|
5573
|
+
}
|
|
5574
|
+
case PICKER_ACTION.FILTER_APPEND: {
|
|
5575
|
+
const query = state.query + action.char;
|
|
5576
|
+
const count = filterCandidates(state.candidates, query).length;
|
|
5577
|
+
return { ...state, query, selectedIndex: clampIndex(state.selectedIndex, count) };
|
|
5578
|
+
}
|
|
5579
|
+
case PICKER_ACTION.FILTER_DELETE: {
|
|
5580
|
+
const query = state.query.slice(0, -1);
|
|
5581
|
+
const count = filterCandidates(state.candidates, query).length;
|
|
5582
|
+
return { ...state, query, selectedIndex: clampIndex(state.selectedIndex, count) };
|
|
5583
|
+
}
|
|
5584
|
+
case PICKER_ACTION.CLAIM:
|
|
5585
|
+
case PICKER_ACTION.CANCEL:
|
|
5586
|
+
return state;
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5589
|
+
|
|
5590
|
+
// src/interfaces/cli/session/pick/SessionPicker.tsx
|
|
5591
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5592
|
+
var SESSION_PICKER_EMPTY_TEXT = "No claimable sessions.";
|
|
5593
|
+
var PRIORITY_COLOR = {
|
|
5594
|
+
[SESSION_PRIORITY.HIGH]: "red",
|
|
5595
|
+
[SESSION_PRIORITY.MEDIUM]: "yellow",
|
|
5596
|
+
[SESSION_PRIORITY.LOW]: "gray"
|
|
5597
|
+
};
|
|
5598
|
+
function toPickerKey(input, key) {
|
|
5599
|
+
return {
|
|
5600
|
+
input,
|
|
5601
|
+
upArrow: key.upArrow,
|
|
5602
|
+
downArrow: key.downArrow,
|
|
5603
|
+
return: key.return,
|
|
5604
|
+
escape: key.escape,
|
|
5605
|
+
backspace: key.backspace,
|
|
5606
|
+
delete: key.delete
|
|
5607
|
+
};
|
|
5608
|
+
}
|
|
5609
|
+
function SessionRow({ session, selected }) {
|
|
5610
|
+
const priority = session.metadata.priority;
|
|
5611
|
+
const marker = selected ? "\u276F" : " ";
|
|
5612
|
+
const badge = priority === DEFAULT_PRIORITY ? "" : ` [${priority}]`;
|
|
5613
|
+
return /* @__PURE__ */ jsxs(Text, { color: selected ? "cyan" : void 0, children: [
|
|
5614
|
+
marker,
|
|
5615
|
+
" ",
|
|
5616
|
+
session.id,
|
|
5617
|
+
/* @__PURE__ */ jsx(Text, { color: PRIORITY_COLOR[priority], children: badge }),
|
|
5618
|
+
" ",
|
|
5619
|
+
session.metadata.goal
|
|
5620
|
+
] });
|
|
5621
|
+
}
|
|
5622
|
+
function PreviewPane({ session }) {
|
|
5623
|
+
if (session === null) {
|
|
5624
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: SESSION_PICKER_EMPTY_TEXT }) });
|
|
5625
|
+
}
|
|
5626
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
5627
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
5628
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "goal:" }),
|
|
5629
|
+
session.metadata.goal
|
|
5630
|
+
] }),
|
|
5631
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
5632
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "next:" }),
|
|
5633
|
+
session.metadata.next_step
|
|
5634
|
+
] })
|
|
5635
|
+
] });
|
|
5636
|
+
}
|
|
5637
|
+
function SessionPicker({ sessions, onClaim, onCancel }) {
|
|
5638
|
+
const [state, setState] = useState(() => initialPickerState(sessions));
|
|
5639
|
+
useInput((input, key) => {
|
|
5640
|
+
const action = keyToAction(toPickerKey(input, key));
|
|
5641
|
+
if (action === null) return;
|
|
5642
|
+
if (action.type === PICKER_ACTION.CLAIM) {
|
|
5643
|
+
const selected2 = selectedSession(state);
|
|
5644
|
+
if (selected2 !== null) onClaim(selected2);
|
|
5645
|
+
return;
|
|
5646
|
+
}
|
|
5647
|
+
if (action.type === PICKER_ACTION.CANCEL) {
|
|
5648
|
+
onCancel();
|
|
5649
|
+
return;
|
|
5650
|
+
}
|
|
5651
|
+
setState((previous) => reducePicker(previous, action));
|
|
5652
|
+
});
|
|
5653
|
+
const visible = visibleCandidates(state);
|
|
5654
|
+
const selected = selectedSession(state);
|
|
5655
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
5656
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
5657
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Pick a session to claim" }),
|
|
5658
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(type to filter, \u2191\u2193 to move, \u23CE to claim, esc to cancel)" })
|
|
5659
|
+
] }),
|
|
5660
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
5661
|
+
"filter: ",
|
|
5662
|
+
state.query
|
|
5663
|
+
] }),
|
|
5664
|
+
visible.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: SESSION_PICKER_EMPTY_TEXT }) : visible.map((session, index) => /* @__PURE__ */ jsx(SessionRow, { session, selected: index === state.selectedIndex }, session.id)),
|
|
5665
|
+
/* @__PURE__ */ jsx(PreviewPane, { session: selected })
|
|
5666
|
+
] });
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
// src/interfaces/cli/session/pick/run-picker.tsx
|
|
5670
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
5671
|
+
var PICK_NON_TTY_MESSAGE = "session pick requires an interactive terminal. Use `spx session pickup --auto` or `spx session pickup <id>` in a non-interactive context.";
|
|
5672
|
+
async function runPicker(sessions) {
|
|
5673
|
+
let claimed = null;
|
|
5674
|
+
let unmount = () => {
|
|
5675
|
+
};
|
|
5676
|
+
const instance = render(
|
|
5677
|
+
/* @__PURE__ */ jsx2(
|
|
5678
|
+
SessionPicker,
|
|
5679
|
+
{
|
|
5680
|
+
sessions,
|
|
5681
|
+
onClaim: (session) => {
|
|
5682
|
+
claimed = session;
|
|
5683
|
+
unmount();
|
|
5684
|
+
},
|
|
5685
|
+
onCancel: () => {
|
|
5686
|
+
unmount();
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5689
|
+
)
|
|
5690
|
+
);
|
|
5691
|
+
unmount = instance.unmount;
|
|
5692
|
+
await instance.waitUntilExit();
|
|
5693
|
+
return claimed;
|
|
5694
|
+
}
|
|
5695
|
+
|
|
4874
5696
|
// src/interfaces/cli/session.ts
|
|
4875
5697
|
async function readStdin() {
|
|
4876
5698
|
if (process.stdin.isTTY) {
|
|
@@ -4909,6 +5731,29 @@ function registerSessionCommands(sessionCmd) {
|
|
|
4909
5731
|
handleError(error);
|
|
4910
5732
|
}
|
|
4911
5733
|
});
|
|
5734
|
+
sessionCmd.command("pick").description("Interactively pick and claim a session (move from todo to doing)").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
5735
|
+
try {
|
|
5736
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
5737
|
+
console.error(PICK_NON_TTY_MESSAGE);
|
|
5738
|
+
process.exit(1);
|
|
5739
|
+
}
|
|
5740
|
+
const sessions = await loadPickCandidates({
|
|
5741
|
+
sessionsDir: options.sessionsDir,
|
|
5742
|
+
onWarning: writeWarning
|
|
5743
|
+
});
|
|
5744
|
+
const claimed = await runPicker(sessions);
|
|
5745
|
+
if (claimed !== null) {
|
|
5746
|
+
const output = await pickupCommand({
|
|
5747
|
+
sessionIds: [claimed.id],
|
|
5748
|
+
sessionsDir: options.sessionsDir,
|
|
5749
|
+
onWarning: writeWarning
|
|
5750
|
+
});
|
|
5751
|
+
console.log(output);
|
|
5752
|
+
}
|
|
5753
|
+
} catch (error) {
|
|
5754
|
+
handleError(error);
|
|
5755
|
+
}
|
|
5756
|
+
});
|
|
4912
5757
|
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) => {
|
|
4913
5758
|
try {
|
|
4914
5759
|
const output = await listCommand({
|
|
@@ -5043,7 +5888,7 @@ var sessionDomain = {
|
|
|
5043
5888
|
|
|
5044
5889
|
// src/lib/spec-tree/index.ts
|
|
5045
5890
|
import { readdir as readdir5, readFile as readFile7 } from "fs/promises";
|
|
5046
|
-
import { join as
|
|
5891
|
+
import { join as join14 } from "path";
|
|
5047
5892
|
var SPEC_TREE_FIELD_KEY = {
|
|
5048
5893
|
VERSION: "version",
|
|
5049
5894
|
PRODUCT: "product",
|
|
@@ -5127,7 +5972,7 @@ function createFilesystemSpecTreeSource(options) {
|
|
|
5127
5972
|
if (ref.path === void 0) {
|
|
5128
5973
|
throw new Error("Filesystem source refs require a path");
|
|
5129
5974
|
}
|
|
5130
|
-
return readFile7(
|
|
5975
|
+
return readFile7(join14(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
|
|
5131
5976
|
}
|
|
5132
5977
|
};
|
|
5133
5978
|
}
|
|
@@ -5409,7 +6254,7 @@ function compareOrderedEntries(left, right) {
|
|
|
5409
6254
|
}
|
|
5410
6255
|
async function* readFilesystemSourceEntries(productDir, registry, schemaVersions, includePath) {
|
|
5411
6256
|
yield* walkFilesystemDirectory({
|
|
5412
|
-
absolutePath:
|
|
6257
|
+
absolutePath: join14(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY),
|
|
5413
6258
|
relativePath: SPEC_TREE_EMPTY_RELATIVE_PATH,
|
|
5414
6259
|
registry,
|
|
5415
6260
|
schemaVersions,
|
|
@@ -5438,7 +6283,7 @@ async function* walkFilesystemDirectory(context) {
|
|
|
5438
6283
|
if (sourceEntry !== null) yield sourceEntry;
|
|
5439
6284
|
if (entry.isDirectory() && shouldDescendIntoDirectory(sourceEntry)) {
|
|
5440
6285
|
yield* walkFilesystemDirectory({
|
|
5441
|
-
absolutePath:
|
|
6286
|
+
absolutePath: join14(context.absolutePath, entry.name),
|
|
5442
6287
|
relativePath,
|
|
5443
6288
|
registry: context.registry,
|
|
5444
6289
|
schemaVersions: context.schemaVersions,
|
|
@@ -5584,14 +6429,14 @@ function formatNextNode(node) {
|
|
|
5584
6429
|
|
|
5585
6430
|
// src/commands/testing/discovery.ts
|
|
5586
6431
|
import { readdir as readdir6 } from "fs/promises";
|
|
5587
|
-
import { join as
|
|
6432
|
+
import { join as join15, relative, sep } from "path";
|
|
5588
6433
|
var TESTS_DIRECTORY_NAME = SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME;
|
|
5589
6434
|
var SPEC_ROOT_DIRECTORY = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
|
|
5590
6435
|
var POSIX_SEPARATOR = "/";
|
|
5591
6436
|
var ERROR_CODE_NOT_FOUND2 = "ENOENT";
|
|
5592
6437
|
async function discoverTestFiles(productDir) {
|
|
5593
6438
|
const found = [];
|
|
5594
|
-
await collectTestFiles(
|
|
6439
|
+
await collectTestFiles(join15(productDir, SPEC_ROOT_DIRECTORY), false, found);
|
|
5595
6440
|
return found.map((absolute) => toPosixRelative(productDir, absolute)).sort(compareAscii);
|
|
5596
6441
|
}
|
|
5597
6442
|
async function collectTestFiles(directory, insideTestsDir, found) {
|
|
@@ -5603,7 +6448,7 @@ async function collectTestFiles(directory, insideTestsDir, found) {
|
|
|
5603
6448
|
throw error;
|
|
5604
6449
|
}
|
|
5605
6450
|
for (const entry of entries) {
|
|
5606
|
-
const childPath =
|
|
6451
|
+
const childPath = join15(directory, entry.name);
|
|
5607
6452
|
if (entry.isDirectory()) {
|
|
5608
6453
|
await collectTestFiles(childPath, insideTestsDir || entry.name === TESTS_DIRECTORY_NAME, found);
|
|
5609
6454
|
} else if (insideTestsDir && entry.isFile()) {
|
|
@@ -5685,11 +6530,11 @@ async function runTests(options, deps) {
|
|
|
5685
6530
|
}
|
|
5686
6531
|
|
|
5687
6532
|
// src/commands/testing/run-command.ts
|
|
5688
|
-
import { join as
|
|
6533
|
+
import { join as join17 } from "path";
|
|
5689
6534
|
|
|
5690
6535
|
// src/testing/run-state.ts
|
|
5691
6536
|
import { createHash as createHash4 } from "crypto";
|
|
5692
|
-
import { join as
|
|
6537
|
+
import { join as join16 } from "path";
|
|
5693
6538
|
var TEST_RUN_STATE_STATUS = {
|
|
5694
6539
|
PASSED: "passed",
|
|
5695
6540
|
FAILED: "failed",
|
|
@@ -5776,12 +6621,12 @@ async function readTestingRuns(productDir, options = {}) {
|
|
|
5776
6621
|
if (hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) {
|
|
5777
6622
|
return { ok: true, value: { terminalRuns: [], incompleteRuns: [] } };
|
|
5778
6623
|
}
|
|
5779
|
-
return { ok: false, error:
|
|
6624
|
+
return { ok: false, error: toErrorMessage3(error) };
|
|
5780
6625
|
}
|
|
5781
6626
|
const terminalRuns = [];
|
|
5782
6627
|
const incompleteRuns = [];
|
|
5783
6628
|
for (const entry of entries.filter(isTestRunFileEntry)) {
|
|
5784
|
-
const runFilePath =
|
|
6629
|
+
const runFilePath = join16(runsDir2, entry.name);
|
|
5785
6630
|
const stateResult = await readTestRunStatePath(runFilePath, fs7);
|
|
5786
6631
|
if (stateResult.ok) {
|
|
5787
6632
|
terminalRuns.push({ runFileName: entry.name, runFilePath, state: stateResult.value });
|
|
@@ -5858,7 +6703,7 @@ async function readTestRunStatePath(runFilePath, fs7) {
|
|
|
5858
6703
|
return {
|
|
5859
6704
|
ok: false,
|
|
5860
6705
|
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:
|
|
6706
|
+
error: toErrorMessage3(error)
|
|
5862
6707
|
};
|
|
5863
6708
|
}
|
|
5864
6709
|
const latest = latestNonEmptyJsonlLine(content);
|
|
@@ -6010,7 +6855,7 @@ function isRecord4(value) {
|
|
|
6010
6855
|
function sha256Hex(value) {
|
|
6011
6856
|
return createHash4(SHA256_ALGORITHM2).update(value).digest(HEX_ENCODING3);
|
|
6012
6857
|
}
|
|
6013
|
-
function
|
|
6858
|
+
function toErrorMessage3(error) {
|
|
6014
6859
|
return error instanceof Error ? error.message : String(error);
|
|
6015
6860
|
}
|
|
6016
6861
|
|
|
@@ -6047,7 +6892,7 @@ function coveredTestPaths(dispatch) {
|
|
|
6047
6892
|
async function readCoveredContents(productDir, paths, fs7) {
|
|
6048
6893
|
const entries = [];
|
|
6049
6894
|
for (const path6 of paths) {
|
|
6050
|
-
entries.push({ path: path6, content: await fs7.readFile(
|
|
6895
|
+
entries.push({ path: path6, content: await fs7.readFile(join17(productDir, path6), TEXT_ENCODING) });
|
|
6051
6896
|
}
|
|
6052
6897
|
return entries;
|
|
6053
6898
|
}
|
|
@@ -6058,7 +6903,7 @@ async function readProductInputEntries(productDir, paths, fs7) {
|
|
|
6058
6903
|
entries.push({
|
|
6059
6904
|
[PRODUCT_INPUT_FIELDS.PATH]: path6,
|
|
6060
6905
|
[PRODUCT_INPUT_FIELDS.PRESENT]: true,
|
|
6061
|
-
[PRODUCT_INPUT_FIELDS.CONTENT]: await fs7.readFile(
|
|
6906
|
+
[PRODUCT_INPUT_FIELDS.CONTENT]: await fs7.readFile(join17(productDir, path6), TEXT_ENCODING)
|
|
6062
6907
|
});
|
|
6063
6908
|
} catch (error) {
|
|
6064
6909
|
if (!hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) throw error;
|
|
@@ -6241,11 +7086,11 @@ function serializeNodeStatus(state) {
|
|
|
6241
7086
|
}
|
|
6242
7087
|
|
|
6243
7088
|
// src/lib/node-status/provider.ts
|
|
6244
|
-
import { join as
|
|
7089
|
+
import { join as join19 } from "path";
|
|
6245
7090
|
|
|
6246
7091
|
// src/lib/node-status/read.ts
|
|
6247
7092
|
import { readFileSync as readFileSync2 } from "fs";
|
|
6248
|
-
import { join as
|
|
7093
|
+
import { join as join18 } from "path";
|
|
6249
7094
|
var NODE_STATUS_FILENAME = "spx.status.json";
|
|
6250
7095
|
var NODE_STATUS_VALUES = new Set(Object.values(SPEC_TREE_NODE_STATE));
|
|
6251
7096
|
function isNodeError2(error) {
|
|
@@ -6255,7 +7100,7 @@ function isSpecTreeNodeState(value) {
|
|
|
6255
7100
|
return typeof value === "string" && NODE_STATUS_VALUES.has(value);
|
|
6256
7101
|
}
|
|
6257
7102
|
function readNodeStatus(nodeDir) {
|
|
6258
|
-
const filePath =
|
|
7103
|
+
const filePath = join18(nodeDir, NODE_STATUS_FILENAME);
|
|
6259
7104
|
let content;
|
|
6260
7105
|
try {
|
|
6261
7106
|
content = readFileSync2(filePath, "utf8");
|
|
@@ -6277,7 +7122,7 @@ function readNodeStatus(nodeDir) {
|
|
|
6277
7122
|
|
|
6278
7123
|
// src/lib/node-status/provider.ts
|
|
6279
7124
|
function nodeDirectory(productDir, node) {
|
|
6280
|
-
return
|
|
7125
|
+
return join19(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
|
|
6281
7126
|
}
|
|
6282
7127
|
function createNodeStatusProvider(productDir) {
|
|
6283
7128
|
return {
|
|
@@ -6289,7 +7134,7 @@ function createNodeStatusProvider(productDir) {
|
|
|
6289
7134
|
|
|
6290
7135
|
// src/lib/node-status/update.ts
|
|
6291
7136
|
import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
|
|
6292
|
-
import { dirname as
|
|
7137
|
+
import { dirname as dirname5, join as join20 } from "path";
|
|
6293
7138
|
var NODE_STATUS_TEXT_ENCODING = "utf8";
|
|
6294
7139
|
async function updateNodeStatus(options) {
|
|
6295
7140
|
const { productDir, resolveOutcome } = options;
|
|
@@ -6327,8 +7172,8 @@ function isNodeExcluded(ignoreReader, node) {
|
|
|
6327
7172
|
return ignoreReader.isUnderIgnoreSource(reference);
|
|
6328
7173
|
}
|
|
6329
7174
|
async function writeNodeStatus(productDir, nodeId, state) {
|
|
6330
|
-
const filePath =
|
|
6331
|
-
await mkdir4(
|
|
7175
|
+
const filePath = join20(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
|
|
7176
|
+
await mkdir4(dirname5(filePath), { recursive: true });
|
|
6332
7177
|
await writeFile2(filePath, serializeNodeStatus(state), NODE_STATUS_TEXT_ENCODING);
|
|
6333
7178
|
}
|
|
6334
7179
|
|
|
@@ -6442,7 +7287,7 @@ function formatNodeLabel(node) {
|
|
|
6442
7287
|
}
|
|
6443
7288
|
|
|
6444
7289
|
// src/testing/languages/python.ts
|
|
6445
|
-
import { basename as
|
|
7290
|
+
import { basename as basename3, dirname as dirname6, join as join21 } from "path/posix";
|
|
6446
7291
|
|
|
6447
7292
|
// src/validation/discovery/language-finder.ts
|
|
6448
7293
|
import fs5 from "fs";
|
|
@@ -6507,16 +7352,16 @@ var PYTHON_TEST_FILE_PREFIX = "test_";
|
|
|
6507
7352
|
var PYTHON_TEST_FILE_EXTENSION = ".py";
|
|
6508
7353
|
var PYTHON_TEST_FILE_PATTERNS = [`${PYTHON_TEST_FILE_PREFIX}*${PYTHON_TEST_FILE_EXTENSION}`];
|
|
6509
7354
|
function matchesTestFile(filePath) {
|
|
6510
|
-
return
|
|
7355
|
+
return basename3(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
|
|
6511
7356
|
}
|
|
6512
7357
|
function coveredProductInputPaths(coveredTestPaths2) {
|
|
6513
7358
|
const paths = /* @__PURE__ */ new Set();
|
|
6514
7359
|
for (const testPath of coveredTestPaths2) {
|
|
6515
7360
|
if (!matchesTestFile(testPath)) continue;
|
|
6516
|
-
let directory =
|
|
7361
|
+
let directory = dirname6(testPath);
|
|
6517
7362
|
while (directory !== "." && directory.length > 0) {
|
|
6518
|
-
paths.add(
|
|
6519
|
-
const parent =
|
|
7363
|
+
paths.add(join21(directory, PYTHON_PRODUCT_INPUT_PATH.CONFTEST));
|
|
7364
|
+
const parent = dirname6(directory);
|
|
6520
7365
|
if (parent === directory) break;
|
|
6521
7366
|
directory = parent;
|
|
6522
7367
|
}
|
|
@@ -6997,7 +7842,7 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
|
|
|
6997
7842
|
|
|
6998
7843
|
// src/validation/steps/markdown.ts
|
|
6999
7844
|
import { existsSync, statSync } from "fs";
|
|
7000
|
-
import { basename as
|
|
7845
|
+
import { basename as basename4, dirname as dirname7, join as join22, relative as pathRelative } from "path";
|
|
7001
7846
|
import { main as markdownlintMain } from "markdownlint-cli2";
|
|
7002
7847
|
import relativeLinksRule from "markdownlint-rule-relative-links";
|
|
7003
7848
|
var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
|
|
@@ -7036,7 +7881,7 @@ function buildMarkdownlintConfig(directoryName) {
|
|
|
7036
7881
|
};
|
|
7037
7882
|
}
|
|
7038
7883
|
function getDefaultDirectories(projectRoot) {
|
|
7039
|
-
return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) =>
|
|
7884
|
+
return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join22(projectRoot, name)).filter((dir) => existsSync(dir));
|
|
7040
7885
|
}
|
|
7041
7886
|
function resolveMarkdownValidationTarget(path6, deps = defaultMarkdownValidationTargetDeps) {
|
|
7042
7887
|
if (isExistingDirectory(path6, deps)) {
|
|
@@ -7095,7 +7940,7 @@ async function validateMarkdown(options) {
|
|
|
7095
7940
|
async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
7096
7941
|
const errors = [];
|
|
7097
7942
|
const directory = targetDirectory(target);
|
|
7098
|
-
const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [
|
|
7943
|
+
const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename4(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
|
|
7099
7944
|
const { customRules, ...markdownlintConfig } = config;
|
|
7100
7945
|
const optionsOverride = {
|
|
7101
7946
|
config: {
|
|
@@ -7119,7 +7964,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
|
7119
7964
|
if (parsed) {
|
|
7120
7965
|
errors.push({
|
|
7121
7966
|
...parsed,
|
|
7122
|
-
file:
|
|
7967
|
+
file: join22(directory, parsed.file)
|
|
7123
7968
|
});
|
|
7124
7969
|
}
|
|
7125
7970
|
}
|
|
@@ -7127,7 +7972,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
|
7127
7972
|
return errors;
|
|
7128
7973
|
}
|
|
7129
7974
|
function targetDirectory(target) {
|
|
7130
|
-
return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ?
|
|
7975
|
+
return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname7(target.path) : target.path;
|
|
7131
7976
|
}
|
|
7132
7977
|
function hasMarkdownExtension(path6) {
|
|
7133
7978
|
const lastDot = path6.lastIndexOf(".");
|
|
@@ -7155,7 +8000,7 @@ function markdownlintConfigDirectoryName(directory, projectRoot) {
|
|
|
7155
8000
|
return rootSegment;
|
|
7156
8001
|
}
|
|
7157
8002
|
}
|
|
7158
|
-
return
|
|
8003
|
+
return basename4(directory);
|
|
7159
8004
|
}
|
|
7160
8005
|
|
|
7161
8006
|
// src/commands/validation/messages.ts
|
|
@@ -7169,6 +8014,7 @@ var VALIDATION_STAGE_DISPLAY_NAMES = {
|
|
|
7169
8014
|
};
|
|
7170
8015
|
var VALIDATION_SKIP_LABELS = {
|
|
7171
8016
|
VERB: "Skipping",
|
|
8017
|
+
CIRCULAR_REASON: "skip-circular",
|
|
7172
8018
|
LITERAL_REASON: "skip-literal",
|
|
7173
8019
|
TYPESCRIPT_ABSENT_REASON: "TypeScript not detected in project",
|
|
7174
8020
|
VALIDATION_PATHS_NO_TARGETS_REASON: "validation paths matched no files",
|
|
@@ -7190,6 +8036,11 @@ var VALIDATION_COMMAND_OUTPUT = {
|
|
|
7190
8036
|
MARKDOWN_NO_ISSUES: `${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: No issues found`,
|
|
7191
8037
|
MARKDOWN_ERROR_SUMMARY_SUFFIX: "error(s) found"
|
|
7192
8038
|
};
|
|
8039
|
+
var CIRCULAR_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR}: skipped (--${VALIDATION_SKIP_LABELS.CIRCULAR_REASON})`;
|
|
8040
|
+
var CIRCULAR_SKIP_JSON_OUTPUT = JSON.stringify({
|
|
8041
|
+
skipped: true,
|
|
8042
|
+
reason: VALIDATION_SKIP_LABELS.CIRCULAR_REASON
|
|
8043
|
+
});
|
|
7193
8044
|
var LITERAL_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.LITERAL}: skipped (--${VALIDATION_SKIP_LABELS.LITERAL_REASON})`;
|
|
7194
8045
|
var LITERAL_SKIP_JSON_OUTPUT = JSON.stringify({
|
|
7195
8046
|
skipped: true,
|
|
@@ -8141,7 +8992,7 @@ var ParseErrorCode;
|
|
|
8141
8992
|
|
|
8142
8993
|
// src/validation/config/scope.ts
|
|
8143
8994
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
8144
|
-
import { isAbsolute as isAbsolute3, join as
|
|
8995
|
+
import { isAbsolute as isAbsolute3, join as join23 } from "path";
|
|
8145
8996
|
var TSCONFIG_FILES = {
|
|
8146
8997
|
full: "tsconfig.json",
|
|
8147
8998
|
production: "tsconfig.production.json"
|
|
@@ -8155,7 +9006,7 @@ var defaultScopeDeps = {
|
|
|
8155
9006
|
readdirSync
|
|
8156
9007
|
};
|
|
8157
9008
|
function resolveProjectPath(projectRoot, path6) {
|
|
8158
|
-
return isAbsolute3(path6) ? path6 :
|
|
9009
|
+
return isAbsolute3(path6) ? path6 : join23(projectRoot, path6);
|
|
8159
9010
|
}
|
|
8160
9011
|
function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
|
|
8161
9012
|
try {
|
|
@@ -8199,7 +9050,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
|
|
|
8199
9050
|
if (hasDirectTsFiles) return true;
|
|
8200
9051
|
const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
|
|
8201
9052
|
for (const subdir of subdirs.slice(0, 5)) {
|
|
8202
|
-
if (hasTypeScriptFilesRecursive(
|
|
9053
|
+
if (hasTypeScriptFilesRecursive(join23(dirPath, subdir.name), maxDepth - 1, deps)) {
|
|
8203
9054
|
return true;
|
|
8204
9055
|
}
|
|
8205
9056
|
}
|
|
@@ -8222,7 +9073,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
|
|
|
8222
9073
|
});
|
|
8223
9074
|
if (!isExcluded) {
|
|
8224
9075
|
try {
|
|
8225
|
-
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(
|
|
9076
|
+
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join23(projectRoot, dir), 2, deps);
|
|
8226
9077
|
if (hasTypeScriptFiles) {
|
|
8227
9078
|
directories.add(dir);
|
|
8228
9079
|
}
|
|
@@ -8253,13 +9104,13 @@ function getLiteralTopLevelPatternDirectory(pattern) {
|
|
|
8253
9104
|
function filterActiveIncludePatterns(patterns, excludePatterns, projectRoot, deps) {
|
|
8254
9105
|
return patterns.filter((pattern) => {
|
|
8255
9106
|
const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
|
|
8256
|
-
return topLevelDir === null || deps.existsSync(
|
|
9107
|
+
return topLevelDir === null || deps.existsSync(join23(projectRoot, topLevelDir));
|
|
8257
9108
|
}).filter((pattern) => pathPassesValidationFilter(pattern, { exclude: excludePatterns }));
|
|
8258
9109
|
}
|
|
8259
9110
|
function getValidationDirectories(scope, projectRoot, deps = defaultScopeDeps) {
|
|
8260
9111
|
const config = resolveTypeScriptConfig(scope, projectRoot, deps);
|
|
8261
9112
|
const configDirectories = getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps);
|
|
8262
|
-
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(
|
|
9113
|
+
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join23(projectRoot, dir)));
|
|
8263
9114
|
return existingDirectories;
|
|
8264
9115
|
}
|
|
8265
9116
|
function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
|
|
@@ -8273,10 +9124,10 @@ function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
|
|
|
8273
9124
|
}
|
|
8274
9125
|
|
|
8275
9126
|
// src/validation/discovery/tool-finder.ts
|
|
8276
|
-
import { execSync as execSync2 } from "child_process";
|
|
8277
9127
|
import fs6 from "fs";
|
|
8278
9128
|
import { createRequire } from "module";
|
|
8279
9129
|
import path5 from "path";
|
|
9130
|
+
import { fileURLToPath } from "url";
|
|
8280
9131
|
|
|
8281
9132
|
// src/validation/discovery/constants.ts
|
|
8282
9133
|
var TOOL_DISCOVERY = {
|
|
@@ -8310,6 +9161,50 @@ var TOOL_DISCOVERY = {
|
|
|
8310
9161
|
|
|
8311
9162
|
// src/validation/discovery/tool-finder.ts
|
|
8312
9163
|
var require2 = createRequire(import.meta.url);
|
|
9164
|
+
var FILE_URL_PROTOCOL = new URL(import.meta.url).protocol;
|
|
9165
|
+
var PACKAGE_MANIFEST_FILENAME = "package.json";
|
|
9166
|
+
var PATH_ENVIRONMENT_VARIABLE = "PATH";
|
|
9167
|
+
var WINDOWS_EXECUTABLE_EXTENSIONS_VARIABLE = "PATHEXT";
|
|
9168
|
+
var WINDOWS_DEFAULT_EXECUTABLE_EXTENSIONS = [".COM", ".EXE", ".BAT", ".CMD"];
|
|
9169
|
+
function executableExtensions() {
|
|
9170
|
+
if (process.platform !== "win32") {
|
|
9171
|
+
return [""];
|
|
9172
|
+
}
|
|
9173
|
+
const pathExtensions = process.env[WINDOWS_EXECUTABLE_EXTENSIONS_VARIABLE]?.split(path5.delimiter).filter(Boolean);
|
|
9174
|
+
return pathExtensions && pathExtensions.length > 0 ? pathExtensions : WINDOWS_DEFAULT_EXECUTABLE_EXTENSIONS;
|
|
9175
|
+
}
|
|
9176
|
+
function executableCandidateNames(tool) {
|
|
9177
|
+
if (process.platform !== "win32" || path5.extname(tool) !== "") {
|
|
9178
|
+
return [tool];
|
|
9179
|
+
}
|
|
9180
|
+
return executableExtensions().map((extension) => `${tool}${extension}`);
|
|
9181
|
+
}
|
|
9182
|
+
function isExecutableFile(filePath) {
|
|
9183
|
+
try {
|
|
9184
|
+
fs6.accessSync(filePath, fs6.constants.X_OK);
|
|
9185
|
+
return true;
|
|
9186
|
+
} catch {
|
|
9187
|
+
return false;
|
|
9188
|
+
}
|
|
9189
|
+
}
|
|
9190
|
+
function findExecutableOnPath(tool) {
|
|
9191
|
+
const pathValue = process.env[PATH_ENVIRONMENT_VARIABLE];
|
|
9192
|
+
if (!pathValue) {
|
|
9193
|
+
return null;
|
|
9194
|
+
}
|
|
9195
|
+
for (const directory of pathValue.split(path5.delimiter)) {
|
|
9196
|
+
if (!directory) {
|
|
9197
|
+
continue;
|
|
9198
|
+
}
|
|
9199
|
+
for (const candidateName of executableCandidateNames(tool)) {
|
|
9200
|
+
const candidatePath = path5.join(directory, candidateName);
|
|
9201
|
+
if (isExecutableFile(candidatePath)) {
|
|
9202
|
+
return candidatePath;
|
|
9203
|
+
}
|
|
9204
|
+
}
|
|
9205
|
+
}
|
|
9206
|
+
return null;
|
|
9207
|
+
}
|
|
8313
9208
|
var defaultToolDiscoveryDeps = {
|
|
8314
9209
|
resolveModule: (modulePath) => {
|
|
8315
9210
|
try {
|
|
@@ -8318,28 +9213,50 @@ var defaultToolDiscoveryDeps = {
|
|
|
8318
9213
|
return null;
|
|
8319
9214
|
}
|
|
8320
9215
|
},
|
|
8321
|
-
|
|
8322
|
-
whichSync: (tool) => {
|
|
9216
|
+
resolveImport: (modulePath) => {
|
|
8323
9217
|
try {
|
|
8324
|
-
|
|
8325
|
-
encoding: "utf-8",
|
|
8326
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
8327
|
-
});
|
|
8328
|
-
return result.trim() || null;
|
|
9218
|
+
return import.meta.resolve(modulePath);
|
|
8329
9219
|
} catch {
|
|
8330
9220
|
return null;
|
|
8331
9221
|
}
|
|
8332
|
-
}
|
|
9222
|
+
},
|
|
9223
|
+
existsSync: fs6.existsSync,
|
|
9224
|
+
whichSync: findExecutableOnPath
|
|
8333
9225
|
};
|
|
9226
|
+
function resolvedModulePath(resolvedPath) {
|
|
9227
|
+
try {
|
|
9228
|
+
const resolvedUrl = new URL(resolvedPath);
|
|
9229
|
+
return resolvedUrl.protocol === FILE_URL_PROTOCOL ? fileURLToPath(resolvedUrl) : resolvedPath;
|
|
9230
|
+
} catch {
|
|
9231
|
+
return resolvedPath;
|
|
9232
|
+
}
|
|
9233
|
+
}
|
|
9234
|
+
function nearestPackageRoot(filePath, existsSync7) {
|
|
9235
|
+
let currentDirectory = path5.dirname(filePath);
|
|
9236
|
+
while (true) {
|
|
9237
|
+
if (existsSync7(path5.join(currentDirectory, PACKAGE_MANIFEST_FILENAME))) {
|
|
9238
|
+
return currentDirectory;
|
|
9239
|
+
}
|
|
9240
|
+
const parentDirectory = path5.dirname(currentDirectory);
|
|
9241
|
+
if (parentDirectory === currentDirectory) {
|
|
9242
|
+
return null;
|
|
9243
|
+
}
|
|
9244
|
+
currentDirectory = parentDirectory;
|
|
9245
|
+
}
|
|
9246
|
+
}
|
|
9247
|
+
function bundledToolPath(resolvedPath, existsSync7) {
|
|
9248
|
+
const bundledFilePath = resolvedModulePath(resolvedPath);
|
|
9249
|
+
return nearestPackageRoot(bundledFilePath, existsSync7) ?? path5.dirname(bundledFilePath);
|
|
9250
|
+
}
|
|
8334
9251
|
async function discoverTool(tool, options = {}) {
|
|
8335
9252
|
const { projectRoot = process.cwd(), deps = defaultToolDiscoveryDeps } = options;
|
|
8336
|
-
const bundledPath = deps.resolveModule(`${tool}/package.json`);
|
|
9253
|
+
const bundledPath = deps.resolveModule(`${tool}/package.json`) ?? deps.resolveImport?.(tool);
|
|
8337
9254
|
if (bundledPath) {
|
|
8338
9255
|
return {
|
|
8339
9256
|
found: true,
|
|
8340
9257
|
location: {
|
|
8341
9258
|
tool,
|
|
8342
|
-
path:
|
|
9259
|
+
path: bundledToolPath(bundledPath, deps.existsSync),
|
|
8343
9260
|
source: TOOL_DISCOVERY.SOURCES.BUNDLED
|
|
8344
9261
|
}
|
|
8345
9262
|
};
|
|
@@ -8382,33 +9299,180 @@ function formatSkipMessage(stepName, result) {
|
|
|
8382
9299
|
}
|
|
8383
9300
|
|
|
8384
9301
|
// src/validation/steps/circular.ts
|
|
8385
|
-
import
|
|
8386
|
-
|
|
9302
|
+
import {
|
|
9303
|
+
cruise as dependencyCruiser
|
|
9304
|
+
} from "dependency-cruiser";
|
|
9305
|
+
import extractTypeScriptConfig from "dependency-cruiser/config-utl/extract-ts-config";
|
|
9306
|
+
import { join as join24 } from "path";
|
|
9307
|
+
var DEPENDENCY_CRUISER_MODULE_SYSTEMS = ["es6", "cjs"];
|
|
9308
|
+
var DEPENDENCY_CRUISER_TYPESCRIPT_SOURCE_GLOB_SUFFIXES = ["**/*.ts", "**/*.tsx"];
|
|
9309
|
+
var DEPENDENCY_CRUISER_TYPESCRIPT_SOURCE_PATTERN = String.raw`\.tsx?$`;
|
|
9310
|
+
var DEPENDENCY_CRUISER_TYPESCRIPT_RESOLVE_EXTENSIONS = [".ts", ".tsx", ".d.ts"];
|
|
9311
|
+
var TSCONFIG_EXCLUDE_SUFFIX_PATTERN = /\/\*\*?\/\*$/u;
|
|
9312
|
+
var REGEX_SPECIAL_CHARACTER_PATTERN = /[.*+?^${}()|[\]\\]/gu;
|
|
9313
|
+
var REGEX_ESCAPE_REPLACEMENT = String.raw`\$&`;
|
|
9314
|
+
var CYCLE_KEY_SEPARATOR = "\0";
|
|
9315
|
+
var DEPENDENCY_CRUISER_PACKAGE_EXCLUDE_PATTERN = "(^|/)node_modules(/|$)";
|
|
9316
|
+
var DEPENDENCY_CRUISER_DEPENDENCY_TYPES = {
|
|
9317
|
+
AMD_DEFINE: "amd-define",
|
|
9318
|
+
AMD_EXOTIC_REQUIRE: "amd-exotic-require",
|
|
9319
|
+
AMD_REQUIRE: "amd-require",
|
|
9320
|
+
DYNAMIC_IMPORT: "dynamic-import",
|
|
9321
|
+
EXPORT: "export",
|
|
9322
|
+
EXOTIC_REQUIRE: "exotic-require",
|
|
9323
|
+
IMPORT: "import",
|
|
9324
|
+
IMPORT_EQUALS: "import-equals",
|
|
9325
|
+
LOCAL: "local",
|
|
9326
|
+
PRE_COMPILATION_ONLY: "pre-compilation-only",
|
|
9327
|
+
REQUIRE: "require",
|
|
9328
|
+
TYPE_IMPORT: "type-import",
|
|
9329
|
+
// dependency-cruiser 16.10.4 emits this for TypeScript `import type` edges.
|
|
9330
|
+
TYPE_ONLY: "type-only"
|
|
9331
|
+
};
|
|
9332
|
+
var DEPENDENCY_CRUISER_TS_PRE_COMPILATION_DEPS = "specify";
|
|
9333
|
+
var ERASURE_ONLY_DEPENDENCY_TYPES = /* @__PURE__ */ new Set([
|
|
9334
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.PRE_COMPILATION_ONLY,
|
|
9335
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.TYPE_IMPORT,
|
|
9336
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.TYPE_ONLY
|
|
9337
|
+
]);
|
|
9338
|
+
var RUNTIME_DEPENDENCY_TYPES = /* @__PURE__ */ new Set([
|
|
9339
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.AMD_DEFINE,
|
|
9340
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.AMD_EXOTIC_REQUIRE,
|
|
9341
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.AMD_REQUIRE,
|
|
9342
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.DYNAMIC_IMPORT,
|
|
9343
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.EXPORT,
|
|
9344
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.EXOTIC_REQUIRE,
|
|
9345
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.IMPORT,
|
|
9346
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.IMPORT_EQUALS,
|
|
9347
|
+
DEPENDENCY_CRUISER_DEPENDENCY_TYPES.REQUIRE
|
|
9348
|
+
]);
|
|
8387
9349
|
var CIRCULAR_DEPS_KEYS = {
|
|
8388
|
-
|
|
9350
|
+
DEPENDENCY_CRUISER: "dependencyCruiser",
|
|
9351
|
+
EXTRACT_TYPESCRIPT_CONFIG: "extractTypeScriptConfig"
|
|
8389
9352
|
};
|
|
8390
9353
|
var defaultCircularDeps = {
|
|
8391
|
-
[CIRCULAR_DEPS_KEYS.
|
|
9354
|
+
[CIRCULAR_DEPS_KEYS.DEPENDENCY_CRUISER]: dependencyCruiser,
|
|
9355
|
+
[CIRCULAR_DEPS_KEYS.EXTRACT_TYPESCRIPT_CONFIG]: extractTypeScriptConfig
|
|
8392
9356
|
};
|
|
9357
|
+
function toDependencyCruiserExcludePatterns(patterns) {
|
|
9358
|
+
return patterns.map((pattern) => {
|
|
9359
|
+
const cleanPattern = pattern.replace(TSCONFIG_EXCLUDE_SUFFIX_PATTERN, "");
|
|
9360
|
+
return cleanPattern.replace(REGEX_SPECIAL_CHARACTER_PATTERN, REGEX_ESCAPE_REPLACEMENT);
|
|
9361
|
+
});
|
|
9362
|
+
}
|
|
9363
|
+
function buildDependencyCruiserOptions(typescriptScope, projectRoot, tsConfigFile) {
|
|
9364
|
+
const excludePatterns = [
|
|
9365
|
+
DEPENDENCY_CRUISER_PACKAGE_EXCLUDE_PATTERN,
|
|
9366
|
+
...toDependencyCruiserExcludePatterns(typescriptScope.excludePatterns)
|
|
9367
|
+
];
|
|
9368
|
+
return {
|
|
9369
|
+
baseDir: projectRoot,
|
|
9370
|
+
enhancedResolveOptions: { extensions: [...DEPENDENCY_CRUISER_TYPESCRIPT_RESOLVE_EXTENSIONS] },
|
|
9371
|
+
exclude: { path: excludePatterns },
|
|
9372
|
+
includeOnly: { path: DEPENDENCY_CRUISER_TYPESCRIPT_SOURCE_PATTERN },
|
|
9373
|
+
moduleSystems: [...DEPENDENCY_CRUISER_MODULE_SYSTEMS],
|
|
9374
|
+
tsConfig: { fileName: tsConfigFile },
|
|
9375
|
+
tsPreCompilationDeps: DEPENDENCY_CRUISER_TS_PRE_COMPILATION_DEPS
|
|
9376
|
+
};
|
|
9377
|
+
}
|
|
9378
|
+
function toDependencyCruiserSourcePatterns(directories) {
|
|
9379
|
+
return directories.flatMap(
|
|
9380
|
+
(directory) => DEPENDENCY_CRUISER_TYPESCRIPT_SOURCE_GLOB_SUFFIXES.map((suffix) => `${directory}/${suffix}`)
|
|
9381
|
+
);
|
|
9382
|
+
}
|
|
9383
|
+
function isCruiseResult(output) {
|
|
9384
|
+
return typeof output === "object" && output !== null && "modules" in output && "summary" in output;
|
|
9385
|
+
}
|
|
9386
|
+
function closeCycle(cycle) {
|
|
9387
|
+
const first = cycle[0];
|
|
9388
|
+
const last = cycle.at(-1);
|
|
9389
|
+
if (first === void 0) {
|
|
9390
|
+
return [];
|
|
9391
|
+
}
|
|
9392
|
+
return last === first ? [...cycle] : [...cycle, first];
|
|
9393
|
+
}
|
|
9394
|
+
function dependencyTypesSurviveRuntime(dependencyTypes) {
|
|
9395
|
+
if (dependencyTypes.includes(DEPENDENCY_CRUISER_DEPENDENCY_TYPES.PRE_COMPILATION_ONLY)) {
|
|
9396
|
+
return false;
|
|
9397
|
+
}
|
|
9398
|
+
if (dependencyTypes.some((dependencyType) => RUNTIME_DEPENDENCY_TYPES.has(dependencyType))) {
|
|
9399
|
+
return true;
|
|
9400
|
+
}
|
|
9401
|
+
return dependencyTypes.every((dependencyType) => !ERASURE_ONLY_DEPENDENCY_TYPES.has(dependencyType));
|
|
9402
|
+
}
|
|
9403
|
+
function dependencySurvivesRuntime(dependency) {
|
|
9404
|
+
return !dependency.typeOnly && !dependency.preCompilationOnly;
|
|
9405
|
+
}
|
|
9406
|
+
function dependencyCycleSurvivesRuntime(dependency) {
|
|
9407
|
+
return dependencySurvivesRuntime(dependency) && (dependency.cycle?.every((cycleDependency) => dependencyTypesSurviveRuntime(cycleDependency.dependencyTypes)) ?? true);
|
|
9408
|
+
}
|
|
9409
|
+
function dependencyCycleForModule(moduleSource, dependency) {
|
|
9410
|
+
if (!dependency.circular || !dependencyCycleSurvivesRuntime(dependency)) {
|
|
9411
|
+
return null;
|
|
9412
|
+
}
|
|
9413
|
+
const cycleTail = dependency.cycle?.map((cycleDependency) => cycleDependency.name) ?? [dependency.resolved];
|
|
9414
|
+
return closeCycle([moduleSource, ...cycleTail]);
|
|
9415
|
+
}
|
|
9416
|
+
function openCycle(cycle) {
|
|
9417
|
+
if (cycle.length > 1 && cycle[0] === cycle.at(-1)) {
|
|
9418
|
+
return cycle.slice(0, -1);
|
|
9419
|
+
}
|
|
9420
|
+
return [...cycle];
|
|
9421
|
+
}
|
|
9422
|
+
function cycleRotations(cycle) {
|
|
9423
|
+
return cycle.map(
|
|
9424
|
+
(_, index) => [
|
|
9425
|
+
...cycle.slice(index),
|
|
9426
|
+
...cycle.slice(0, index)
|
|
9427
|
+
].join(CYCLE_KEY_SEPARATOR)
|
|
9428
|
+
);
|
|
9429
|
+
}
|
|
9430
|
+
function canonicalCycleKey(cycle) {
|
|
9431
|
+
const opened = openCycle(cycle);
|
|
9432
|
+
const reversed = [...opened].reverse();
|
|
9433
|
+
const keys = [...cycleRotations(opened), ...cycleRotations(reversed)].sort(
|
|
9434
|
+
(left, right) => left.localeCompare(right)
|
|
9435
|
+
);
|
|
9436
|
+
return keys[0] ?? "";
|
|
9437
|
+
}
|
|
9438
|
+
function uniqueCycles(cycles) {
|
|
9439
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9440
|
+
const result = [];
|
|
9441
|
+
for (const cycle of cycles) {
|
|
9442
|
+
const key = canonicalCycleKey(cycle);
|
|
9443
|
+
if (!seen.has(key)) {
|
|
9444
|
+
seen.add(key);
|
|
9445
|
+
result.push(cycle);
|
|
9446
|
+
}
|
|
9447
|
+
}
|
|
9448
|
+
return result;
|
|
9449
|
+
}
|
|
9450
|
+
function circularDependencyCycles(result) {
|
|
9451
|
+
const cycles = result.modules.flatMap(
|
|
9452
|
+
(module) => module.dependencies.flatMap((dependency) => {
|
|
9453
|
+
const cycle = dependencyCycleForModule(module.source, dependency);
|
|
9454
|
+
return cycle === null ? [] : [cycle];
|
|
9455
|
+
})
|
|
9456
|
+
);
|
|
9457
|
+
return uniqueCycles(cycles);
|
|
9458
|
+
}
|
|
8393
9459
|
async function validateCircularDependencies(scope, typescriptScope, projectRoot, deps = defaultCircularDeps) {
|
|
8394
9460
|
try {
|
|
8395
|
-
const
|
|
8396
|
-
if (
|
|
9461
|
+
const analyzeSourcePatterns = toDependencyCruiserSourcePatterns(typescriptScope.directories);
|
|
9462
|
+
if (analyzeSourcePatterns.length === 0) {
|
|
8397
9463
|
return { success: true };
|
|
8398
9464
|
}
|
|
8399
|
-
const tsConfigFile =
|
|
8400
|
-
const
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
});
|
|
8411
|
-
const circular = result.circular();
|
|
9465
|
+
const tsConfigFile = join24(projectRoot, TSCONFIG_FILES[scope]);
|
|
9466
|
+
const result = await deps.dependencyCruiser(
|
|
9467
|
+
analyzeSourcePatterns,
|
|
9468
|
+
buildDependencyCruiserOptions(typescriptScope, projectRoot, tsConfigFile),
|
|
9469
|
+
void 0,
|
|
9470
|
+
{ tsConfig: deps.extractTypeScriptConfig(tsConfigFile) }
|
|
9471
|
+
);
|
|
9472
|
+
if (!isCruiseResult(result.output)) {
|
|
9473
|
+
return { success: false, error: "dependency-cruiser returned non-structured output" };
|
|
9474
|
+
}
|
|
9475
|
+
const circular = circularDependencyCycles(result.output);
|
|
8412
9476
|
if (circular.length === 0) {
|
|
8413
9477
|
return { success: true };
|
|
8414
9478
|
} else {
|
|
@@ -8441,7 +9505,7 @@ async function circularCommand(options) {
|
|
|
8441
9505
|
durationMs: Date.now() - startTime
|
|
8442
9506
|
};
|
|
8443
9507
|
}
|
|
8444
|
-
const toolResult = await discoverTool("
|
|
9508
|
+
const toolResult = await discoverTool("dependency-cruiser", { projectRoot: cwd });
|
|
8445
9509
|
if (!toolResult.found) {
|
|
8446
9510
|
const skipMessage = formatSkipMessage("circular dependency check", toolResult);
|
|
8447
9511
|
return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
|
|
@@ -8479,7 +9543,7 @@ ${cycles}`;
|
|
|
8479
9543
|
import { existsSync as existsSync3 } from "fs";
|
|
8480
9544
|
import { mkdtemp, rm, writeFile as writeFile3 } from "fs/promises";
|
|
8481
9545
|
import { tmpdir } from "os";
|
|
8482
|
-
import { isAbsolute as isAbsolute4, join as
|
|
9546
|
+
import { isAbsolute as isAbsolute4, join as join25 } from "path";
|
|
8483
9547
|
var defaultKnipProcessRunner = lifecycleProcessRunner;
|
|
8484
9548
|
var KNIP_COMMAND_TOKENS = {
|
|
8485
9549
|
COMMAND: "knip",
|
|
@@ -8507,7 +9571,7 @@ async function validateKnip2(context, runner = defaultKnipProcessRunner, deps =
|
|
|
8507
9571
|
}
|
|
8508
9572
|
async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
8509
9573
|
const scopedTsconfig = typescriptScope.filteredByValidationPaths ? await createScopedKnipTsconfig(projectRoot, typescriptScope, deps) : void 0;
|
|
8510
|
-
const localBin =
|
|
9574
|
+
const localBin = join25(projectRoot, "node_modules", ".bin", "knip");
|
|
8511
9575
|
const binary = deps.existsSync(localBin) ? localBin : "npx";
|
|
8512
9576
|
const baseArgs = scopedTsconfig === void 0 ? [] : [
|
|
8513
9577
|
KNIP_COMMAND_TOKENS.USE_TSCONFIG_FILES_FLAG,
|
|
@@ -8562,12 +9626,12 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
|
8562
9626
|
});
|
|
8563
9627
|
}
|
|
8564
9628
|
async function createScopedKnipTsconfig(projectRoot, typescriptScope, deps) {
|
|
8565
|
-
const tempDir = await deps.mkdtemp(
|
|
8566
|
-
const configPath =
|
|
8567
|
-
const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern :
|
|
9629
|
+
const tempDir = await deps.mkdtemp(join25(tmpdir(), "validate-knip-"));
|
|
9630
|
+
const configPath = join25(tempDir, TSCONFIG_FILES.full);
|
|
9631
|
+
const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join25(projectRoot, pattern);
|
|
8568
9632
|
const project = typescriptScope.filePatterns.length > 0 ? typescriptScope.filePatterns : typescriptScope.directories.map((directory) => `${directory}/**/*.{js,ts,tsx}`);
|
|
8569
9633
|
const config = {
|
|
8570
|
-
extends:
|
|
9634
|
+
extends: join25(projectRoot, TSCONFIG_FILES.full),
|
|
8571
9635
|
include: project.map(toProjectPathPattern),
|
|
8572
9636
|
exclude: typescriptScope.excludePatterns.map(toProjectPathPattern)
|
|
8573
9637
|
};
|
|
@@ -8617,12 +9681,12 @@ async function knipCommand(options) {
|
|
|
8617
9681
|
|
|
8618
9682
|
// src/validation/steps/eslint.ts
|
|
8619
9683
|
import { existsSync as existsSync5 } from "fs";
|
|
8620
|
-
import { join as
|
|
9684
|
+
import { join as join27 } from "path";
|
|
8621
9685
|
|
|
8622
9686
|
// src/validation/lint-policy.ts
|
|
8623
9687
|
import { execFileSync } from "child_process";
|
|
8624
9688
|
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
8625
|
-
import { join as
|
|
9689
|
+
import { join as join26 } from "path";
|
|
8626
9690
|
|
|
8627
9691
|
// src/validation/lint-policy-constants.ts
|
|
8628
9692
|
var LINT_POLICY_MANIFESTS = {
|
|
@@ -8670,17 +9734,17 @@ function isSpecTreeNodePath(entry) {
|
|
|
8670
9734
|
}
|
|
8671
9735
|
function readManifest(productDir, file, key) {
|
|
8672
9736
|
return parseLintPolicyManifest(
|
|
8673
|
-
readFileSync4(
|
|
9737
|
+
readFileSync4(join26(productDir, file), "utf-8"),
|
|
8674
9738
|
file,
|
|
8675
9739
|
key
|
|
8676
9740
|
);
|
|
8677
9741
|
}
|
|
8678
9742
|
function manifestExists(productDir, file) {
|
|
8679
|
-
return existsSync4(
|
|
9743
|
+
return existsSync4(join26(productDir, file));
|
|
8680
9744
|
}
|
|
8681
9745
|
function findDeprecatedSpecNodePath(productDir) {
|
|
8682
9746
|
function visit2(relativeDirectory) {
|
|
8683
|
-
const absoluteDirectory =
|
|
9747
|
+
const absoluteDirectory = join26(productDir, relativeDirectory);
|
|
8684
9748
|
for (const entry of readdirSync2(absoluteDirectory, { withFileTypes: true })) {
|
|
8685
9749
|
if (!entry.isDirectory()) {
|
|
8686
9750
|
continue;
|
|
@@ -8696,7 +9760,7 @@ function findDeprecatedSpecNodePath(productDir) {
|
|
|
8696
9760
|
}
|
|
8697
9761
|
return void 0;
|
|
8698
9762
|
}
|
|
8699
|
-
const specTreeRootPath =
|
|
9763
|
+
const specTreeRootPath = join26(productDir, SPEC_TREE_ROOT);
|
|
8700
9764
|
if (!existsSync4(specTreeRootPath)) {
|
|
8701
9765
|
return void 0;
|
|
8702
9766
|
}
|
|
@@ -8722,7 +9786,7 @@ function assertManifestEntries(productDir, file, entries, suffixPredicate, suffi
|
|
|
8722
9786
|
if (!suffixPredicate(entry)) {
|
|
8723
9787
|
throw new Error(`${file} entry must ${suffixDescription}: ${entry}`);
|
|
8724
9788
|
}
|
|
8725
|
-
const absoluteEntry =
|
|
9789
|
+
const absoluteEntry = join26(productDir, entry);
|
|
8726
9790
|
if (!existsSync4(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
|
|
8727
9791
|
throw new Error(`${file} entry does not exist as a directory: ${entry}`);
|
|
8728
9792
|
}
|
|
@@ -8982,7 +10046,7 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
|
|
|
8982
10046
|
scopeConfig: context.scopeConfig
|
|
8983
10047
|
});
|
|
8984
10048
|
return new Promise((resolve6) => {
|
|
8985
|
-
const localBin =
|
|
10049
|
+
const localBin = join27(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
|
|
8986
10050
|
const binary = existsSync5(localBin) ? localBin : "npx";
|
|
8987
10051
|
const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
|
|
8988
10052
|
const eslintProcess = spawnManagedSubprocess(runner, binary, spawnArgs, {
|
|
@@ -9084,7 +10148,7 @@ async function lintCommand(options) {
|
|
|
9084
10148
|
|
|
9085
10149
|
// src/validation/literal/index.ts
|
|
9086
10150
|
import { readFile as readFile8 } from "fs/promises";
|
|
9087
|
-
import { isAbsolute as isAbsolute5, relative as relative5, resolve as
|
|
10151
|
+
import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve5 } from "path";
|
|
9088
10152
|
|
|
9089
10153
|
// src/lib/file-inclusion/predicates/ignore-source.ts
|
|
9090
10154
|
var IGNORE_SOURCE_LAYER = "ignore-source";
|
|
@@ -9119,7 +10183,7 @@ var ignoreSourceLayer = makeLayer(
|
|
|
9119
10183
|
|
|
9120
10184
|
// src/lib/file-inclusion/pipeline.ts
|
|
9121
10185
|
import { readdir as readdir7 } from "fs/promises";
|
|
9122
|
-
import { join as
|
|
10186
|
+
import { join as join28, relative as relative4, sep as sep2 } from "path";
|
|
9123
10187
|
var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
|
|
9124
10188
|
function isNodeError3(err) {
|
|
9125
10189
|
return err instanceof Error && "code" in err;
|
|
@@ -9137,10 +10201,10 @@ async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
|
|
|
9137
10201
|
for (const entry of dirEntries) {
|
|
9138
10202
|
if (entry.isDirectory()) {
|
|
9139
10203
|
if (artifactDirs.has(entry.name)) continue;
|
|
9140
|
-
const absolutePath =
|
|
10204
|
+
const absolutePath = join28(absoluteDir, entry.name);
|
|
9141
10205
|
await collectPaths(absolutePath, projectRoot, result, artifactDirs);
|
|
9142
10206
|
} else if (entry.isFile()) {
|
|
9143
|
-
const absolutePath =
|
|
10207
|
+
const absolutePath = join28(absoluteDir, entry.name);
|
|
9144
10208
|
const rel = relative4(projectRoot, absolutePath);
|
|
9145
10209
|
result.push(sep2 === "/" ? rel : rel.split(sep2).join("/"));
|
|
9146
10210
|
}
|
|
@@ -9547,7 +10611,7 @@ async function validateLiteralReuse(input) {
|
|
|
9547
10611
|
const config = input.config ?? literalConfigDescriptor.defaults;
|
|
9548
10612
|
const request = input.files ? {
|
|
9549
10613
|
explicit: input.files.map((f) => {
|
|
9550
|
-
const abs = isAbsolute5(f) ? f :
|
|
10614
|
+
const abs = isAbsolute5(f) ? f : resolve5(input.productDir, f);
|
|
9551
10615
|
return relative5(input.productDir, abs).split(/[\\/]/g).join("/");
|
|
9552
10616
|
})
|
|
9553
10617
|
} : { walkRoot: input.productDir };
|
|
@@ -9559,7 +10623,7 @@ async function validateLiteralReuse(input) {
|
|
|
9559
10623
|
EMPTY_IGNORE_READER
|
|
9560
10624
|
);
|
|
9561
10625
|
const filtered = applyPathFilter2(scope.included, input.pathConfig);
|
|
9562
|
-
const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) =>
|
|
10626
|
+
const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve5(input.productDir, entry.path));
|
|
9563
10627
|
const collectOptions = {
|
|
9564
10628
|
visitorKeys: defaultVisitorKeys,
|
|
9565
10629
|
minStringLength: config.minStringLength,
|
|
@@ -9800,7 +10864,7 @@ function formatLoc(loc) {
|
|
|
9800
10864
|
// src/validation/steps/typescript.ts
|
|
9801
10865
|
import { existsSync as existsSync6, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
9802
10866
|
import { mkdtemp as mkdtemp2 } from "fs/promises";
|
|
9803
|
-
import { isAbsolute as isAbsolute6, join as
|
|
10867
|
+
import { isAbsolute as isAbsolute6, join as join29 } from "path";
|
|
9804
10868
|
var defaultTypeScriptProcessRunner = lifecycleProcessRunner;
|
|
9805
10869
|
var defaultTypeScriptDeps = {
|
|
9806
10870
|
mkdtemp: mkdtemp2,
|
|
@@ -9813,9 +10877,9 @@ var TEMPORARY_TSCONFIG_COMPILER_OPTIONS = { noEmit: true };
|
|
|
9813
10877
|
var TEMPORARY_TSCONFIG_PARENT_SEGMENTS = ["node_modules", ".cache", "spx"];
|
|
9814
10878
|
var TEMPORARY_TSCONFIG_DIR_PREFIX = "validate-ts-";
|
|
9815
10879
|
async function createTemporaryTsconfigDir(projectRoot, deps) {
|
|
9816
|
-
const parent =
|
|
10880
|
+
const parent = join29(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
|
|
9817
10881
|
deps.mkdirSync(parent, { recursive: true });
|
|
9818
|
-
return deps.mkdtemp(
|
|
10882
|
+
return deps.mkdtemp(join29(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
|
|
9819
10883
|
}
|
|
9820
10884
|
function buildTypeScriptArgs(context) {
|
|
9821
10885
|
const { scope, configFile } = context;
|
|
@@ -9823,11 +10887,11 @@ function buildTypeScriptArgs(context) {
|
|
|
9823
10887
|
}
|
|
9824
10888
|
async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defaultTypeScriptDeps) {
|
|
9825
10889
|
const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
|
|
9826
|
-
const configPath =
|
|
10890
|
+
const configPath = join29(tempDir, "tsconfig.json");
|
|
9827
10891
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
9828
|
-
const absoluteFiles = files.map((file) => isAbsolute6(file) ? file :
|
|
10892
|
+
const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join29(projectRoot, file));
|
|
9829
10893
|
const tempConfig = {
|
|
9830
|
-
extends:
|
|
10894
|
+
extends: join29(projectRoot, baseConfigFile),
|
|
9831
10895
|
files: absoluteFiles,
|
|
9832
10896
|
include: [],
|
|
9833
10897
|
exclude: [],
|
|
@@ -9844,11 +10908,11 @@ async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defa
|
|
|
9844
10908
|
}
|
|
9845
10909
|
async function createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps = defaultTypeScriptDeps) {
|
|
9846
10910
|
const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
|
|
9847
|
-
const configPath =
|
|
10911
|
+
const configPath = join29(tempDir, "tsconfig.json");
|
|
9848
10912
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
9849
|
-
const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern :
|
|
10913
|
+
const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join29(projectRoot, pattern);
|
|
9850
10914
|
const tempConfig = {
|
|
9851
|
-
extends:
|
|
10915
|
+
extends: join29(projectRoot, baseConfigFile),
|
|
9852
10916
|
include: scopeConfig.filePatterns.map(toProjectPathPattern),
|
|
9853
10917
|
exclude: scopeConfig.excludePatterns.map(toProjectPathPattern),
|
|
9854
10918
|
compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
|
|
@@ -9876,7 +10940,7 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9876
10940
|
const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, projectRoot, deps);
|
|
9877
10941
|
try {
|
|
9878
10942
|
return await new Promise((resolve6) => {
|
|
9879
|
-
const tscBin =
|
|
10943
|
+
const tscBin = join29(projectRoot, "node_modules", ".bin", "tsc");
|
|
9880
10944
|
const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9881
10945
|
const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
|
|
9882
10946
|
const tscProcess = spawnManagedSubprocess(runner, tscBinary, tscArgs2, {
|
|
@@ -9909,7 +10973,7 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9909
10973
|
return { success: true, skipped: true };
|
|
9910
10974
|
}
|
|
9911
10975
|
const { configPath, cleanup } = await createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps);
|
|
9912
|
-
const tscBin =
|
|
10976
|
+
const tscBin = join29(projectRoot, "node_modules", ".bin", "tsc");
|
|
9913
10977
|
tool = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9914
10978
|
tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
|
|
9915
10979
|
return new Promise((resolve6) => {
|
|
@@ -9931,7 +10995,7 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9931
10995
|
});
|
|
9932
10996
|
});
|
|
9933
10997
|
} else {
|
|
9934
|
-
const tscBin =
|
|
10998
|
+
const tscBin = join29(projectRoot, "node_modules", ".bin", "tsc");
|
|
9935
10999
|
tool = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9936
11000
|
const rawArgs = buildTypeScriptArgs({ scope, configFile });
|
|
9937
11001
|
tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
|
|
@@ -10024,6 +11088,13 @@ async function typescriptCommand(options) {
|
|
|
10024
11088
|
|
|
10025
11089
|
// src/validation/languages/typescript.ts
|
|
10026
11090
|
var TYPESCRIPT_LANGUAGE_NAME = "typescript";
|
|
11091
|
+
async function runCircularStage(context) {
|
|
11092
|
+
if (context.skipCircular) {
|
|
11093
|
+
const skipOutput = context.json ? CIRCULAR_SKIP_JSON_OUTPUT : CIRCULAR_SKIP_OUTPUT;
|
|
11094
|
+
return { exitCode: 0, output: context.quiet ? "" : skipOutput };
|
|
11095
|
+
}
|
|
11096
|
+
return circularCommand({ cwd: context.cwd, quiet: context.quiet, json: context.json });
|
|
11097
|
+
}
|
|
10027
11098
|
async function runLiteralStage(context) {
|
|
10028
11099
|
if (context.skipLiteral) {
|
|
10029
11100
|
const skipOutput = context.json ? LITERAL_SKIP_JSON_OUTPUT : LITERAL_SKIP_OUTPUT;
|
|
@@ -10042,7 +11113,7 @@ var typescriptValidationLanguage = {
|
|
|
10042
11113
|
{
|
|
10043
11114
|
name: VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR,
|
|
10044
11115
|
failsPipeline: true,
|
|
10045
|
-
run:
|
|
11116
|
+
run: runCircularStage
|
|
10046
11117
|
},
|
|
10047
11118
|
{
|
|
10048
11119
|
name: VALIDATION_STAGE_DISPLAY_NAMES.KNIP,
|
|
@@ -10122,11 +11193,11 @@ function formatStepWithTiming(stepNumber, result, quiet) {
|
|
|
10122
11193
|
return `[${stepNumber}/${VALIDATION_PIPELINE_TOTAL_STEPS}] ${result.output}${timing}`;
|
|
10123
11194
|
}
|
|
10124
11195
|
async function allCommand(options) {
|
|
10125
|
-
const { cwd, scope, files, fix, quiet = false, json, skipLiteral = false } = options;
|
|
11196
|
+
const { cwd, scope, files, fix, quiet = false, json, skipCircular = false, skipLiteral = false } = options;
|
|
10126
11197
|
const startTime = Date.now();
|
|
10127
11198
|
const outputs = [];
|
|
10128
11199
|
let hasFailure = false;
|
|
10129
|
-
const context = { cwd, scope, files, fix, quiet, json, skipLiteral };
|
|
11200
|
+
const context = { cwd, scope, files, fix, quiet, json, skipCircular, skipLiteral };
|
|
10130
11201
|
let stepNumber = 0;
|
|
10131
11202
|
for (const stage of validationPipelineStages) {
|
|
10132
11203
|
stepNumber += 1;
|
|
@@ -10139,59 +11210,17 @@ async function allCommand(options) {
|
|
|
10139
11210
|
if (!quiet) {
|
|
10140
11211
|
const summary = formatSummary({ success: !hasFailure, totalDurationMs });
|
|
10141
11212
|
outputs.push("", summary);
|
|
10142
|
-
}
|
|
10143
|
-
return {
|
|
10144
|
-
exitCode: hasFailure ? 1 : 0,
|
|
10145
|
-
output: outputs.join("\n"),
|
|
10146
|
-
durationMs: totalDurationMs
|
|
10147
|
-
};
|
|
10148
|
-
}
|
|
10149
|
-
|
|
10150
|
-
// src/interfaces/cli/sanitize.ts
|
|
10151
|
-
var MAX_CLI_ARGUMENT_DISPLAY_LENGTH = 120;
|
|
10152
|
-
var ELLIPSIS_TOKEN = "...";
|
|
10153
|
-
var SENTINEL_UNDEFINED = "<undefined>";
|
|
10154
|
-
var SENTINEL_NULL = "<null>";
|
|
10155
|
-
var SENTINEL_EMPTY = "<empty>";
|
|
10156
|
-
var CONTROL_CHAR_UPPER_BOUND = 31;
|
|
10157
|
-
var DEL_CHAR_CODE = 127;
|
|
10158
|
-
var HEX_RADIX = 16;
|
|
10159
|
-
var HEX_PAD = 2;
|
|
10160
|
-
function formatHexEscape(code) {
|
|
10161
|
-
return `\\x${code.toString(HEX_RADIX).padStart(HEX_PAD, "0")}`;
|
|
10162
|
-
}
|
|
10163
|
-
function nonStringSentinel(type) {
|
|
10164
|
-
return `<non-string:${type}>`;
|
|
10165
|
-
}
|
|
10166
|
-
function sanitizeCliArgument(input) {
|
|
10167
|
-
if (input === void 0) return SENTINEL_UNDEFINED;
|
|
10168
|
-
if (input === null) return SENTINEL_NULL;
|
|
10169
|
-
if (typeof input !== "string") return nonStringSentinel(typeof input);
|
|
10170
|
-
if (input.length === 0) return SENTINEL_EMPTY;
|
|
10171
|
-
const escaped = escapeControlCharacters(input);
|
|
10172
|
-
return truncate(escaped);
|
|
10173
|
-
}
|
|
10174
|
-
function escapeControlCharacters(value) {
|
|
10175
|
-
let out = "";
|
|
10176
|
-
for (const char of value) {
|
|
10177
|
-
const code = char.codePointAt(0);
|
|
10178
|
-
if (code === void 0) continue;
|
|
10179
|
-
if (code <= CONTROL_CHAR_UPPER_BOUND || code === DEL_CHAR_CODE) {
|
|
10180
|
-
out += formatHexEscape(code);
|
|
10181
|
-
} else {
|
|
10182
|
-
out += char;
|
|
10183
|
-
}
|
|
10184
|
-
}
|
|
10185
|
-
return out;
|
|
10186
|
-
}
|
|
10187
|
-
function truncate(value) {
|
|
10188
|
-
if (value.length <= MAX_CLI_ARGUMENT_DISPLAY_LENGTH) return value;
|
|
10189
|
-
return value.slice(0, MAX_CLI_ARGUMENT_DISPLAY_LENGTH - ELLIPSIS_TOKEN.length) + ELLIPSIS_TOKEN;
|
|
11213
|
+
}
|
|
11214
|
+
return {
|
|
11215
|
+
exitCode: hasFailure ? 1 : 0,
|
|
11216
|
+
output: outputs.join("\n"),
|
|
11217
|
+
durationMs: totalDurationMs
|
|
11218
|
+
};
|
|
10190
11219
|
}
|
|
10191
11220
|
|
|
10192
11221
|
// src/validation/literal/allowlist-existing.ts
|
|
10193
11222
|
import { rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
10194
|
-
import { dirname as
|
|
11223
|
+
import { dirname as dirname8, join as join30 } from "path";
|
|
10195
11224
|
var EXIT_OK = 0;
|
|
10196
11225
|
var EXIT_ERROR = 1;
|
|
10197
11226
|
var INCLUDE_FIELD = "include";
|
|
@@ -10211,9 +11240,9 @@ var productionReader = {
|
|
|
10211
11240
|
};
|
|
10212
11241
|
var productionWriter = {
|
|
10213
11242
|
async write(filePath, content) {
|
|
10214
|
-
const dir =
|
|
11243
|
+
const dir = dirname8(filePath);
|
|
10215
11244
|
const random = Math.random().toString(RANDOM_BASE).slice(RANDOM_PREFIX_SLICE, RANDOM_PREFIX_SLICE + RANDOM_TOKEN_LENGTH);
|
|
10216
|
-
const tmpPath =
|
|
11245
|
+
const tmpPath = join30(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
|
|
10217
11246
|
await writeFile4(tmpPath, content, "utf8");
|
|
10218
11247
|
await rename4(tmpPath, filePath);
|
|
10219
11248
|
}
|
|
@@ -10363,6 +11392,10 @@ var literalValidationCliOptions = {
|
|
|
10363
11392
|
}
|
|
10364
11393
|
};
|
|
10365
11394
|
var allValidationCliOptions = {
|
|
11395
|
+
skipCircular: {
|
|
11396
|
+
flag: "--skip-circular",
|
|
11397
|
+
description: "Skip circular dependency detection for this validation all run"
|
|
11398
|
+
},
|
|
10366
11399
|
skipLiteral: {
|
|
10367
11400
|
flag: "--skip-literal",
|
|
10368
11401
|
description: "Skip literal reuse detection for this validation all run"
|
|
@@ -10491,12 +11524,13 @@ function registerValidationCommands(validationCmd) {
|
|
|
10491
11524
|
process.exit(result.exitCode);
|
|
10492
11525
|
});
|
|
10493
11526
|
addCommonOptions(markdownCmd);
|
|
10494
|
-
const allCmd = addValidationSubcommand(validationCmd, subcommands.all).option("--fix", "Auto-fix ESLint issues").option(allValidationCliOptions.skipLiteral.flag, allValidationCliOptions.skipLiteral.description).action(async (options) => {
|
|
11527
|
+
const allCmd = addValidationSubcommand(validationCmd, subcommands.all).option("--fix", "Auto-fix ESLint issues").option(allValidationCliOptions.skipCircular.flag, allValidationCliOptions.skipCircular.description).option(allValidationCliOptions.skipLiteral.flag, allValidationCliOptions.skipLiteral.description).action(async (options) => {
|
|
10495
11528
|
const result = await allCommand({
|
|
10496
11529
|
cwd: process.cwd(),
|
|
10497
11530
|
scope: options.scope,
|
|
10498
11531
|
files: options.files,
|
|
10499
11532
|
fix: options.fix,
|
|
11533
|
+
skipCircular: options.skipCircular,
|
|
10500
11534
|
skipLiteral: options.skipLiteral,
|
|
10501
11535
|
quiet: options.quiet,
|
|
10502
11536
|
json: options.json
|
|
@@ -10534,284 +11568,9 @@ var validationDomain = {
|
|
|
10534
11568
|
}
|
|
10535
11569
|
};
|
|
10536
11570
|
|
|
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
11571
|
// src/commands/worktree/claim.ts
|
|
10798
11572
|
async function claimCommand(options) {
|
|
10799
|
-
|
|
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
|
-
);
|
|
11573
|
+
return claimWorktreeOccupancy(options);
|
|
10815
11574
|
}
|
|
10816
11575
|
|
|
10817
11576
|
// src/commands/worktree/release.ts
|
|
@@ -10827,14 +11586,13 @@ var WORKTREE_STATUS_FORMAT = {
|
|
|
10827
11586
|
TEXT: "text"
|
|
10828
11587
|
};
|
|
10829
11588
|
async function statusCommand2(options) {
|
|
10830
|
-
const table = options.processTable ?? defaultProcessTable;
|
|
10831
11589
|
const multiTargetRequest = options.worktrees !== void 0 && options.worktrees.length > 1;
|
|
10832
11590
|
const targets = await resolveStatusTargets(options);
|
|
10833
11591
|
if (!targets.ok) return targets;
|
|
10834
11592
|
const records = [];
|
|
10835
11593
|
for (const target of targets.value) {
|
|
10836
11594
|
const worktreesDir = await resolveWorktreesDir({ ...options, cwd: target.worktreeRoot });
|
|
10837
|
-
const occupancy = await readOccupancy(worktreesDir, target.name,
|
|
11595
|
+
const occupancy = await readOccupancy(worktreesDir, target.name, options.processTable, { fs: options.fs });
|
|
10838
11596
|
if (!occupancy.ok) return occupancy;
|
|
10839
11597
|
records.push({ worktree: target.name, status: occupancy.value });
|
|
10840
11598
|
}
|
|
@@ -10872,6 +11630,19 @@ function renderTextStatus(record) {
|
|
|
10872
11630
|
return `${record.worktree} ${record.status}`;
|
|
10873
11631
|
}
|
|
10874
11632
|
|
|
11633
|
+
// src/lib/worktree-path-info.ts
|
|
11634
|
+
import { stat as stat4 } from "fs/promises";
|
|
11635
|
+
var defaultWorktreePathInfo = {
|
|
11636
|
+
isExistingNonDirectory: async (path6) => {
|
|
11637
|
+
try {
|
|
11638
|
+
const pathStats = await stat4(path6);
|
|
11639
|
+
return !pathStats.isDirectory();
|
|
11640
|
+
} catch {
|
|
11641
|
+
return false;
|
|
11642
|
+
}
|
|
11643
|
+
}
|
|
11644
|
+
};
|
|
11645
|
+
|
|
10875
11646
|
// src/interfaces/cli/worktree.ts
|
|
10876
11647
|
var WORKTREE_CLI = {
|
|
10877
11648
|
COMMAND: "worktree",
|
|
@@ -10891,6 +11662,13 @@ function handleError2(error) {
|
|
|
10891
11662
|
function registerWorktreeCommands(worktreeCmd) {
|
|
10892
11663
|
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
11664
|
const result = await claimCommand({
|
|
11665
|
+
claimWriteToken: createClaimWriteToken(),
|
|
11666
|
+
cwd: process.cwd(),
|
|
11667
|
+
env: process.env,
|
|
11668
|
+
fs: defaultOccupancyFileSystem,
|
|
11669
|
+
gitDeps: defaultGitDependencies,
|
|
11670
|
+
processTable: defaultProcessTable,
|
|
11671
|
+
selfPid: process.pid,
|
|
10894
11672
|
sessionId: options.sessionId,
|
|
10895
11673
|
worktreesDir: options.worktreesDir,
|
|
10896
11674
|
onWarning: writeWarning
|
|
@@ -10899,8 +11677,13 @@ function registerWorktreeCommands(worktreeCmd) {
|
|
|
10899
11677
|
});
|
|
10900
11678
|
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
11679
|
const result = await statusCommand2({
|
|
11680
|
+
cwd: process.cwd(),
|
|
11681
|
+
fs: defaultOccupancyFileSystem,
|
|
11682
|
+
gitDeps: defaultGitDependencies,
|
|
10902
11683
|
worktrees,
|
|
10903
11684
|
format: options.format,
|
|
11685
|
+
pathInfo: defaultWorktreePathInfo,
|
|
11686
|
+
processTable: defaultProcessTable,
|
|
10904
11687
|
worktreesDir: options.worktreesDir,
|
|
10905
11688
|
onWarning: writeWarning
|
|
10906
11689
|
});
|
|
@@ -10908,7 +11691,13 @@ function registerWorktreeCommands(worktreeCmd) {
|
|
|
10908
11691
|
console.log(result.value);
|
|
10909
11692
|
});
|
|
10910
11693
|
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({
|
|
11694
|
+
const result = await releaseCommand2({
|
|
11695
|
+
cwd: process.cwd(),
|
|
11696
|
+
fs: defaultOccupancyFileSystem,
|
|
11697
|
+
gitDeps: defaultGitDependencies,
|
|
11698
|
+
worktreesDir: options.worktreesDir,
|
|
11699
|
+
onWarning: writeWarning
|
|
11700
|
+
});
|
|
10912
11701
|
if (!result.ok) handleError2(result.error);
|
|
10913
11702
|
});
|
|
10914
11703
|
}
|
|
@@ -10926,6 +11715,7 @@ var CLI_DOMAINS = [
|
|
|
10926
11715
|
claudeDomain,
|
|
10927
11716
|
compactDomain,
|
|
10928
11717
|
configDomain,
|
|
11718
|
+
hookDomain,
|
|
10929
11719
|
sessionDomain,
|
|
10930
11720
|
specDomain,
|
|
10931
11721
|
testingDomain,
|