@outcomeeng/spx 0.5.4 → 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 +1432 -243
- package/dist/cli.js.map +1 -1
- package/package.json +14 -10
package/dist/cli.js
CHANGED
|
@@ -1046,6 +1046,7 @@ var STATE_STORE_PATH = {
|
|
|
1046
1046
|
BRANCH_SCOPE: "branch",
|
|
1047
1047
|
WORKTREE_SCOPE: "worktree",
|
|
1048
1048
|
SESSIONS_SCOPE: "sessions",
|
|
1049
|
+
WORKTREES_SCOPE: "worktrees",
|
|
1049
1050
|
RUNS_DIR: "runs",
|
|
1050
1051
|
RUN_FILE_PREFIX: "run-",
|
|
1051
1052
|
JSONL_EXTENSION: ".jsonl"
|
|
@@ -1065,11 +1066,13 @@ var STATE_STORE_ERROR = {
|
|
|
1065
1066
|
RECORD_WRITE_FAILED: "state-store record write failed",
|
|
1066
1067
|
RECORD_READ_FAILED: "state-store record read failed"
|
|
1067
1068
|
};
|
|
1069
|
+
var STATE_STORE_RUN_TOKEN = {
|
|
1070
|
+
ID_BYTES: 6,
|
|
1071
|
+
TIMESTAMP_MILLISECOND_DIGITS: 3
|
|
1072
|
+
};
|
|
1068
1073
|
var HEX_ENCODING = "hex";
|
|
1069
|
-
var RUN_ID_BYTES = 6;
|
|
1070
1074
|
var RUN_FILE_CREATE_ATTEMPTS = 10;
|
|
1071
1075
|
var RUN_TIMESTAMP_SEPARATOR = "_";
|
|
1072
|
-
var RUN_TIMESTAMP_MILLISECOND_DIGITS = 3;
|
|
1073
1076
|
var SLUG_SEPARATOR = "-";
|
|
1074
1077
|
var EMPTY_STRING = "";
|
|
1075
1078
|
var RUN_TOKEN_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-\d{3}-[a-f0-9]{12}$/;
|
|
@@ -1109,6 +1112,16 @@ function worktreeScopeDir(productDir) {
|
|
|
1109
1112
|
function sessionsScopeDir(productDir) {
|
|
1110
1113
|
return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
|
|
1111
1114
|
}
|
|
1115
|
+
async function resolveWorktreesScopeDir(options = {}) {
|
|
1116
|
+
const gitResult = await detectGitCommonDirProductRoot(options.cwd, options.deps);
|
|
1117
|
+
return {
|
|
1118
|
+
worktreesDir: worktreesScopeDir(gitResult.productDir),
|
|
1119
|
+
warning: gitResult.warning
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function worktreesScopeDir(productDir) {
|
|
1123
|
+
return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.WORKTREES_SCOPE);
|
|
1124
|
+
}
|
|
1112
1125
|
function composeScopeDir(baseScopeDir, ...tokens) {
|
|
1113
1126
|
const segments = [];
|
|
1114
1127
|
for (const token of tokens) {
|
|
@@ -1133,11 +1146,19 @@ function formatRunTimestamp(date) {
|
|
|
1133
1146
|
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
1134
1147
|
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
1135
1148
|
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
1136
|
-
const milliseconds = String(date.getUTCMilliseconds()).padStart(
|
|
1149
|
+
const milliseconds = String(date.getUTCMilliseconds()).padStart(
|
|
1150
|
+
STATE_STORE_RUN_TOKEN.TIMESTAMP_MILLISECOND_DIGITS,
|
|
1151
|
+
"0"
|
|
1152
|
+
);
|
|
1137
1153
|
return `${year}-${month}-${day}${RUN_TIMESTAMP_SEPARATOR}${hours}-${minutes}-${seconds}-${milliseconds}`;
|
|
1138
1154
|
}
|
|
1139
1155
|
function generateRunId(randomBytes = nodeRandomBytes) {
|
|
1140
|
-
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 };
|
|
1141
1162
|
}
|
|
1142
1163
|
function runFileName(runToken) {
|
|
1143
1164
|
return `${STATE_STORE_PATH.RUN_FILE_PREFIX}${runToken}${STATE_STORE_PATH.JSONL_EXTENSION}`;
|
|
@@ -1154,7 +1175,6 @@ async function createJsonlRunFile(scopeDir, domainName, options = {}) {
|
|
|
1154
1175
|
if (!domainRunsDir.ok) return domainRunsDir;
|
|
1155
1176
|
const maxAttempts = options.maxAttempts ?? RUN_FILE_CREATE_ATTEMPTS;
|
|
1156
1177
|
const startedDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1157
|
-
const startedAt = formatRunTimestamp(startedDate);
|
|
1158
1178
|
const randomBytes = options.randomBytes ?? nodeRandomBytes;
|
|
1159
1179
|
try {
|
|
1160
1180
|
await fs7.mkdir(domainRunsDir.value, { recursive: true });
|
|
@@ -1165,15 +1185,14 @@ async function createJsonlRunFile(scopeDir, domainName, options = {}) {
|
|
|
1165
1185
|
};
|
|
1166
1186
|
}
|
|
1167
1187
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1168
|
-
const
|
|
1169
|
-
const
|
|
1170
|
-
const name = runFileName(runToken);
|
|
1188
|
+
const token = createStateStoreRunToken({ date: startedDate, randomBytes });
|
|
1189
|
+
const name = runFileName(token.runToken);
|
|
1171
1190
|
const path6 = join2(domainRunsDir.value, name);
|
|
1172
1191
|
try {
|
|
1173
1192
|
await fs7.writeFile(path6, EMPTY_STRING, { flag: EXCLUSIVE_CREATE_FLAG });
|
|
1174
1193
|
return {
|
|
1175
1194
|
ok: true,
|
|
1176
|
-
value: { runsDir: domainRunsDir.value, runFilePath: path6, runFileName: name,
|
|
1195
|
+
value: { runsDir: domainRunsDir.value, runFilePath: path6, runFileName: name, ...token }
|
|
1177
1196
|
};
|
|
1178
1197
|
} catch (error) {
|
|
1179
1198
|
if (hasErrorCode(error, ERROR_CODE_FILE_EXISTS)) continue;
|
|
@@ -2371,11 +2390,6 @@ function knipAdapter(scope, config) {
|
|
|
2371
2390
|
return scope.excluded.flatMap((entry) => [config.ignoreFlag, entry.path]);
|
|
2372
2391
|
}
|
|
2373
2392
|
|
|
2374
|
-
// src/lib/file-inclusion/adapters/madge.ts
|
|
2375
|
-
function madgeAdapter(scope, config) {
|
|
2376
|
-
return scope.excluded.flatMap((entry) => [config.ignoreFlag, entry.path]);
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
2393
|
// src/lib/file-inclusion/adapters/markdownlint.ts
|
|
2380
2394
|
function markdownlintAdapter(scope, config) {
|
|
2381
2395
|
return scope.excluded.flatMap((entry) => [config.ignoreFlag, entry.path]);
|
|
@@ -2400,7 +2414,6 @@ function vitestAdapter(scope, config) {
|
|
|
2400
2414
|
var TOOL_DEFAULT_FLAGS = {
|
|
2401
2415
|
eslint: "--ignore-pattern",
|
|
2402
2416
|
tsc: "--exclude",
|
|
2403
|
-
madge: "--exclude",
|
|
2404
2417
|
knip: "--exclude",
|
|
2405
2418
|
markdownlint: "--ignore",
|
|
2406
2419
|
pytest: "--ignore",
|
|
@@ -2409,7 +2422,6 @@ var TOOL_DEFAULT_FLAGS = {
|
|
|
2409
2422
|
var ADAPTER_MAP = {
|
|
2410
2423
|
eslint: eslintAdapter,
|
|
2411
2424
|
tsc: tscAdapter,
|
|
2412
|
-
madge: madgeAdapter,
|
|
2413
2425
|
knip: knipAdapter,
|
|
2414
2426
|
markdownlint: markdownlintAdapter,
|
|
2415
2427
|
pytest: pytestAdapter,
|
|
@@ -2516,8 +2528,8 @@ var HIDDEN_PREFIX_LAYER = "hidden-prefix";
|
|
|
2516
2528
|
var LAYER2 = HIDDEN_PREFIX_LAYER;
|
|
2517
2529
|
function hiddenPrefixPredicate(path6, config) {
|
|
2518
2530
|
const segments = path6.split("/");
|
|
2519
|
-
const
|
|
2520
|
-
const matched =
|
|
2531
|
+
const basename5 = segments[segments.length - 1] ?? "";
|
|
2532
|
+
const matched = basename5.startsWith(config.hiddenPrefix);
|
|
2521
2533
|
return { matched, layer: LAYER2 };
|
|
2522
2534
|
}
|
|
2523
2535
|
|
|
@@ -3488,9 +3500,640 @@ var configDomain = {
|
|
|
3488
3500
|
}
|
|
3489
3501
|
};
|
|
3490
3502
|
|
|
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")}`;
|
|
3515
|
+
}
|
|
3516
|
+
function nonStringSentinel(type) {
|
|
3517
|
+
return `<non-string:${type}>`;
|
|
3518
|
+
}
|
|
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;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
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;
|
|
3543
|
+
}
|
|
3544
|
+
|
|
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"
|
|
3554
|
+
};
|
|
3555
|
+
var OCCUPANCY_CLAIM = {
|
|
3556
|
+
FILE_EXTENSION: ".claim",
|
|
3557
|
+
TEMP_EXTENSION: ".tmp",
|
|
3558
|
+
UNREADABLE_STARTED_AT_PREFIX: "unreadable:"
|
|
3559
|
+
};
|
|
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"
|
|
3567
|
+
};
|
|
3568
|
+
var ERROR_DETAIL_SEPARATOR2 = ": ";
|
|
3569
|
+
function claimFileName(name) {
|
|
3570
|
+
return `${name}${OCCUPANCY_CLAIM.FILE_EXTENSION}`;
|
|
3571
|
+
}
|
|
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)) };
|
|
3576
|
+
}
|
|
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}` };
|
|
3581
|
+
}
|
|
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)) };
|
|
3607
|
+
}
|
|
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)) };
|
|
3618
|
+
}
|
|
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)) };
|
|
3629
|
+
}
|
|
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 };
|
|
3650
|
+
}
|
|
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
|
+
|
|
3491
4134
|
// src/commands/session/archive.ts
|
|
3492
4135
|
import { mkdir, rename, stat } from "fs/promises";
|
|
3493
|
-
import { dirname as
|
|
4136
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
3494
4137
|
|
|
3495
4138
|
// src/domains/session/archive.ts
|
|
3496
4139
|
var SESSION_FILE_EXTENSION = ".md";
|
|
@@ -3722,7 +4365,7 @@ var SessionInvalidJsonHeaderError = class extends SessionError {
|
|
|
3722
4365
|
};
|
|
3723
4366
|
|
|
3724
4367
|
// src/commands/session/resolve-config.ts
|
|
3725
|
-
import { join as
|
|
4368
|
+
import { join as join7 } from "path";
|
|
3726
4369
|
|
|
3727
4370
|
// src/config/defaults.ts
|
|
3728
4371
|
var DEFAULT_CONFIG = {
|
|
@@ -3742,18 +4385,18 @@ async function resolveSessionConfig(options = {}) {
|
|
|
3742
4385
|
if (sessionsDir) {
|
|
3743
4386
|
return {
|
|
3744
4387
|
config: {
|
|
3745
|
-
todoDir:
|
|
3746
|
-
doingDir:
|
|
3747
|
-
archiveDir:
|
|
4388
|
+
todoDir: join7(sessionsDir, statusDirs2.todo),
|
|
4389
|
+
doingDir: join7(sessionsDir, statusDirs2.doing),
|
|
4390
|
+
archiveDir: join7(sessionsDir, statusDirs2.archive)
|
|
3748
4391
|
}
|
|
3749
4392
|
};
|
|
3750
4393
|
}
|
|
3751
4394
|
const { sessionsDir: baseDir, warning } = await resolveSessionsScopeDir({ cwd, deps });
|
|
3752
4395
|
return {
|
|
3753
4396
|
config: {
|
|
3754
|
-
todoDir:
|
|
3755
|
-
doingDir:
|
|
3756
|
-
archiveDir:
|
|
4397
|
+
todoDir: join7(baseDir, statusDirs2.todo),
|
|
4398
|
+
doingDir: join7(baseDir, statusDirs2.doing),
|
|
4399
|
+
archiveDir: join7(baseDir, statusDirs2.archive)
|
|
3757
4400
|
},
|
|
3758
4401
|
warning
|
|
3759
4402
|
};
|
|
@@ -3790,9 +4433,9 @@ async function fileExists(path6) {
|
|
|
3790
4433
|
}
|
|
3791
4434
|
async function probeSessionPaths(sessionId, config) {
|
|
3792
4435
|
const filename = `${sessionId}${SESSION_FILE_EXTENSION}`;
|
|
3793
|
-
const todoPath =
|
|
3794
|
-
const doingPath =
|
|
3795
|
-
const archivePath =
|
|
4436
|
+
const todoPath = join8(config.todoDir, filename);
|
|
4437
|
+
const doingPath = join8(config.doingDir, filename);
|
|
4438
|
+
const archivePath = join8(config.archiveDir, filename);
|
|
3796
4439
|
return {
|
|
3797
4440
|
todo: await fileExists(todoPath) ? todoPath : null,
|
|
3798
4441
|
doing: await fileExists(doingPath) ? doingPath : null,
|
|
@@ -3813,7 +4456,7 @@ async function resolveArchivePaths(sessionId, config) {
|
|
|
3813
4456
|
}
|
|
3814
4457
|
async function archiveSingle(sessionId, config) {
|
|
3815
4458
|
const { source, target } = await resolveArchivePaths(sessionId, config);
|
|
3816
|
-
await mkdir(
|
|
4459
|
+
await mkdir(dirname4(target), { recursive: true });
|
|
3817
4460
|
await rename(source, target);
|
|
3818
4461
|
return `${SESSION_ARCHIVE_OUTPUT.ARCHIVED}: ${sessionId}
|
|
3819
4462
|
${SESSION_ARCHIVE_OUTPUT.ARCHIVE_LOCATION}: ${target}`;
|
|
@@ -3836,7 +4479,7 @@ function resolveDeletePath(sessionId, existingPaths) {
|
|
|
3836
4479
|
}
|
|
3837
4480
|
|
|
3838
4481
|
// src/domains/session/show.ts
|
|
3839
|
-
import { join as
|
|
4482
|
+
import { join as join9 } from "path";
|
|
3840
4483
|
|
|
3841
4484
|
// src/domains/session/list.ts
|
|
3842
4485
|
import { parse as parseYaml2 } from "yaml";
|
|
@@ -3887,6 +4530,7 @@ var SESSION_PRIORITY = {
|
|
|
3887
4530
|
};
|
|
3888
4531
|
var SESSION_STATUSES = ["todo", "doing", "archive"];
|
|
3889
4532
|
var DEFAULT_LIST_STATUSES = ["doing", "todo"];
|
|
4533
|
+
var CLAIMABLE_STATUS = SESSION_STATUSES[0];
|
|
3890
4534
|
var PRIORITY_ORDER = {
|
|
3891
4535
|
[SESSION_PRIORITY.HIGH]: 0,
|
|
3892
4536
|
[SESSION_PRIORITY.MEDIUM]: 1,
|
|
@@ -4053,12 +4697,12 @@ var SESSION_SHOW_LABEL = {
|
|
|
4053
4697
|
};
|
|
4054
4698
|
var SESSION_SHOW_SEPARATOR_CHAR = "\u2500";
|
|
4055
4699
|
var SESSION_SHOW_SEPARATOR_WIDTH = 40;
|
|
4056
|
-
var sessionsBaseDir =
|
|
4700
|
+
var sessionsBaseDir = join9(STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
|
|
4057
4701
|
var { statusDirs } = DEFAULT_CONFIG.sessions;
|
|
4058
4702
|
var DEFAULT_SESSION_CONFIG = {
|
|
4059
|
-
todoDir:
|
|
4060
|
-
doingDir:
|
|
4061
|
-
archiveDir:
|
|
4703
|
+
todoDir: join9(sessionsBaseDir, statusDirs.todo),
|
|
4704
|
+
doingDir: join9(sessionsBaseDir, statusDirs.doing),
|
|
4705
|
+
archiveDir: join9(sessionsBaseDir, statusDirs.archive)
|
|
4062
4706
|
};
|
|
4063
4707
|
var SEARCH_ORDER = [...SESSION_STATUSES];
|
|
4064
4708
|
function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
|
|
@@ -4116,7 +4760,7 @@ async function deleteCommand(options) {
|
|
|
4116
4760
|
|
|
4117
4761
|
// src/commands/session/handoff.ts
|
|
4118
4762
|
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
4119
|
-
import { join as
|
|
4763
|
+
import { join as join10, resolve as resolve4 } from "path";
|
|
4120
4764
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
4121
4765
|
|
|
4122
4766
|
// src/domains/session/create.ts
|
|
@@ -4394,8 +5038,8 @@ async function handoffCommand(options) {
|
|
|
4394
5038
|
const yaml = stringifyYaml3(frontMatterObject, { defaultStringType: "QUOTE_DOUBLE" }).trimEnd();
|
|
4395
5039
|
const fullContent = `${SESSION_FRONT_MATTER_OPEN}${yaml}${SESSION_FRONT_MATTER_CLOSE}${body}`;
|
|
4396
5040
|
const filename = `${sessionId}.md`;
|
|
4397
|
-
const sessionPath =
|
|
4398
|
-
const absolutePath =
|
|
5041
|
+
const sessionPath = join10(config.todoDir, filename);
|
|
5042
|
+
const absolutePath = resolve4(sessionPath);
|
|
4399
5043
|
await mkdir2(config.todoDir, { recursive: true });
|
|
4400
5044
|
await writeFile(sessionPath, fullContent, "utf-8");
|
|
4401
5045
|
const output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
|
|
@@ -4405,7 +5049,7 @@ async function handoffCommand(options) {
|
|
|
4405
5049
|
|
|
4406
5050
|
// src/commands/session/list.ts
|
|
4407
5051
|
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
4408
|
-
import { join as
|
|
5052
|
+
import { join as join11 } from "path";
|
|
4409
5053
|
var SESSION_LIST_FORMAT = {
|
|
4410
5054
|
TEXT: "text",
|
|
4411
5055
|
JSON: "json"
|
|
@@ -4419,7 +5063,7 @@ async function loadSessionsFromDir(dir, status) {
|
|
|
4419
5063
|
for (const file of files) {
|
|
4420
5064
|
if (!file.endsWith(".md")) continue;
|
|
4421
5065
|
const id = file.replace(".md", "");
|
|
4422
|
-
const filePath =
|
|
5066
|
+
const filePath = join11(dir, file);
|
|
4423
5067
|
const content = await readFile3(filePath, "utf-8");
|
|
4424
5068
|
const metadata = parseSessionMetadata(content);
|
|
4425
5069
|
sessions.push({
|
|
@@ -4492,7 +5136,7 @@ async function listCommand(options) {
|
|
|
4492
5136
|
|
|
4493
5137
|
// src/commands/session/pickup.ts
|
|
4494
5138
|
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rename as rename2 } from "fs/promises";
|
|
4495
|
-
import { join as
|
|
5139
|
+
import { join as join12 } from "path";
|
|
4496
5140
|
|
|
4497
5141
|
// src/domains/session/pickup.ts
|
|
4498
5142
|
function buildClaimPaths(sessionId, config) {
|
|
@@ -4528,7 +5172,6 @@ function selectBestSession(sessions) {
|
|
|
4528
5172
|
}
|
|
4529
5173
|
|
|
4530
5174
|
// src/commands/session/pickup.ts
|
|
4531
|
-
var PICKUP_SOURCE_STATUS = SESSION_STATUSES[0];
|
|
4532
5175
|
var PICKUP_TARGET_STATUS = SESSION_STATUSES[1];
|
|
4533
5176
|
async function loadTodoSessions(config) {
|
|
4534
5177
|
try {
|
|
@@ -4537,12 +5180,12 @@ async function loadTodoSessions(config) {
|
|
|
4537
5180
|
for (const file of files) {
|
|
4538
5181
|
if (!file.endsWith(".md")) continue;
|
|
4539
5182
|
const id = file.replace(".md", "");
|
|
4540
|
-
const filePath =
|
|
5183
|
+
const filePath = join12(config.todoDir, file);
|
|
4541
5184
|
const content = await readFile4(filePath, "utf-8");
|
|
4542
5185
|
const metadata = parseSessionMetadata(content);
|
|
4543
5186
|
sessions.push({
|
|
4544
5187
|
id,
|
|
4545
|
-
status:
|
|
5188
|
+
status: CLAIMABLE_STATUS,
|
|
4546
5189
|
path: filePath,
|
|
4547
5190
|
metadata
|
|
4548
5191
|
});
|
|
@@ -4591,9 +5234,15 @@ async function pickupCommand(options) {
|
|
|
4591
5234
|
return processBatch(options.sessionIds, (id) => pickupSingle(id, config));
|
|
4592
5235
|
}
|
|
4593
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
|
+
|
|
4594
5243
|
// src/commands/session/prune.ts
|
|
4595
5244
|
import { readdir as readdir3, readFile as readFile5, unlink as unlink2 } from "fs/promises";
|
|
4596
|
-
import { join as
|
|
5245
|
+
import { join as join13 } from "path";
|
|
4597
5246
|
|
|
4598
5247
|
// src/domains/session/prune.ts
|
|
4599
5248
|
var DEFAULT_KEEP_COUNT = 5;
|
|
@@ -4642,7 +5291,7 @@ async function loadArchiveSessions(config) {
|
|
|
4642
5291
|
for (const file of files) {
|
|
4643
5292
|
if (!file.endsWith(".md")) continue;
|
|
4644
5293
|
const id = file.replace(".md", "");
|
|
4645
|
-
const filePath =
|
|
5294
|
+
const filePath = join13(config.archiveDir, file);
|
|
4646
5295
|
const content = await readFile5(filePath, "utf-8");
|
|
4647
5296
|
const metadata = parseSessionMetadata(content);
|
|
4648
5297
|
sessions.push({
|
|
@@ -4860,22 +5509,206 @@ Output:
|
|
|
4860
5509
|
<PICKUP_ID>session-id</PICKUP_ID> tag for each claimed session
|
|
4861
5510
|
`;
|
|
4862
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
|
+
|
|
4863
5696
|
// src/interfaces/cli/session.ts
|
|
4864
5697
|
async function readStdin() {
|
|
4865
5698
|
if (process.stdin.isTTY) {
|
|
4866
5699
|
return void 0;
|
|
4867
5700
|
}
|
|
4868
|
-
return new Promise((
|
|
5701
|
+
return new Promise((resolve6) => {
|
|
4869
5702
|
let data = "";
|
|
4870
5703
|
process.stdin.setEncoding("utf-8");
|
|
4871
5704
|
process.stdin.on("data", (chunk) => {
|
|
4872
5705
|
data += chunk;
|
|
4873
5706
|
});
|
|
4874
5707
|
process.stdin.on("end", () => {
|
|
4875
|
-
|
|
5708
|
+
resolve6(data.length === 0 ? void 0 : data);
|
|
4876
5709
|
});
|
|
4877
5710
|
process.stdin.on("error", () => {
|
|
4878
|
-
|
|
5711
|
+
resolve6(void 0);
|
|
4879
5712
|
});
|
|
4880
5713
|
});
|
|
4881
5714
|
}
|
|
@@ -4898,6 +5731,29 @@ function registerSessionCommands(sessionCmd) {
|
|
|
4898
5731
|
handleError(error);
|
|
4899
5732
|
}
|
|
4900
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
|
+
});
|
|
4901
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) => {
|
|
4902
5758
|
try {
|
|
4903
5759
|
const output = await listCommand({
|
|
@@ -5032,7 +5888,7 @@ var sessionDomain = {
|
|
|
5032
5888
|
|
|
5033
5889
|
// src/lib/spec-tree/index.ts
|
|
5034
5890
|
import { readdir as readdir5, readFile as readFile7 } from "fs/promises";
|
|
5035
|
-
import { join as
|
|
5891
|
+
import { join as join14 } from "path";
|
|
5036
5892
|
var SPEC_TREE_FIELD_KEY = {
|
|
5037
5893
|
VERSION: "version",
|
|
5038
5894
|
PRODUCT: "product",
|
|
@@ -5116,7 +5972,7 @@ function createFilesystemSpecTreeSource(options) {
|
|
|
5116
5972
|
if (ref.path === void 0) {
|
|
5117
5973
|
throw new Error("Filesystem source refs require a path");
|
|
5118
5974
|
}
|
|
5119
|
-
return readFile7(
|
|
5975
|
+
return readFile7(join14(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
|
|
5120
5976
|
}
|
|
5121
5977
|
};
|
|
5122
5978
|
}
|
|
@@ -5398,7 +6254,7 @@ function compareOrderedEntries(left, right) {
|
|
|
5398
6254
|
}
|
|
5399
6255
|
async function* readFilesystemSourceEntries(productDir, registry, schemaVersions, includePath) {
|
|
5400
6256
|
yield* walkFilesystemDirectory({
|
|
5401
|
-
absolutePath:
|
|
6257
|
+
absolutePath: join14(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY),
|
|
5402
6258
|
relativePath: SPEC_TREE_EMPTY_RELATIVE_PATH,
|
|
5403
6259
|
registry,
|
|
5404
6260
|
schemaVersions,
|
|
@@ -5427,7 +6283,7 @@ async function* walkFilesystemDirectory(context) {
|
|
|
5427
6283
|
if (sourceEntry !== null) yield sourceEntry;
|
|
5428
6284
|
if (entry.isDirectory() && shouldDescendIntoDirectory(sourceEntry)) {
|
|
5429
6285
|
yield* walkFilesystemDirectory({
|
|
5430
|
-
absolutePath:
|
|
6286
|
+
absolutePath: join14(context.absolutePath, entry.name),
|
|
5431
6287
|
relativePath,
|
|
5432
6288
|
registry: context.registry,
|
|
5433
6289
|
schemaVersions: context.schemaVersions,
|
|
@@ -5573,14 +6429,14 @@ function formatNextNode(node) {
|
|
|
5573
6429
|
|
|
5574
6430
|
// src/commands/testing/discovery.ts
|
|
5575
6431
|
import { readdir as readdir6 } from "fs/promises";
|
|
5576
|
-
import { join as
|
|
6432
|
+
import { join as join15, relative, sep } from "path";
|
|
5577
6433
|
var TESTS_DIRECTORY_NAME = SPEC_TREE_EVIDENCE_FILE.DIRECTORY_NAME;
|
|
5578
6434
|
var SPEC_ROOT_DIRECTORY = SPEC_TREE_CONFIG.ROOT_DIRECTORY;
|
|
5579
6435
|
var POSIX_SEPARATOR = "/";
|
|
5580
6436
|
var ERROR_CODE_NOT_FOUND2 = "ENOENT";
|
|
5581
6437
|
async function discoverTestFiles(productDir) {
|
|
5582
6438
|
const found = [];
|
|
5583
|
-
await collectTestFiles(
|
|
6439
|
+
await collectTestFiles(join15(productDir, SPEC_ROOT_DIRECTORY), false, found);
|
|
5584
6440
|
return found.map((absolute) => toPosixRelative(productDir, absolute)).sort(compareAscii);
|
|
5585
6441
|
}
|
|
5586
6442
|
async function collectTestFiles(directory, insideTestsDir, found) {
|
|
@@ -5592,7 +6448,7 @@ async function collectTestFiles(directory, insideTestsDir, found) {
|
|
|
5592
6448
|
throw error;
|
|
5593
6449
|
}
|
|
5594
6450
|
for (const entry of entries) {
|
|
5595
|
-
const childPath =
|
|
6451
|
+
const childPath = join15(directory, entry.name);
|
|
5596
6452
|
if (entry.isDirectory()) {
|
|
5597
6453
|
await collectTestFiles(childPath, insideTestsDir || entry.name === TESTS_DIRECTORY_NAME, found);
|
|
5598
6454
|
} else if (insideTestsDir && entry.isFile()) {
|
|
@@ -5674,11 +6530,11 @@ async function runTests(options, deps) {
|
|
|
5674
6530
|
}
|
|
5675
6531
|
|
|
5676
6532
|
// src/commands/testing/run-command.ts
|
|
5677
|
-
import { join as
|
|
6533
|
+
import { join as join17 } from "path";
|
|
5678
6534
|
|
|
5679
6535
|
// src/testing/run-state.ts
|
|
5680
6536
|
import { createHash as createHash4 } from "crypto";
|
|
5681
|
-
import { join as
|
|
6537
|
+
import { join as join16 } from "path";
|
|
5682
6538
|
var TEST_RUN_STATE_STATUS = {
|
|
5683
6539
|
PASSED: "passed",
|
|
5684
6540
|
FAILED: "failed",
|
|
@@ -5765,12 +6621,12 @@ async function readTestingRuns(productDir, options = {}) {
|
|
|
5765
6621
|
if (hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) {
|
|
5766
6622
|
return { ok: true, value: { terminalRuns: [], incompleteRuns: [] } };
|
|
5767
6623
|
}
|
|
5768
|
-
return { ok: false, error:
|
|
6624
|
+
return { ok: false, error: toErrorMessage3(error) };
|
|
5769
6625
|
}
|
|
5770
6626
|
const terminalRuns = [];
|
|
5771
6627
|
const incompleteRuns = [];
|
|
5772
6628
|
for (const entry of entries.filter(isTestRunFileEntry)) {
|
|
5773
|
-
const runFilePath =
|
|
6629
|
+
const runFilePath = join16(runsDir2, entry.name);
|
|
5774
6630
|
const stateResult = await readTestRunStatePath(runFilePath, fs7);
|
|
5775
6631
|
if (stateResult.ok) {
|
|
5776
6632
|
terminalRuns.push({ runFileName: entry.name, runFilePath, state: stateResult.value });
|
|
@@ -5847,7 +6703,7 @@ async function readTestRunStatePath(runFilePath, fs7) {
|
|
|
5847
6703
|
return {
|
|
5848
6704
|
ok: false,
|
|
5849
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,
|
|
5850
|
-
error:
|
|
6706
|
+
error: toErrorMessage3(error)
|
|
5851
6707
|
};
|
|
5852
6708
|
}
|
|
5853
6709
|
const latest = latestNonEmptyJsonlLine(content);
|
|
@@ -5999,7 +6855,7 @@ function isRecord4(value) {
|
|
|
5999
6855
|
function sha256Hex(value) {
|
|
6000
6856
|
return createHash4(SHA256_ALGORITHM2).update(value).digest(HEX_ENCODING3);
|
|
6001
6857
|
}
|
|
6002
|
-
function
|
|
6858
|
+
function toErrorMessage3(error) {
|
|
6003
6859
|
return error instanceof Error ? error.message : String(error);
|
|
6004
6860
|
}
|
|
6005
6861
|
|
|
@@ -6036,7 +6892,7 @@ function coveredTestPaths(dispatch) {
|
|
|
6036
6892
|
async function readCoveredContents(productDir, paths, fs7) {
|
|
6037
6893
|
const entries = [];
|
|
6038
6894
|
for (const path6 of paths) {
|
|
6039
|
-
entries.push({ path: path6, content: await fs7.readFile(
|
|
6895
|
+
entries.push({ path: path6, content: await fs7.readFile(join17(productDir, path6), TEXT_ENCODING) });
|
|
6040
6896
|
}
|
|
6041
6897
|
return entries;
|
|
6042
6898
|
}
|
|
@@ -6047,7 +6903,7 @@ async function readProductInputEntries(productDir, paths, fs7) {
|
|
|
6047
6903
|
entries.push({
|
|
6048
6904
|
[PRODUCT_INPUT_FIELDS.PATH]: path6,
|
|
6049
6905
|
[PRODUCT_INPUT_FIELDS.PRESENT]: true,
|
|
6050
|
-
[PRODUCT_INPUT_FIELDS.CONTENT]: await fs7.readFile(
|
|
6906
|
+
[PRODUCT_INPUT_FIELDS.CONTENT]: await fs7.readFile(join17(productDir, path6), TEXT_ENCODING)
|
|
6051
6907
|
});
|
|
6052
6908
|
} catch (error) {
|
|
6053
6909
|
if (!hasErrorCode(error, TESTING_RUN_STATE_ERROR_CODE.NOT_FOUND)) throw error;
|
|
@@ -6230,11 +7086,11 @@ function serializeNodeStatus(state) {
|
|
|
6230
7086
|
}
|
|
6231
7087
|
|
|
6232
7088
|
// src/lib/node-status/provider.ts
|
|
6233
|
-
import { join as
|
|
7089
|
+
import { join as join19 } from "path";
|
|
6234
7090
|
|
|
6235
7091
|
// src/lib/node-status/read.ts
|
|
6236
7092
|
import { readFileSync as readFileSync2 } from "fs";
|
|
6237
|
-
import { join as
|
|
7093
|
+
import { join as join18 } from "path";
|
|
6238
7094
|
var NODE_STATUS_FILENAME = "spx.status.json";
|
|
6239
7095
|
var NODE_STATUS_VALUES = new Set(Object.values(SPEC_TREE_NODE_STATE));
|
|
6240
7096
|
function isNodeError2(error) {
|
|
@@ -6244,7 +7100,7 @@ function isSpecTreeNodeState(value) {
|
|
|
6244
7100
|
return typeof value === "string" && NODE_STATUS_VALUES.has(value);
|
|
6245
7101
|
}
|
|
6246
7102
|
function readNodeStatus(nodeDir) {
|
|
6247
|
-
const filePath =
|
|
7103
|
+
const filePath = join18(nodeDir, NODE_STATUS_FILENAME);
|
|
6248
7104
|
let content;
|
|
6249
7105
|
try {
|
|
6250
7106
|
content = readFileSync2(filePath, "utf8");
|
|
@@ -6266,7 +7122,7 @@ function readNodeStatus(nodeDir) {
|
|
|
6266
7122
|
|
|
6267
7123
|
// src/lib/node-status/provider.ts
|
|
6268
7124
|
function nodeDirectory(productDir, node) {
|
|
6269
|
-
return
|
|
7125
|
+
return join19(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
|
|
6270
7126
|
}
|
|
6271
7127
|
function createNodeStatusProvider(productDir) {
|
|
6272
7128
|
return {
|
|
@@ -6278,7 +7134,7 @@ function createNodeStatusProvider(productDir) {
|
|
|
6278
7134
|
|
|
6279
7135
|
// src/lib/node-status/update.ts
|
|
6280
7136
|
import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
|
|
6281
|
-
import { dirname as
|
|
7137
|
+
import { dirname as dirname5, join as join20 } from "path";
|
|
6282
7138
|
var NODE_STATUS_TEXT_ENCODING = "utf8";
|
|
6283
7139
|
async function updateNodeStatus(options) {
|
|
6284
7140
|
const { productDir, resolveOutcome } = options;
|
|
@@ -6316,8 +7172,8 @@ function isNodeExcluded(ignoreReader, node) {
|
|
|
6316
7172
|
return ignoreReader.isUnderIgnoreSource(reference);
|
|
6317
7173
|
}
|
|
6318
7174
|
async function writeNodeStatus(productDir, nodeId, state) {
|
|
6319
|
-
const filePath =
|
|
6320
|
-
await mkdir4(
|
|
7175
|
+
const filePath = join20(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
|
|
7176
|
+
await mkdir4(dirname5(filePath), { recursive: true });
|
|
6321
7177
|
await writeFile2(filePath, serializeNodeStatus(state), NODE_STATUS_TEXT_ENCODING);
|
|
6322
7178
|
}
|
|
6323
7179
|
|
|
@@ -6431,7 +7287,7 @@ function formatNodeLabel(node) {
|
|
|
6431
7287
|
}
|
|
6432
7288
|
|
|
6433
7289
|
// src/testing/languages/python.ts
|
|
6434
|
-
import { basename as
|
|
7290
|
+
import { basename as basename3, dirname as dirname6, join as join21 } from "path/posix";
|
|
6435
7291
|
|
|
6436
7292
|
// src/validation/discovery/language-finder.ts
|
|
6437
7293
|
import fs5 from "fs";
|
|
@@ -6496,16 +7352,16 @@ var PYTHON_TEST_FILE_PREFIX = "test_";
|
|
|
6496
7352
|
var PYTHON_TEST_FILE_EXTENSION = ".py";
|
|
6497
7353
|
var PYTHON_TEST_FILE_PATTERNS = [`${PYTHON_TEST_FILE_PREFIX}*${PYTHON_TEST_FILE_EXTENSION}`];
|
|
6498
7354
|
function matchesTestFile(filePath) {
|
|
6499
|
-
return
|
|
7355
|
+
return basename3(filePath).startsWith(PYTHON_TEST_FILE_PREFIX) && filePath.endsWith(PYTHON_TEST_FILE_EXTENSION);
|
|
6500
7356
|
}
|
|
6501
7357
|
function coveredProductInputPaths(coveredTestPaths2) {
|
|
6502
7358
|
const paths = /* @__PURE__ */ new Set();
|
|
6503
7359
|
for (const testPath of coveredTestPaths2) {
|
|
6504
7360
|
if (!matchesTestFile(testPath)) continue;
|
|
6505
|
-
let directory =
|
|
7361
|
+
let directory = dirname6(testPath);
|
|
6506
7362
|
while (directory !== "." && directory.length > 0) {
|
|
6507
|
-
paths.add(
|
|
6508
|
-
const parent =
|
|
7363
|
+
paths.add(join21(directory, PYTHON_PRODUCT_INPUT_PATH.CONFTEST));
|
|
7364
|
+
const parent = dirname6(directory);
|
|
6509
7365
|
if (parent === directory) break;
|
|
6510
7366
|
directory = parent;
|
|
6511
7367
|
}
|
|
@@ -6986,7 +7842,7 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
|
|
|
6986
7842
|
|
|
6987
7843
|
// src/validation/steps/markdown.ts
|
|
6988
7844
|
import { existsSync, statSync } from "fs";
|
|
6989
|
-
import { basename as
|
|
7845
|
+
import { basename as basename4, dirname as dirname7, join as join22, relative as pathRelative } from "path";
|
|
6990
7846
|
import { main as markdownlintMain } from "markdownlint-cli2";
|
|
6991
7847
|
import relativeLinksRule from "markdownlint-rule-relative-links";
|
|
6992
7848
|
var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
|
|
@@ -7025,7 +7881,7 @@ function buildMarkdownlintConfig(directoryName) {
|
|
|
7025
7881
|
};
|
|
7026
7882
|
}
|
|
7027
7883
|
function getDefaultDirectories(projectRoot) {
|
|
7028
|
-
return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) =>
|
|
7884
|
+
return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join22(projectRoot, name)).filter((dir) => existsSync(dir));
|
|
7029
7885
|
}
|
|
7030
7886
|
function resolveMarkdownValidationTarget(path6, deps = defaultMarkdownValidationTargetDeps) {
|
|
7031
7887
|
if (isExistingDirectory(path6, deps)) {
|
|
@@ -7084,7 +7940,7 @@ async function validateMarkdown(options) {
|
|
|
7084
7940
|
async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
7085
7941
|
const errors = [];
|
|
7086
7942
|
const directory = targetDirectory(target);
|
|
7087
|
-
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];
|
|
7088
7944
|
const { customRules, ...markdownlintConfig } = config;
|
|
7089
7945
|
const optionsOverride = {
|
|
7090
7946
|
config: {
|
|
@@ -7108,7 +7964,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
|
7108
7964
|
if (parsed) {
|
|
7109
7965
|
errors.push({
|
|
7110
7966
|
...parsed,
|
|
7111
|
-
file:
|
|
7967
|
+
file: join22(directory, parsed.file)
|
|
7112
7968
|
});
|
|
7113
7969
|
}
|
|
7114
7970
|
}
|
|
@@ -7116,7 +7972,7 @@ async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
|
7116
7972
|
return errors;
|
|
7117
7973
|
}
|
|
7118
7974
|
function targetDirectory(target) {
|
|
7119
|
-
return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ?
|
|
7975
|
+
return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname7(target.path) : target.path;
|
|
7120
7976
|
}
|
|
7121
7977
|
function hasMarkdownExtension(path6) {
|
|
7122
7978
|
const lastDot = path6.lastIndexOf(".");
|
|
@@ -7144,7 +8000,7 @@ function markdownlintConfigDirectoryName(directory, projectRoot) {
|
|
|
7144
8000
|
return rootSegment;
|
|
7145
8001
|
}
|
|
7146
8002
|
}
|
|
7147
|
-
return
|
|
8003
|
+
return basename4(directory);
|
|
7148
8004
|
}
|
|
7149
8005
|
|
|
7150
8006
|
// src/commands/validation/messages.ts
|
|
@@ -7158,6 +8014,7 @@ var VALIDATION_STAGE_DISPLAY_NAMES = {
|
|
|
7158
8014
|
};
|
|
7159
8015
|
var VALIDATION_SKIP_LABELS = {
|
|
7160
8016
|
VERB: "Skipping",
|
|
8017
|
+
CIRCULAR_REASON: "skip-circular",
|
|
7161
8018
|
LITERAL_REASON: "skip-literal",
|
|
7162
8019
|
TYPESCRIPT_ABSENT_REASON: "TypeScript not detected in project",
|
|
7163
8020
|
VALIDATION_PATHS_NO_TARGETS_REASON: "validation paths matched no files",
|
|
@@ -7179,6 +8036,11 @@ var VALIDATION_COMMAND_OUTPUT = {
|
|
|
7179
8036
|
MARKDOWN_NO_ISSUES: `${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: No issues found`,
|
|
7180
8037
|
MARKDOWN_ERROR_SUMMARY_SUFFIX: "error(s) found"
|
|
7181
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
|
+
});
|
|
7182
8044
|
var LITERAL_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.LITERAL}: skipped (--${VALIDATION_SKIP_LABELS.LITERAL_REASON})`;
|
|
7183
8045
|
var LITERAL_SKIP_JSON_OUTPUT = JSON.stringify({
|
|
7184
8046
|
skipped: true,
|
|
@@ -7813,39 +8675,39 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7813
8675
|
const token = _scanner.scan();
|
|
7814
8676
|
switch (_scanner.getTokenError()) {
|
|
7815
8677
|
case 4:
|
|
7816
|
-
|
|
8678
|
+
handleError3(
|
|
7817
8679
|
14
|
|
7818
8680
|
/* ParseErrorCode.InvalidUnicode */
|
|
7819
8681
|
);
|
|
7820
8682
|
break;
|
|
7821
8683
|
case 5:
|
|
7822
|
-
|
|
8684
|
+
handleError3(
|
|
7823
8685
|
15
|
|
7824
8686
|
/* ParseErrorCode.InvalidEscapeCharacter */
|
|
7825
8687
|
);
|
|
7826
8688
|
break;
|
|
7827
8689
|
case 3:
|
|
7828
|
-
|
|
8690
|
+
handleError3(
|
|
7829
8691
|
13
|
|
7830
8692
|
/* ParseErrorCode.UnexpectedEndOfNumber */
|
|
7831
8693
|
);
|
|
7832
8694
|
break;
|
|
7833
8695
|
case 1:
|
|
7834
8696
|
if (!disallowComments) {
|
|
7835
|
-
|
|
8697
|
+
handleError3(
|
|
7836
8698
|
11
|
|
7837
8699
|
/* ParseErrorCode.UnexpectedEndOfComment */
|
|
7838
8700
|
);
|
|
7839
8701
|
}
|
|
7840
8702
|
break;
|
|
7841
8703
|
case 2:
|
|
7842
|
-
|
|
8704
|
+
handleError3(
|
|
7843
8705
|
12
|
|
7844
8706
|
/* ParseErrorCode.UnexpectedEndOfString */
|
|
7845
8707
|
);
|
|
7846
8708
|
break;
|
|
7847
8709
|
case 6:
|
|
7848
|
-
|
|
8710
|
+
handleError3(
|
|
7849
8711
|
16
|
|
7850
8712
|
/* ParseErrorCode.InvalidCharacter */
|
|
7851
8713
|
);
|
|
@@ -7855,7 +8717,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7855
8717
|
case 12:
|
|
7856
8718
|
case 13:
|
|
7857
8719
|
if (disallowComments) {
|
|
7858
|
-
|
|
8720
|
+
handleError3(
|
|
7859
8721
|
10
|
|
7860
8722
|
/* ParseErrorCode.InvalidCommentToken */
|
|
7861
8723
|
);
|
|
@@ -7864,7 +8726,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7864
8726
|
}
|
|
7865
8727
|
break;
|
|
7866
8728
|
case 16:
|
|
7867
|
-
|
|
8729
|
+
handleError3(
|
|
7868
8730
|
1
|
|
7869
8731
|
/* ParseErrorCode.InvalidSymbol */
|
|
7870
8732
|
);
|
|
@@ -7877,7 +8739,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7877
8739
|
}
|
|
7878
8740
|
}
|
|
7879
8741
|
}
|
|
7880
|
-
function
|
|
8742
|
+
function handleError3(error, skipUntilAfter = [], skipUntil = []) {
|
|
7881
8743
|
onError(error);
|
|
7882
8744
|
if (skipUntilAfter.length + skipUntil.length > 0) {
|
|
7883
8745
|
let token = _scanner.getToken();
|
|
@@ -7909,7 +8771,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7909
8771
|
const tokenValue = _scanner.getTokenValue();
|
|
7910
8772
|
let value = Number(tokenValue);
|
|
7911
8773
|
if (isNaN(value)) {
|
|
7912
|
-
|
|
8774
|
+
handleError3(
|
|
7913
8775
|
2
|
|
7914
8776
|
/* ParseErrorCode.InvalidNumberFormat */
|
|
7915
8777
|
);
|
|
@@ -7934,7 +8796,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7934
8796
|
}
|
|
7935
8797
|
function parseProperty() {
|
|
7936
8798
|
if (_scanner.getToken() !== 10) {
|
|
7937
|
-
|
|
8799
|
+
handleError3(3, [], [
|
|
7938
8800
|
2,
|
|
7939
8801
|
5
|
|
7940
8802
|
/* SyntaxKind.CommaToken */
|
|
@@ -7946,14 +8808,14 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7946
8808
|
onSeparator(":");
|
|
7947
8809
|
scanNext();
|
|
7948
8810
|
if (!parseValue()) {
|
|
7949
|
-
|
|
8811
|
+
handleError3(4, [], [
|
|
7950
8812
|
2,
|
|
7951
8813
|
5
|
|
7952
8814
|
/* SyntaxKind.CommaToken */
|
|
7953
8815
|
]);
|
|
7954
8816
|
}
|
|
7955
8817
|
} else {
|
|
7956
|
-
|
|
8818
|
+
handleError3(5, [], [
|
|
7957
8819
|
2,
|
|
7958
8820
|
5
|
|
7959
8821
|
/* SyntaxKind.CommaToken */
|
|
@@ -7969,7 +8831,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7969
8831
|
while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
|
|
7970
8832
|
if (_scanner.getToken() === 5) {
|
|
7971
8833
|
if (!needsComma) {
|
|
7972
|
-
|
|
8834
|
+
handleError3(4, [], []);
|
|
7973
8835
|
}
|
|
7974
8836
|
onSeparator(",");
|
|
7975
8837
|
scanNext();
|
|
@@ -7977,10 +8839,10 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7977
8839
|
break;
|
|
7978
8840
|
}
|
|
7979
8841
|
} else if (needsComma) {
|
|
7980
|
-
|
|
8842
|
+
handleError3(6, [], []);
|
|
7981
8843
|
}
|
|
7982
8844
|
if (!parseProperty()) {
|
|
7983
|
-
|
|
8845
|
+
handleError3(4, [], [
|
|
7984
8846
|
2,
|
|
7985
8847
|
5
|
|
7986
8848
|
/* SyntaxKind.CommaToken */
|
|
@@ -7990,7 +8852,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7990
8852
|
}
|
|
7991
8853
|
onObjectEnd();
|
|
7992
8854
|
if (_scanner.getToken() !== 2) {
|
|
7993
|
-
|
|
8855
|
+
handleError3(7, [
|
|
7994
8856
|
2
|
|
7995
8857
|
/* SyntaxKind.CloseBraceToken */
|
|
7996
8858
|
], []);
|
|
@@ -8007,7 +8869,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
8007
8869
|
while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
|
|
8008
8870
|
if (_scanner.getToken() === 5) {
|
|
8009
8871
|
if (!needsComma) {
|
|
8010
|
-
|
|
8872
|
+
handleError3(4, [], []);
|
|
8011
8873
|
}
|
|
8012
8874
|
onSeparator(",");
|
|
8013
8875
|
scanNext();
|
|
@@ -8015,7 +8877,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
8015
8877
|
break;
|
|
8016
8878
|
}
|
|
8017
8879
|
} else if (needsComma) {
|
|
8018
|
-
|
|
8880
|
+
handleError3(6, [], []);
|
|
8019
8881
|
}
|
|
8020
8882
|
if (isFirstElement) {
|
|
8021
8883
|
_jsonPath.push(0);
|
|
@@ -8024,7 +8886,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
8024
8886
|
_jsonPath[_jsonPath.length - 1]++;
|
|
8025
8887
|
}
|
|
8026
8888
|
if (!parseValue()) {
|
|
8027
|
-
|
|
8889
|
+
handleError3(4, [], [
|
|
8028
8890
|
4,
|
|
8029
8891
|
5
|
|
8030
8892
|
/* SyntaxKind.CommaToken */
|
|
@@ -8037,7 +8899,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
8037
8899
|
_jsonPath.pop();
|
|
8038
8900
|
}
|
|
8039
8901
|
if (_scanner.getToken() !== 4) {
|
|
8040
|
-
|
|
8902
|
+
handleError3(8, [
|
|
8041
8903
|
4
|
|
8042
8904
|
/* SyntaxKind.CloseBracketToken */
|
|
8043
8905
|
], []);
|
|
@@ -8063,15 +8925,15 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
8063
8925
|
if (options.allowEmptyContent) {
|
|
8064
8926
|
return true;
|
|
8065
8927
|
}
|
|
8066
|
-
|
|
8928
|
+
handleError3(4, [], []);
|
|
8067
8929
|
return false;
|
|
8068
8930
|
}
|
|
8069
8931
|
if (!parseValue()) {
|
|
8070
|
-
|
|
8932
|
+
handleError3(4, [], []);
|
|
8071
8933
|
return false;
|
|
8072
8934
|
}
|
|
8073
8935
|
if (_scanner.getToken() !== 17) {
|
|
8074
|
-
|
|
8936
|
+
handleError3(9, [], []);
|
|
8075
8937
|
}
|
|
8076
8938
|
return true;
|
|
8077
8939
|
}
|
|
@@ -8130,7 +8992,7 @@ var ParseErrorCode;
|
|
|
8130
8992
|
|
|
8131
8993
|
// src/validation/config/scope.ts
|
|
8132
8994
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
8133
|
-
import { isAbsolute as isAbsolute3, join as
|
|
8995
|
+
import { isAbsolute as isAbsolute3, join as join23 } from "path";
|
|
8134
8996
|
var TSCONFIG_FILES = {
|
|
8135
8997
|
full: "tsconfig.json",
|
|
8136
8998
|
production: "tsconfig.production.json"
|
|
@@ -8144,7 +9006,7 @@ var defaultScopeDeps = {
|
|
|
8144
9006
|
readdirSync
|
|
8145
9007
|
};
|
|
8146
9008
|
function resolveProjectPath(projectRoot, path6) {
|
|
8147
|
-
return isAbsolute3(path6) ? path6 :
|
|
9009
|
+
return isAbsolute3(path6) ? path6 : join23(projectRoot, path6);
|
|
8148
9010
|
}
|
|
8149
9011
|
function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
|
|
8150
9012
|
try {
|
|
@@ -8188,7 +9050,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
|
|
|
8188
9050
|
if (hasDirectTsFiles) return true;
|
|
8189
9051
|
const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
|
|
8190
9052
|
for (const subdir of subdirs.slice(0, 5)) {
|
|
8191
|
-
if (hasTypeScriptFilesRecursive(
|
|
9053
|
+
if (hasTypeScriptFilesRecursive(join23(dirPath, subdir.name), maxDepth - 1, deps)) {
|
|
8192
9054
|
return true;
|
|
8193
9055
|
}
|
|
8194
9056
|
}
|
|
@@ -8211,7 +9073,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
|
|
|
8211
9073
|
});
|
|
8212
9074
|
if (!isExcluded) {
|
|
8213
9075
|
try {
|
|
8214
|
-
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(
|
|
9076
|
+
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join23(projectRoot, dir), 2, deps);
|
|
8215
9077
|
if (hasTypeScriptFiles) {
|
|
8216
9078
|
directories.add(dir);
|
|
8217
9079
|
}
|
|
@@ -8242,13 +9104,13 @@ function getLiteralTopLevelPatternDirectory(pattern) {
|
|
|
8242
9104
|
function filterActiveIncludePatterns(patterns, excludePatterns, projectRoot, deps) {
|
|
8243
9105
|
return patterns.filter((pattern) => {
|
|
8244
9106
|
const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
|
|
8245
|
-
return topLevelDir === null || deps.existsSync(
|
|
9107
|
+
return topLevelDir === null || deps.existsSync(join23(projectRoot, topLevelDir));
|
|
8246
9108
|
}).filter((pattern) => pathPassesValidationFilter(pattern, { exclude: excludePatterns }));
|
|
8247
9109
|
}
|
|
8248
9110
|
function getValidationDirectories(scope, projectRoot, deps = defaultScopeDeps) {
|
|
8249
9111
|
const config = resolveTypeScriptConfig(scope, projectRoot, deps);
|
|
8250
9112
|
const configDirectories = getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps);
|
|
8251
|
-
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(
|
|
9113
|
+
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join23(projectRoot, dir)));
|
|
8252
9114
|
return existingDirectories;
|
|
8253
9115
|
}
|
|
8254
9116
|
function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
|
|
@@ -8262,10 +9124,10 @@ function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
|
|
|
8262
9124
|
}
|
|
8263
9125
|
|
|
8264
9126
|
// src/validation/discovery/tool-finder.ts
|
|
8265
|
-
import { execSync as execSync2 } from "child_process";
|
|
8266
9127
|
import fs6 from "fs";
|
|
8267
9128
|
import { createRequire } from "module";
|
|
8268
9129
|
import path5 from "path";
|
|
9130
|
+
import { fileURLToPath } from "url";
|
|
8269
9131
|
|
|
8270
9132
|
// src/validation/discovery/constants.ts
|
|
8271
9133
|
var TOOL_DISCOVERY = {
|
|
@@ -8299,6 +9161,50 @@ var TOOL_DISCOVERY = {
|
|
|
8299
9161
|
|
|
8300
9162
|
// src/validation/discovery/tool-finder.ts
|
|
8301
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
|
+
}
|
|
8302
9208
|
var defaultToolDiscoveryDeps = {
|
|
8303
9209
|
resolveModule: (modulePath) => {
|
|
8304
9210
|
try {
|
|
@@ -8307,28 +9213,50 @@ var defaultToolDiscoveryDeps = {
|
|
|
8307
9213
|
return null;
|
|
8308
9214
|
}
|
|
8309
9215
|
},
|
|
8310
|
-
|
|
8311
|
-
whichSync: (tool) => {
|
|
9216
|
+
resolveImport: (modulePath) => {
|
|
8312
9217
|
try {
|
|
8313
|
-
|
|
8314
|
-
encoding: "utf-8",
|
|
8315
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
8316
|
-
});
|
|
8317
|
-
return result.trim() || null;
|
|
9218
|
+
return import.meta.resolve(modulePath);
|
|
8318
9219
|
} catch {
|
|
8319
9220
|
return null;
|
|
8320
9221
|
}
|
|
8321
|
-
}
|
|
9222
|
+
},
|
|
9223
|
+
existsSync: fs6.existsSync,
|
|
9224
|
+
whichSync: findExecutableOnPath
|
|
8322
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
|
+
}
|
|
8323
9251
|
async function discoverTool(tool, options = {}) {
|
|
8324
9252
|
const { projectRoot = process.cwd(), deps = defaultToolDiscoveryDeps } = options;
|
|
8325
|
-
const bundledPath = deps.resolveModule(`${tool}/package.json`);
|
|
9253
|
+
const bundledPath = deps.resolveModule(`${tool}/package.json`) ?? deps.resolveImport?.(tool);
|
|
8326
9254
|
if (bundledPath) {
|
|
8327
9255
|
return {
|
|
8328
9256
|
found: true,
|
|
8329
9257
|
location: {
|
|
8330
9258
|
tool,
|
|
8331
|
-
path:
|
|
9259
|
+
path: bundledToolPath(bundledPath, deps.existsSync),
|
|
8332
9260
|
source: TOOL_DISCOVERY.SOURCES.BUNDLED
|
|
8333
9261
|
}
|
|
8334
9262
|
};
|
|
@@ -8371,33 +9299,180 @@ function formatSkipMessage(stepName, result) {
|
|
|
8371
9299
|
}
|
|
8372
9300
|
|
|
8373
9301
|
// src/validation/steps/circular.ts
|
|
8374
|
-
import
|
|
8375
|
-
|
|
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
|
+
]);
|
|
8376
9349
|
var CIRCULAR_DEPS_KEYS = {
|
|
8377
|
-
|
|
9350
|
+
DEPENDENCY_CRUISER: "dependencyCruiser",
|
|
9351
|
+
EXTRACT_TYPESCRIPT_CONFIG: "extractTypeScriptConfig"
|
|
8378
9352
|
};
|
|
8379
9353
|
var defaultCircularDeps = {
|
|
8380
|
-
[CIRCULAR_DEPS_KEYS.
|
|
9354
|
+
[CIRCULAR_DEPS_KEYS.DEPENDENCY_CRUISER]: dependencyCruiser,
|
|
9355
|
+
[CIRCULAR_DEPS_KEYS.EXTRACT_TYPESCRIPT_CONFIG]: extractTypeScriptConfig
|
|
8381
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
|
+
}
|
|
8382
9459
|
async function validateCircularDependencies(scope, typescriptScope, projectRoot, deps = defaultCircularDeps) {
|
|
8383
9460
|
try {
|
|
8384
|
-
const
|
|
8385
|
-
if (
|
|
9461
|
+
const analyzeSourcePatterns = toDependencyCruiserSourcePatterns(typescriptScope.directories);
|
|
9462
|
+
if (analyzeSourcePatterns.length === 0) {
|
|
8386
9463
|
return { success: true };
|
|
8387
9464
|
}
|
|
8388
|
-
const tsConfigFile =
|
|
8389
|
-
const
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
});
|
|
8400
|
-
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);
|
|
8401
9476
|
if (circular.length === 0) {
|
|
8402
9477
|
return { success: true };
|
|
8403
9478
|
} else {
|
|
@@ -8430,7 +9505,7 @@ async function circularCommand(options) {
|
|
|
8430
9505
|
durationMs: Date.now() - startTime
|
|
8431
9506
|
};
|
|
8432
9507
|
}
|
|
8433
|
-
const toolResult = await discoverTool("
|
|
9508
|
+
const toolResult = await discoverTool("dependency-cruiser", { projectRoot: cwd });
|
|
8434
9509
|
if (!toolResult.found) {
|
|
8435
9510
|
const skipMessage = formatSkipMessage("circular dependency check", toolResult);
|
|
8436
9511
|
return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
|
|
@@ -8468,7 +9543,7 @@ ${cycles}`;
|
|
|
8468
9543
|
import { existsSync as existsSync3 } from "fs";
|
|
8469
9544
|
import { mkdtemp, rm, writeFile as writeFile3 } from "fs/promises";
|
|
8470
9545
|
import { tmpdir } from "os";
|
|
8471
|
-
import { isAbsolute as isAbsolute4, join as
|
|
9546
|
+
import { isAbsolute as isAbsolute4, join as join25 } from "path";
|
|
8472
9547
|
var defaultKnipProcessRunner = lifecycleProcessRunner;
|
|
8473
9548
|
var KNIP_COMMAND_TOKENS = {
|
|
8474
9549
|
COMMAND: "knip",
|
|
@@ -8496,7 +9571,7 @@ async function validateKnip2(context, runner = defaultKnipProcessRunner, deps =
|
|
|
8496
9571
|
}
|
|
8497
9572
|
async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
8498
9573
|
const scopedTsconfig = typescriptScope.filteredByValidationPaths ? await createScopedKnipTsconfig(projectRoot, typescriptScope, deps) : void 0;
|
|
8499
|
-
const localBin =
|
|
9574
|
+
const localBin = join25(projectRoot, "node_modules", ".bin", "knip");
|
|
8500
9575
|
const binary = deps.existsSync(localBin) ? localBin : "npx";
|
|
8501
9576
|
const baseArgs = scopedTsconfig === void 0 ? [] : [
|
|
8502
9577
|
KNIP_COMMAND_TOKENS.USE_TSCONFIG_FILES_FLAG,
|
|
@@ -8526,13 +9601,13 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
|
8526
9601
|
knipProcess.stderr?.on("data", (data) => {
|
|
8527
9602
|
knipError += data.toString();
|
|
8528
9603
|
});
|
|
8529
|
-
return new Promise((
|
|
9604
|
+
return new Promise((resolve6) => {
|
|
8530
9605
|
const resolveAfterCleanup = (result) => {
|
|
8531
9606
|
if (resultResolved) {
|
|
8532
9607
|
return;
|
|
8533
9608
|
}
|
|
8534
9609
|
resultResolved = true;
|
|
8535
|
-
void cleanupOnce().finally(() =>
|
|
9610
|
+
void cleanupOnce().finally(() => resolve6(result));
|
|
8536
9611
|
};
|
|
8537
9612
|
knipProcess.on("close", (code) => {
|
|
8538
9613
|
if (code === 0) {
|
|
@@ -8551,12 +9626,12 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
|
8551
9626
|
});
|
|
8552
9627
|
}
|
|
8553
9628
|
async function createScopedKnipTsconfig(projectRoot, typescriptScope, deps) {
|
|
8554
|
-
const tempDir = await deps.mkdtemp(
|
|
8555
|
-
const configPath =
|
|
8556
|
-
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);
|
|
8557
9632
|
const project = typescriptScope.filePatterns.length > 0 ? typescriptScope.filePatterns : typescriptScope.directories.map((directory) => `${directory}/**/*.{js,ts,tsx}`);
|
|
8558
9633
|
const config = {
|
|
8559
|
-
extends:
|
|
9634
|
+
extends: join25(projectRoot, TSCONFIG_FILES.full),
|
|
8560
9635
|
include: project.map(toProjectPathPattern),
|
|
8561
9636
|
exclude: typescriptScope.excludePatterns.map(toProjectPathPattern)
|
|
8562
9637
|
};
|
|
@@ -8606,12 +9681,12 @@ async function knipCommand(options) {
|
|
|
8606
9681
|
|
|
8607
9682
|
// src/validation/steps/eslint.ts
|
|
8608
9683
|
import { existsSync as existsSync5 } from "fs";
|
|
8609
|
-
import { join as
|
|
9684
|
+
import { join as join27 } from "path";
|
|
8610
9685
|
|
|
8611
9686
|
// src/validation/lint-policy.ts
|
|
8612
9687
|
import { execFileSync } from "child_process";
|
|
8613
9688
|
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
8614
|
-
import { join as
|
|
9689
|
+
import { join as join26 } from "path";
|
|
8615
9690
|
|
|
8616
9691
|
// src/validation/lint-policy-constants.ts
|
|
8617
9692
|
var LINT_POLICY_MANIFESTS = {
|
|
@@ -8659,17 +9734,17 @@ function isSpecTreeNodePath(entry) {
|
|
|
8659
9734
|
}
|
|
8660
9735
|
function readManifest(productDir, file, key) {
|
|
8661
9736
|
return parseLintPolicyManifest(
|
|
8662
|
-
readFileSync4(
|
|
9737
|
+
readFileSync4(join26(productDir, file), "utf-8"),
|
|
8663
9738
|
file,
|
|
8664
9739
|
key
|
|
8665
9740
|
);
|
|
8666
9741
|
}
|
|
8667
9742
|
function manifestExists(productDir, file) {
|
|
8668
|
-
return existsSync4(
|
|
9743
|
+
return existsSync4(join26(productDir, file));
|
|
8669
9744
|
}
|
|
8670
9745
|
function findDeprecatedSpecNodePath(productDir) {
|
|
8671
9746
|
function visit2(relativeDirectory) {
|
|
8672
|
-
const absoluteDirectory =
|
|
9747
|
+
const absoluteDirectory = join26(productDir, relativeDirectory);
|
|
8673
9748
|
for (const entry of readdirSync2(absoluteDirectory, { withFileTypes: true })) {
|
|
8674
9749
|
if (!entry.isDirectory()) {
|
|
8675
9750
|
continue;
|
|
@@ -8685,7 +9760,7 @@ function findDeprecatedSpecNodePath(productDir) {
|
|
|
8685
9760
|
}
|
|
8686
9761
|
return void 0;
|
|
8687
9762
|
}
|
|
8688
|
-
const specTreeRootPath =
|
|
9763
|
+
const specTreeRootPath = join26(productDir, SPEC_TREE_ROOT);
|
|
8689
9764
|
if (!existsSync4(specTreeRootPath)) {
|
|
8690
9765
|
return void 0;
|
|
8691
9766
|
}
|
|
@@ -8711,7 +9786,7 @@ function assertManifestEntries(productDir, file, entries, suffixPredicate, suffi
|
|
|
8711
9786
|
if (!suffixPredicate(entry)) {
|
|
8712
9787
|
throw new Error(`${file} entry must ${suffixDescription}: ${entry}`);
|
|
8713
9788
|
}
|
|
8714
|
-
const absoluteEntry =
|
|
9789
|
+
const absoluteEntry = join26(productDir, entry);
|
|
8715
9790
|
if (!existsSync4(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
|
|
8716
9791
|
throw new Error(`${file} entry does not exist as a directory: ${entry}`);
|
|
8717
9792
|
}
|
|
@@ -8970,8 +10045,8 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
|
|
|
8970
10045
|
scope,
|
|
8971
10046
|
scopeConfig: context.scopeConfig
|
|
8972
10047
|
});
|
|
8973
|
-
return new Promise((
|
|
8974
|
-
const localBin =
|
|
10048
|
+
return new Promise((resolve6) => {
|
|
10049
|
+
const localBin = join27(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
|
|
8975
10050
|
const binary = existsSync5(localBin) ? localBin : "npx";
|
|
8976
10051
|
const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
|
|
8977
10052
|
const eslintProcess = spawnManagedSubprocess(runner, binary, spawnArgs, {
|
|
@@ -8980,13 +10055,13 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
|
|
|
8980
10055
|
forwardValidationSubprocessOutput(eslintProcess, outputStreams);
|
|
8981
10056
|
eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
8982
10057
|
if (code === 0) {
|
|
8983
|
-
|
|
10058
|
+
resolve6({ success: true });
|
|
8984
10059
|
} else {
|
|
8985
|
-
|
|
10060
|
+
resolve6({ success: false, error: `ESLint exited with code ${code}` });
|
|
8986
10061
|
}
|
|
8987
10062
|
});
|
|
8988
10063
|
eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
8989
|
-
|
|
10064
|
+
resolve6({ success: false, error: error.message });
|
|
8990
10065
|
});
|
|
8991
10066
|
});
|
|
8992
10067
|
}
|
|
@@ -9073,7 +10148,7 @@ async function lintCommand(options) {
|
|
|
9073
10148
|
|
|
9074
10149
|
// src/validation/literal/index.ts
|
|
9075
10150
|
import { readFile as readFile8 } from "fs/promises";
|
|
9076
|
-
import { isAbsolute as isAbsolute5, relative as relative5, resolve as
|
|
10151
|
+
import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve5 } from "path";
|
|
9077
10152
|
|
|
9078
10153
|
// src/lib/file-inclusion/predicates/ignore-source.ts
|
|
9079
10154
|
var IGNORE_SOURCE_LAYER = "ignore-source";
|
|
@@ -9108,7 +10183,7 @@ var ignoreSourceLayer = makeLayer(
|
|
|
9108
10183
|
|
|
9109
10184
|
// src/lib/file-inclusion/pipeline.ts
|
|
9110
10185
|
import { readdir as readdir7 } from "fs/promises";
|
|
9111
|
-
import { join as
|
|
10186
|
+
import { join as join28, relative as relative4, sep as sep2 } from "path";
|
|
9112
10187
|
var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
|
|
9113
10188
|
function isNodeError3(err) {
|
|
9114
10189
|
return err instanceof Error && "code" in err;
|
|
@@ -9126,10 +10201,10 @@ async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
|
|
|
9126
10201
|
for (const entry of dirEntries) {
|
|
9127
10202
|
if (entry.isDirectory()) {
|
|
9128
10203
|
if (artifactDirs.has(entry.name)) continue;
|
|
9129
|
-
const absolutePath =
|
|
10204
|
+
const absolutePath = join28(absoluteDir, entry.name);
|
|
9130
10205
|
await collectPaths(absolutePath, projectRoot, result, artifactDirs);
|
|
9131
10206
|
} else if (entry.isFile()) {
|
|
9132
|
-
const absolutePath =
|
|
10207
|
+
const absolutePath = join28(absoluteDir, entry.name);
|
|
9133
10208
|
const rel = relative4(projectRoot, absolutePath);
|
|
9134
10209
|
result.push(sep2 === "/" ? rel : rel.split(sep2).join("/"));
|
|
9135
10210
|
}
|
|
@@ -9536,7 +10611,7 @@ async function validateLiteralReuse(input) {
|
|
|
9536
10611
|
const config = input.config ?? literalConfigDescriptor.defaults;
|
|
9537
10612
|
const request = input.files ? {
|
|
9538
10613
|
explicit: input.files.map((f) => {
|
|
9539
|
-
const abs = isAbsolute5(f) ? f :
|
|
10614
|
+
const abs = isAbsolute5(f) ? f : resolve5(input.productDir, f);
|
|
9540
10615
|
return relative5(input.productDir, abs).split(/[\\/]/g).join("/");
|
|
9541
10616
|
})
|
|
9542
10617
|
} : { walkRoot: input.productDir };
|
|
@@ -9548,7 +10623,7 @@ async function validateLiteralReuse(input) {
|
|
|
9548
10623
|
EMPTY_IGNORE_READER
|
|
9549
10624
|
);
|
|
9550
10625
|
const filtered = applyPathFilter2(scope.included, input.pathConfig);
|
|
9551
|
-
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));
|
|
9552
10627
|
const collectOptions = {
|
|
9553
10628
|
visitorKeys: defaultVisitorKeys,
|
|
9554
10629
|
minStringLength: config.minStringLength,
|
|
@@ -9789,7 +10864,7 @@ function formatLoc(loc) {
|
|
|
9789
10864
|
// src/validation/steps/typescript.ts
|
|
9790
10865
|
import { existsSync as existsSync6, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
9791
10866
|
import { mkdtemp as mkdtemp2 } from "fs/promises";
|
|
9792
|
-
import { isAbsolute as isAbsolute6, join as
|
|
10867
|
+
import { isAbsolute as isAbsolute6, join as join29 } from "path";
|
|
9793
10868
|
var defaultTypeScriptProcessRunner = lifecycleProcessRunner;
|
|
9794
10869
|
var defaultTypeScriptDeps = {
|
|
9795
10870
|
mkdtemp: mkdtemp2,
|
|
@@ -9802,9 +10877,9 @@ var TEMPORARY_TSCONFIG_COMPILER_OPTIONS = { noEmit: true };
|
|
|
9802
10877
|
var TEMPORARY_TSCONFIG_PARENT_SEGMENTS = ["node_modules", ".cache", "spx"];
|
|
9803
10878
|
var TEMPORARY_TSCONFIG_DIR_PREFIX = "validate-ts-";
|
|
9804
10879
|
async function createTemporaryTsconfigDir(projectRoot, deps) {
|
|
9805
|
-
const parent =
|
|
10880
|
+
const parent = join29(projectRoot, ...TEMPORARY_TSCONFIG_PARENT_SEGMENTS);
|
|
9806
10881
|
deps.mkdirSync(parent, { recursive: true });
|
|
9807
|
-
return deps.mkdtemp(
|
|
10882
|
+
return deps.mkdtemp(join29(parent, TEMPORARY_TSCONFIG_DIR_PREFIX));
|
|
9808
10883
|
}
|
|
9809
10884
|
function buildTypeScriptArgs(context) {
|
|
9810
10885
|
const { scope, configFile } = context;
|
|
@@ -9812,11 +10887,11 @@ function buildTypeScriptArgs(context) {
|
|
|
9812
10887
|
}
|
|
9813
10888
|
async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defaultTypeScriptDeps) {
|
|
9814
10889
|
const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
|
|
9815
|
-
const configPath =
|
|
10890
|
+
const configPath = join29(tempDir, "tsconfig.json");
|
|
9816
10891
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
9817
|
-
const absoluteFiles = files.map((file) => isAbsolute6(file) ? file :
|
|
10892
|
+
const absoluteFiles = files.map((file) => isAbsolute6(file) ? file : join29(projectRoot, file));
|
|
9818
10893
|
const tempConfig = {
|
|
9819
|
-
extends:
|
|
10894
|
+
extends: join29(projectRoot, baseConfigFile),
|
|
9820
10895
|
files: absoluteFiles,
|
|
9821
10896
|
include: [],
|
|
9822
10897
|
exclude: [],
|
|
@@ -9833,11 +10908,11 @@ async function createFileSpecificTsconfig(scope, files, projectRoot, deps = defa
|
|
|
9833
10908
|
}
|
|
9834
10909
|
async function createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps = defaultTypeScriptDeps) {
|
|
9835
10910
|
const tempDir = await createTemporaryTsconfigDir(projectRoot, deps);
|
|
9836
|
-
const configPath =
|
|
10911
|
+
const configPath = join29(tempDir, "tsconfig.json");
|
|
9837
10912
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
9838
|
-
const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern :
|
|
10913
|
+
const toProjectPathPattern = (pattern) => isAbsolute6(pattern) ? pattern : join29(projectRoot, pattern);
|
|
9839
10914
|
const tempConfig = {
|
|
9840
|
-
extends:
|
|
10915
|
+
extends: join29(projectRoot, baseConfigFile),
|
|
9841
10916
|
include: scopeConfig.filePatterns.map(toProjectPathPattern),
|
|
9842
10917
|
exclude: scopeConfig.excludePatterns.map(toProjectPathPattern),
|
|
9843
10918
|
compilerOptions: TEMPORARY_TSCONFIG_COMPILER_OPTIONS
|
|
@@ -9864,8 +10939,8 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9864
10939
|
if (files && files.length > 0) {
|
|
9865
10940
|
const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, projectRoot, deps);
|
|
9866
10941
|
try {
|
|
9867
|
-
return await new Promise((
|
|
9868
|
-
const tscBin =
|
|
10942
|
+
return await new Promise((resolve6) => {
|
|
10943
|
+
const tscBin = join29(projectRoot, "node_modules", ".bin", "tsc");
|
|
9869
10944
|
const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9870
10945
|
const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
|
|
9871
10946
|
const tscProcess = spawnManagedSubprocess(runner, tscBinary, tscArgs2, {
|
|
@@ -9875,14 +10950,14 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9875
10950
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
9876
10951
|
cleanup();
|
|
9877
10952
|
if (code === 0) {
|
|
9878
|
-
|
|
10953
|
+
resolve6({ success: true, skipped: false });
|
|
9879
10954
|
} else {
|
|
9880
|
-
|
|
10955
|
+
resolve6({ success: false, error: `TypeScript exited with code ${code}` });
|
|
9881
10956
|
}
|
|
9882
10957
|
});
|
|
9883
10958
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
9884
10959
|
cleanup();
|
|
9885
|
-
|
|
10960
|
+
resolve6({ success: false, error: error.message });
|
|
9886
10961
|
});
|
|
9887
10962
|
});
|
|
9888
10963
|
} catch (error) {
|
|
@@ -9898,10 +10973,10 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9898
10973
|
return { success: true, skipped: true };
|
|
9899
10974
|
}
|
|
9900
10975
|
const { configPath, cleanup } = await createScopeFilteredTsconfig(scope, projectRoot, scopeConfig, deps);
|
|
9901
|
-
const tscBin =
|
|
10976
|
+
const tscBin = join29(projectRoot, "node_modules", ".bin", "tsc");
|
|
9902
10977
|
tool = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9903
10978
|
tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
|
|
9904
|
-
return new Promise((
|
|
10979
|
+
return new Promise((resolve6) => {
|
|
9905
10980
|
const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
|
|
9906
10981
|
cwd: projectRoot
|
|
9907
10982
|
});
|
|
@@ -9909,36 +10984,36 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9909
10984
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
9910
10985
|
cleanup();
|
|
9911
10986
|
if (code === 0) {
|
|
9912
|
-
|
|
10987
|
+
resolve6({ success: true, skipped: false });
|
|
9913
10988
|
} else {
|
|
9914
|
-
|
|
10989
|
+
resolve6({ success: false, error: `TypeScript exited with code ${code}` });
|
|
9915
10990
|
}
|
|
9916
10991
|
});
|
|
9917
10992
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
9918
10993
|
cleanup();
|
|
9919
|
-
|
|
10994
|
+
resolve6({ success: false, error: error.message });
|
|
9920
10995
|
});
|
|
9921
10996
|
});
|
|
9922
10997
|
} else {
|
|
9923
|
-
const tscBin =
|
|
10998
|
+
const tscBin = join29(projectRoot, "node_modules", ".bin", "tsc");
|
|
9924
10999
|
tool = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9925
11000
|
const rawArgs = buildTypeScriptArgs({ scope, configFile });
|
|
9926
11001
|
tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
|
|
9927
11002
|
}
|
|
9928
|
-
return new Promise((
|
|
11003
|
+
return new Promise((resolve6) => {
|
|
9929
11004
|
const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
|
|
9930
11005
|
cwd: projectRoot
|
|
9931
11006
|
});
|
|
9932
11007
|
forwardValidationSubprocessOutput(tscProcess, outputStreams);
|
|
9933
11008
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
9934
11009
|
if (code === 0) {
|
|
9935
|
-
|
|
11010
|
+
resolve6({ success: true, skipped: false });
|
|
9936
11011
|
} else {
|
|
9937
|
-
|
|
11012
|
+
resolve6({ success: false, error: `TypeScript exited with code ${code}` });
|
|
9938
11013
|
}
|
|
9939
11014
|
});
|
|
9940
11015
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
9941
|
-
|
|
11016
|
+
resolve6({ success: false, error: error.message });
|
|
9942
11017
|
});
|
|
9943
11018
|
});
|
|
9944
11019
|
}
|
|
@@ -10013,6 +11088,13 @@ async function typescriptCommand(options) {
|
|
|
10013
11088
|
|
|
10014
11089
|
// src/validation/languages/typescript.ts
|
|
10015
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
|
+
}
|
|
10016
11098
|
async function runLiteralStage(context) {
|
|
10017
11099
|
if (context.skipLiteral) {
|
|
10018
11100
|
const skipOutput = context.json ? LITERAL_SKIP_JSON_OUTPUT : LITERAL_SKIP_OUTPUT;
|
|
@@ -10031,7 +11113,7 @@ var typescriptValidationLanguage = {
|
|
|
10031
11113
|
{
|
|
10032
11114
|
name: VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR,
|
|
10033
11115
|
failsPipeline: true,
|
|
10034
|
-
run:
|
|
11116
|
+
run: runCircularStage
|
|
10035
11117
|
},
|
|
10036
11118
|
{
|
|
10037
11119
|
name: VALIDATION_STAGE_DISPLAY_NAMES.KNIP,
|
|
@@ -10111,11 +11193,11 @@ function formatStepWithTiming(stepNumber, result, quiet) {
|
|
|
10111
11193
|
return `[${stepNumber}/${VALIDATION_PIPELINE_TOTAL_STEPS}] ${result.output}${timing}`;
|
|
10112
11194
|
}
|
|
10113
11195
|
async function allCommand(options) {
|
|
10114
|
-
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;
|
|
10115
11197
|
const startTime = Date.now();
|
|
10116
11198
|
const outputs = [];
|
|
10117
11199
|
let hasFailure = false;
|
|
10118
|
-
const context = { cwd, scope, files, fix, quiet, json, skipLiteral };
|
|
11200
|
+
const context = { cwd, scope, files, fix, quiet, json, skipCircular, skipLiteral };
|
|
10119
11201
|
let stepNumber = 0;
|
|
10120
11202
|
for (const stage of validationPipelineStages) {
|
|
10121
11203
|
stepNumber += 1;
|
|
@@ -10136,51 +11218,9 @@ async function allCommand(options) {
|
|
|
10136
11218
|
};
|
|
10137
11219
|
}
|
|
10138
11220
|
|
|
10139
|
-
// src/interfaces/cli/sanitize.ts
|
|
10140
|
-
var MAX_CLI_ARGUMENT_DISPLAY_LENGTH = 120;
|
|
10141
|
-
var ELLIPSIS_TOKEN = "...";
|
|
10142
|
-
var SENTINEL_UNDEFINED = "<undefined>";
|
|
10143
|
-
var SENTINEL_NULL = "<null>";
|
|
10144
|
-
var SENTINEL_EMPTY = "<empty>";
|
|
10145
|
-
var CONTROL_CHAR_UPPER_BOUND = 31;
|
|
10146
|
-
var DEL_CHAR_CODE = 127;
|
|
10147
|
-
var HEX_RADIX = 16;
|
|
10148
|
-
var HEX_PAD = 2;
|
|
10149
|
-
function formatHexEscape(code) {
|
|
10150
|
-
return `\\x${code.toString(HEX_RADIX).padStart(HEX_PAD, "0")}`;
|
|
10151
|
-
}
|
|
10152
|
-
function nonStringSentinel(type) {
|
|
10153
|
-
return `<non-string:${type}>`;
|
|
10154
|
-
}
|
|
10155
|
-
function sanitizeCliArgument(input) {
|
|
10156
|
-
if (input === void 0) return SENTINEL_UNDEFINED;
|
|
10157
|
-
if (input === null) return SENTINEL_NULL;
|
|
10158
|
-
if (typeof input !== "string") return nonStringSentinel(typeof input);
|
|
10159
|
-
if (input.length === 0) return SENTINEL_EMPTY;
|
|
10160
|
-
const escaped = escapeControlCharacters(input);
|
|
10161
|
-
return truncate(escaped);
|
|
10162
|
-
}
|
|
10163
|
-
function escapeControlCharacters(value) {
|
|
10164
|
-
let out = "";
|
|
10165
|
-
for (const char of value) {
|
|
10166
|
-
const code = char.codePointAt(0);
|
|
10167
|
-
if (code === void 0) continue;
|
|
10168
|
-
if (code <= CONTROL_CHAR_UPPER_BOUND || code === DEL_CHAR_CODE) {
|
|
10169
|
-
out += formatHexEscape(code);
|
|
10170
|
-
} else {
|
|
10171
|
-
out += char;
|
|
10172
|
-
}
|
|
10173
|
-
}
|
|
10174
|
-
return out;
|
|
10175
|
-
}
|
|
10176
|
-
function truncate(value) {
|
|
10177
|
-
if (value.length <= MAX_CLI_ARGUMENT_DISPLAY_LENGTH) return value;
|
|
10178
|
-
return value.slice(0, MAX_CLI_ARGUMENT_DISPLAY_LENGTH - ELLIPSIS_TOKEN.length) + ELLIPSIS_TOKEN;
|
|
10179
|
-
}
|
|
10180
|
-
|
|
10181
11221
|
// src/validation/literal/allowlist-existing.ts
|
|
10182
11222
|
import { rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
10183
|
-
import { dirname as
|
|
11223
|
+
import { dirname as dirname8, join as join30 } from "path";
|
|
10184
11224
|
var EXIT_OK = 0;
|
|
10185
11225
|
var EXIT_ERROR = 1;
|
|
10186
11226
|
var INCLUDE_FIELD = "include";
|
|
@@ -10200,9 +11240,9 @@ var productionReader = {
|
|
|
10200
11240
|
};
|
|
10201
11241
|
var productionWriter = {
|
|
10202
11242
|
async write(filePath, content) {
|
|
10203
|
-
const dir =
|
|
11243
|
+
const dir = dirname8(filePath);
|
|
10204
11244
|
const random = Math.random().toString(RANDOM_BASE).slice(RANDOM_PREFIX_SLICE, RANDOM_PREFIX_SLICE + RANDOM_TOKEN_LENGTH);
|
|
10205
|
-
const tmpPath =
|
|
11245
|
+
const tmpPath = join30(dir, `${TEMP_FILE_PREFIX}${random}${TEMP_FILE_SUFFIX}`);
|
|
10206
11246
|
await writeFile4(tmpPath, content, "utf8");
|
|
10207
11247
|
await rename4(tmpPath, filePath);
|
|
10208
11248
|
}
|
|
@@ -10352,6 +11392,10 @@ var literalValidationCliOptions = {
|
|
|
10352
11392
|
}
|
|
10353
11393
|
};
|
|
10354
11394
|
var allValidationCliOptions = {
|
|
11395
|
+
skipCircular: {
|
|
11396
|
+
flag: "--skip-circular",
|
|
11397
|
+
description: "Skip circular dependency detection for this validation all run"
|
|
11398
|
+
},
|
|
10355
11399
|
skipLiteral: {
|
|
10356
11400
|
flag: "--skip-literal",
|
|
10357
11401
|
description: "Skip literal reuse detection for this validation all run"
|
|
@@ -10480,12 +11524,13 @@ function registerValidationCommands(validationCmd) {
|
|
|
10480
11524
|
process.exit(result.exitCode);
|
|
10481
11525
|
});
|
|
10482
11526
|
addCommonOptions(markdownCmd);
|
|
10483
|
-
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) => {
|
|
10484
11528
|
const result = await allCommand({
|
|
10485
11529
|
cwd: process.cwd(),
|
|
10486
11530
|
scope: options.scope,
|
|
10487
11531
|
files: options.files,
|
|
10488
11532
|
fix: options.fix,
|
|
11533
|
+
skipCircular: options.skipCircular,
|
|
10489
11534
|
skipLiteral: options.skipLiteral,
|
|
10490
11535
|
quiet: options.quiet,
|
|
10491
11536
|
json: options.json
|
|
@@ -10523,15 +11568,159 @@ var validationDomain = {
|
|
|
10523
11568
|
}
|
|
10524
11569
|
};
|
|
10525
11570
|
|
|
11571
|
+
// src/commands/worktree/claim.ts
|
|
11572
|
+
async function claimCommand(options) {
|
|
11573
|
+
return claimWorktreeOccupancy(options);
|
|
11574
|
+
}
|
|
11575
|
+
|
|
11576
|
+
// src/commands/worktree/release.ts
|
|
11577
|
+
async function releaseCommand2(options) {
|
|
11578
|
+
const worktreesDir = await resolveWorktreesDir(options);
|
|
11579
|
+
const name = await resolveCurrentWorktreeName(options);
|
|
11580
|
+
return removeClaim(worktreesDir, name, { fs: options.fs });
|
|
11581
|
+
}
|
|
11582
|
+
|
|
11583
|
+
// src/commands/worktree/status.ts
|
|
11584
|
+
var WORKTREE_STATUS_FORMAT = {
|
|
11585
|
+
JSON: "json",
|
|
11586
|
+
TEXT: "text"
|
|
11587
|
+
};
|
|
11588
|
+
async function statusCommand2(options) {
|
|
11589
|
+
const multiTargetRequest = options.worktrees !== void 0 && options.worktrees.length > 1;
|
|
11590
|
+
const targets = await resolveStatusTargets(options);
|
|
11591
|
+
if (!targets.ok) return targets;
|
|
11592
|
+
const records = [];
|
|
11593
|
+
for (const target of targets.value) {
|
|
11594
|
+
const worktreesDir = await resolveWorktreesDir({ ...options, cwd: target.worktreeRoot });
|
|
11595
|
+
const occupancy = await readOccupancy(worktreesDir, target.name, options.processTable, { fs: options.fs });
|
|
11596
|
+
if (!occupancy.ok) return occupancy;
|
|
11597
|
+
records.push({ worktree: target.name, status: occupancy.value });
|
|
11598
|
+
}
|
|
11599
|
+
return { ok: true, value: renderStatus(records, options.format, multiTargetRequest) };
|
|
11600
|
+
}
|
|
11601
|
+
async function resolveStatusTargets(options) {
|
|
11602
|
+
const requested = options.worktrees;
|
|
11603
|
+
if (requested === void 0 || requested.length === 0) {
|
|
11604
|
+
const target = await resolveTargetWorktree(options);
|
|
11605
|
+
if (!target.ok) return target;
|
|
11606
|
+
return { ok: true, value: [target.value] };
|
|
11607
|
+
}
|
|
11608
|
+
const targets = [];
|
|
11609
|
+
let firstError;
|
|
11610
|
+
for (const worktree of requested) {
|
|
11611
|
+
const target = await resolveTargetWorktree({ ...options, worktree });
|
|
11612
|
+
if (target.ok) {
|
|
11613
|
+
targets.push(target.value);
|
|
11614
|
+
} else {
|
|
11615
|
+
firstError ??= target.error;
|
|
11616
|
+
}
|
|
11617
|
+
}
|
|
11618
|
+
if (targets.length === 0) {
|
|
11619
|
+
return { ok: false, error: firstError ?? "no worktree status targets resolved" };
|
|
11620
|
+
}
|
|
11621
|
+
return { ok: true, value: targets };
|
|
11622
|
+
}
|
|
11623
|
+
function renderStatus(records, format2, multiTargetRequest) {
|
|
11624
|
+
if (format2 === WORKTREE_STATUS_FORMAT.JSON) {
|
|
11625
|
+
return JSON.stringify(multiTargetRequest ? records : records[0]);
|
|
11626
|
+
}
|
|
11627
|
+
return records.map(renderTextStatus).join("\n");
|
|
11628
|
+
}
|
|
11629
|
+
function renderTextStatus(record) {
|
|
11630
|
+
return `${record.worktree} ${record.status}`;
|
|
11631
|
+
}
|
|
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
|
+
|
|
11646
|
+
// src/interfaces/cli/worktree.ts
|
|
11647
|
+
var WORKTREE_CLI = {
|
|
11648
|
+
COMMAND: "worktree",
|
|
11649
|
+
CLAIM: "claim",
|
|
11650
|
+
STATUS: "status",
|
|
11651
|
+
RELEASE: "release",
|
|
11652
|
+
WORKTREE_ARGUMENT: "[worktrees...]",
|
|
11653
|
+
SESSION_ID_FLAG: "--session-id",
|
|
11654
|
+
FORMAT_FLAG: "--format",
|
|
11655
|
+
WORKTREES_DIR_FLAG: "--worktrees-dir"
|
|
11656
|
+
};
|
|
11657
|
+
var WORKTREE_DOMAIN_DESCRIPTION = "Coordinate worktree occupancy across a bare-repository pool";
|
|
11658
|
+
function handleError2(error) {
|
|
11659
|
+
console.error("Error:", error);
|
|
11660
|
+
process.exit(1);
|
|
11661
|
+
}
|
|
11662
|
+
function registerWorktreeCommands(worktreeCmd) {
|
|
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) => {
|
|
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,
|
|
11672
|
+
sessionId: options.sessionId,
|
|
11673
|
+
worktreesDir: options.worktreesDir,
|
|
11674
|
+
onWarning: writeWarning
|
|
11675
|
+
});
|
|
11676
|
+
if (!result.ok) handleError2(result.error);
|
|
11677
|
+
});
|
|
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) => {
|
|
11679
|
+
const result = await statusCommand2({
|
|
11680
|
+
cwd: process.cwd(),
|
|
11681
|
+
fs: defaultOccupancyFileSystem,
|
|
11682
|
+
gitDeps: defaultGitDependencies,
|
|
11683
|
+
worktrees,
|
|
11684
|
+
format: options.format,
|
|
11685
|
+
pathInfo: defaultWorktreePathInfo,
|
|
11686
|
+
processTable: defaultProcessTable,
|
|
11687
|
+
worktreesDir: options.worktreesDir,
|
|
11688
|
+
onWarning: writeWarning
|
|
11689
|
+
});
|
|
11690
|
+
if (!result.ok) handleError2(result.error);
|
|
11691
|
+
console.log(result.value);
|
|
11692
|
+
});
|
|
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) => {
|
|
11694
|
+
const result = await releaseCommand2({
|
|
11695
|
+
cwd: process.cwd(),
|
|
11696
|
+
fs: defaultOccupancyFileSystem,
|
|
11697
|
+
gitDeps: defaultGitDependencies,
|
|
11698
|
+
worktreesDir: options.worktreesDir,
|
|
11699
|
+
onWarning: writeWarning
|
|
11700
|
+
});
|
|
11701
|
+
if (!result.ok) handleError2(result.error);
|
|
11702
|
+
});
|
|
11703
|
+
}
|
|
11704
|
+
var worktreeDomain = {
|
|
11705
|
+
name: WORKTREE_CLI.COMMAND,
|
|
11706
|
+
description: WORKTREE_DOMAIN_DESCRIPTION,
|
|
11707
|
+
register: (program2) => {
|
|
11708
|
+
const worktreeCmd = program2.command(WORKTREE_CLI.COMMAND).description(WORKTREE_DOMAIN_DESCRIPTION);
|
|
11709
|
+
registerWorktreeCommands(worktreeCmd);
|
|
11710
|
+
}
|
|
11711
|
+
};
|
|
11712
|
+
|
|
10526
11713
|
// src/interfaces/cli/registry.ts
|
|
10527
11714
|
var CLI_DOMAINS = [
|
|
10528
11715
|
claudeDomain,
|
|
10529
11716
|
compactDomain,
|
|
10530
11717
|
configDomain,
|
|
11718
|
+
hookDomain,
|
|
10531
11719
|
sessionDomain,
|
|
10532
11720
|
specDomain,
|
|
10533
11721
|
testingDomain,
|
|
10534
|
-
validationDomain
|
|
11722
|
+
validationDomain,
|
|
11723
|
+
worktreeDomain
|
|
10535
11724
|
];
|
|
10536
11725
|
|
|
10537
11726
|
// src/cli.ts
|