@outcomeeng/spx 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +534 -302
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -49,38 +49,44 @@ async function findSettingsFiles(root, visited = /* @__PURE__ */ new Set()) {
|
|
|
49
49
|
}
|
|
50
50
|
visited.add(normalizedRoot);
|
|
51
51
|
try {
|
|
52
|
-
|
|
53
|
-
if (!stats.isDirectory()) {
|
|
54
|
-
throw new Error(`Path is not a directory: ${normalizedRoot}`);
|
|
55
|
-
}
|
|
56
|
-
const entries = await fs.readdir(normalizedRoot, { withFileTypes: true });
|
|
57
|
-
const results = [];
|
|
58
|
-
for (const entry of entries) {
|
|
59
|
-
const fullPath = path.join(normalizedRoot, entry.name);
|
|
60
|
-
if (entry.isDirectory() && entry.name === ".claude") {
|
|
61
|
-
const settingsPath = path.join(fullPath, "settings.local.json");
|
|
62
|
-
if (await isValidSettingsFile(settingsPath)) {
|
|
63
|
-
results.push(settingsPath);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (entry.isDirectory() && entry.name !== ".claude") {
|
|
67
|
-
const subFiles = await findSettingsFiles(fullPath, visited);
|
|
68
|
-
results.push(...subFiles);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return results;
|
|
52
|
+
return await findSettingsFilesInDirectory(normalizedRoot, visited);
|
|
72
53
|
} catch (error) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
54
|
+
throw discoveryError(normalizedRoot, error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function findSettingsFilesInDirectory(normalizedRoot, visited) {
|
|
58
|
+
const stats = await fs.stat(normalizedRoot);
|
|
59
|
+
if (!stats.isDirectory()) {
|
|
60
|
+
throw new Error(`Path is not a directory: ${normalizedRoot}`);
|
|
61
|
+
}
|
|
62
|
+
const entries = await fs.readdir(normalizedRoot, { withFileTypes: true });
|
|
63
|
+
const results = [];
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
results.push(...await settingsFilesForEntry(normalizedRoot, entry, visited));
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
async function settingsFilesForEntry(normalizedRoot, entry, visited) {
|
|
70
|
+
if (!entry.isDirectory()) return [];
|
|
71
|
+
const fullPath = path.join(normalizedRoot, entry.name);
|
|
72
|
+
if (entry.name === ".claude") {
|
|
73
|
+
const settingsPath = path.join(fullPath, "settings.local.json");
|
|
74
|
+
return await isValidSettingsFile(settingsPath) ? [settingsPath] : [];
|
|
83
75
|
}
|
|
76
|
+
return findSettingsFiles(fullPath, visited);
|
|
77
|
+
}
|
|
78
|
+
function discoveryError(normalizedRoot, error) {
|
|
79
|
+
if (!(error instanceof Error)) return new Error(unknownErrorMessage(error));
|
|
80
|
+
if (error.message.includes("ENOENT")) return new Error(`Directory not found: ${normalizedRoot}`);
|
|
81
|
+
if (error.message.includes("EACCES")) return new Error(`Permission denied: ${normalizedRoot}`);
|
|
82
|
+
return new Error(`Failed to search directory "${normalizedRoot}": ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
function unknownErrorMessage(error) {
|
|
85
|
+
if (typeof error === "string") return error;
|
|
86
|
+
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return error.toString();
|
|
87
|
+
if (typeof error === "symbol") return error.description ?? "Symbol()";
|
|
88
|
+
if (error === void 0) return "undefined";
|
|
89
|
+
return Object.prototype.toString.call(error);
|
|
84
90
|
}
|
|
85
91
|
async function isValidSettingsFile(filePath) {
|
|
86
92
|
try {
|
|
@@ -801,6 +807,7 @@ function toErrorMessage(error) {
|
|
|
801
807
|
|
|
802
808
|
// src/lib/claude/permissions/parser.ts
|
|
803
809
|
import fs2 from "fs/promises";
|
|
810
|
+
var PERMISSION_PATTERN = /^([^(]+)\((.+)\)$/;
|
|
804
811
|
async function parseSettingsFile(filePath) {
|
|
805
812
|
try {
|
|
806
813
|
const content = await fs2.readFile(filePath, "utf-8");
|
|
@@ -814,7 +821,7 @@ async function parseSettingsFile(filePath) {
|
|
|
814
821
|
}
|
|
815
822
|
}
|
|
816
823
|
function parsePermission(raw, category) {
|
|
817
|
-
const match =
|
|
824
|
+
const match = PERMISSION_PATTERN.exec(raw);
|
|
818
825
|
if (!match) {
|
|
819
826
|
throw new Error(`Malformed permission string: "${raw}"`);
|
|
820
827
|
}
|
|
@@ -1095,88 +1102,91 @@ async function createBackup(settingsPath) {
|
|
|
1095
1102
|
|
|
1096
1103
|
// src/lib/claude/settings/reporter.ts
|
|
1097
1104
|
function formatReport(result, previewOnly, globalSettingsPath, outputFile) {
|
|
1098
|
-
const lines = [];
|
|
1099
|
-
lines.push("Scanning for Claude Code settings files...");
|
|
1100
|
-
lines.push("");
|
|
1101
|
-
lines.push(`Found ${result.filesScanned} settings files`);
|
|
1102
|
-
lines.push(` Processed: ${result.filesProcessed}`);
|
|
1103
|
-
if (result.filesSkipped > 0) {
|
|
1104
|
-
lines.push(` Skipped: ${result.filesSkipped} (no permissions)`);
|
|
1105
|
-
}
|
|
1106
|
-
lines.push("");
|
|
1107
1105
|
const totalAdded = result.added.allow.length + result.added.deny.length + result.added.ask.length;
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1106
|
+
return [
|
|
1107
|
+
...headerLines(),
|
|
1108
|
+
...filesSummaryLines(result),
|
|
1109
|
+
...permissionLines(result, totalAdded),
|
|
1110
|
+
...subsumptionLines(result),
|
|
1111
|
+
...conflictLines(result),
|
|
1112
|
+
...backupLines(result),
|
|
1113
|
+
...summaryLines(result),
|
|
1114
|
+
...finalStatusLines(result, previewOnly, globalSettingsPath, outputFile)
|
|
1115
|
+
].join("\n");
|
|
1116
|
+
}
|
|
1117
|
+
function headerLines() {
|
|
1118
|
+
return ["Scanning for Claude Code settings files...", ""];
|
|
1119
|
+
}
|
|
1120
|
+
function filesSummaryLines(result) {
|
|
1121
|
+
const lines = [`Found ${result.filesScanned} settings files`, ` Processed: ${result.filesProcessed}`];
|
|
1122
|
+
if (result.filesSkipped > 0) lines.push(` Skipped: ${result.filesSkipped} (no permissions)`);
|
|
1123
|
+
return [...lines, ""];
|
|
1124
|
+
}
|
|
1125
|
+
function permissionLines(result, totalAdded) {
|
|
1126
|
+
if (totalAdded === 0) return ["No new permissions to add (all permissions already in global settings)", ""];
|
|
1127
|
+
return [
|
|
1128
|
+
`Permissions to add: ${totalAdded}`,
|
|
1129
|
+
...permissionCategoryLines("allow", result.added.allow),
|
|
1130
|
+
...permissionCategoryLines("deny", result.added.deny),
|
|
1131
|
+
...permissionCategoryLines("ask", result.added.ask),
|
|
1132
|
+
""
|
|
1133
|
+
];
|
|
1134
|
+
}
|
|
1135
|
+
function permissionCategoryLines(label, permissions) {
|
|
1136
|
+
if (permissions.length === 0) return [];
|
|
1137
|
+
return ["", ` ${label}:`, ...permissions.map((permission) => ` + ${permission}`)];
|
|
1138
|
+
}
|
|
1139
|
+
function subsumptionLines(result) {
|
|
1140
|
+
if (result.subsumed.length === 0) return [];
|
|
1141
|
+
return [
|
|
1142
|
+
`Subsumed permissions removed: ${result.subsumed.length}`,
|
|
1143
|
+
" (narrower permissions replaced by broader ones)",
|
|
1144
|
+
...result.subsumed.map((permission) => ` - ${permission}`),
|
|
1145
|
+
""
|
|
1146
|
+
];
|
|
1147
|
+
}
|
|
1148
|
+
function conflictLines(result) {
|
|
1149
|
+
if (result.conflictsResolved === 0) return [];
|
|
1150
|
+
return [`Conflicts resolved: ${result.conflictsResolved}`, " (permissions moved from allow to deny)", ""];
|
|
1151
|
+
}
|
|
1152
|
+
function backupLines(result) {
|
|
1153
|
+
return result.backupPath ? [`Backup created: ${result.backupPath}`, ""] : [];
|
|
1154
|
+
}
|
|
1155
|
+
function summaryLines(result) {
|
|
1156
|
+
return [
|
|
1157
|
+
"Summary:",
|
|
1158
|
+
` Files scanned: ${result.filesScanned}`,
|
|
1159
|
+
` Permissions added: ${result.added.allow.length} allow, ${result.added.deny.length} deny, ${result.added.ask.length} ask`,
|
|
1160
|
+
...optionalSummaryLines(result),
|
|
1161
|
+
""
|
|
1162
|
+
];
|
|
1163
|
+
}
|
|
1164
|
+
function optionalSummaryLines(result) {
|
|
1165
|
+
return [
|
|
1166
|
+
...result.subsumed.length > 0 ? [` Subsumed removed: ${result.subsumed.length}`] : [],
|
|
1167
|
+
...result.conflictsResolved > 0 ? [` Conflicts resolved: ${result.conflictsResolved}`] : []
|
|
1168
|
+
];
|
|
1169
|
+
}
|
|
1170
|
+
function finalStatusLines(result, previewOnly, globalSettingsPath, outputFile) {
|
|
1164
1171
|
if (previewOnly) {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
lines.push("");
|
|
1173
|
-
lines.push("To apply to your global settings:");
|
|
1174
|
-
lines.push(` \u2022 Review the file, then copy to: ${globalSettingsPath || "~/.claude/settings.json"}`);
|
|
1175
|
-
lines.push(` \u2022 Or run: spx claude settings consolidate --write`);
|
|
1176
|
-
} else {
|
|
1177
|
-
lines.push(`\u2713 Global settings updated: ${globalSettingsPath || "~/.claude/settings.json"}`);
|
|
1172
|
+
return [
|
|
1173
|
+
"\u2139\uFE0F Preview mode: No changes written",
|
|
1174
|
+
"",
|
|
1175
|
+
"To apply changes:",
|
|
1176
|
+
" \u2022 Modify global settings: spx claude settings consolidate --write",
|
|
1177
|
+
" \u2022 Write to file: spx claude settings consolidate --output-file /path/to/file"
|
|
1178
|
+
];
|
|
1178
1179
|
}
|
|
1179
|
-
|
|
1180
|
+
if (outputFile) {
|
|
1181
|
+
return [
|
|
1182
|
+
`\u2713 Settings written to: ${result.outputPath || outputFile}`,
|
|
1183
|
+
"",
|
|
1184
|
+
"To apply to your global settings:",
|
|
1185
|
+
` \u2022 Review the file, then copy to: ${globalSettingsPath || "~/.claude/settings.json"}`,
|
|
1186
|
+
" \u2022 Or run: spx claude settings consolidate --write"
|
|
1187
|
+
];
|
|
1188
|
+
}
|
|
1189
|
+
return [`\u2713 Global settings updated: ${globalSettingsPath || "~/.claude/settings.json"}`];
|
|
1180
1190
|
}
|
|
1181
1191
|
|
|
1182
1192
|
// src/lib/claude/settings/writer.ts
|
|
@@ -1403,7 +1413,7 @@ var COMPACT_MARKER = {
|
|
|
1403
1413
|
FOUNDATION: "SPEC_TREE_FOUNDATION",
|
|
1404
1414
|
CONTEXT: "SPEC_TREE_CONTEXT",
|
|
1405
1415
|
TARGET_ATTRIBUTE: "target",
|
|
1406
|
-
ESCAPED_TARGET_QUOTE:
|
|
1416
|
+
ESCAPED_TARGET_QUOTE: String.raw`\"`,
|
|
1407
1417
|
UNESCAPED_TARGET_QUOTE: '"'
|
|
1408
1418
|
};
|
|
1409
1419
|
var COMPACT_ERROR = {
|
|
@@ -1510,8 +1520,8 @@ function readTargetPath(value, startIndex) {
|
|
|
1510
1520
|
return value.slice(startIndex, endIndex);
|
|
1511
1521
|
}
|
|
1512
1522
|
function isTargetPathCharacter(character) {
|
|
1513
|
-
const code = character.
|
|
1514
|
-
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || character === "." || character === "_" || character === "-" || character === "/";
|
|
1523
|
+
const code = character.codePointAt(0);
|
|
1524
|
+
return code !== void 0 && (code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || character === "." || character === "_" || character === "-" || character === "/");
|
|
1515
1525
|
}
|
|
1516
1526
|
|
|
1517
1527
|
// src/commands/compact/retrieve.ts
|
|
@@ -2826,7 +2836,7 @@ var PATH_SEGMENT_SEPARATOR = "/";
|
|
|
2826
2836
|
function normalizePathPrefix(value) {
|
|
2827
2837
|
let end = value.length;
|
|
2828
2838
|
while (end > 0 && value[end - 1] === PATH_SEGMENT_SEPARATOR) {
|
|
2829
|
-
end
|
|
2839
|
+
end -= 1;
|
|
2830
2840
|
}
|
|
2831
2841
|
return value.slice(0, end);
|
|
2832
2842
|
}
|
|
@@ -2898,6 +2908,10 @@ var DEFAULT_MIN_STRING_LENGTH = 4;
|
|
|
2898
2908
|
var DEFAULT_MIN_NUMBER_DIGITS = 4;
|
|
2899
2909
|
var LEGACY_LITERAL_ALLOWLIST_FIELD = "allowlist";
|
|
2900
2910
|
var LEGACY_LITERAL_ALLOWLIST_ERROR = "validation.literal.values.allowlist is no longer valid; move its contents up one level to validation.literal.values.{presets,include,exclude}";
|
|
2911
|
+
var LITERAL_STRING_LIST_FIELDS = {
|
|
2912
|
+
INCLUDE: "include",
|
|
2913
|
+
EXCLUDE: "exclude"
|
|
2914
|
+
};
|
|
2901
2915
|
var PRESET_NAMES = {
|
|
2902
2916
|
WEB: "web"
|
|
2903
2917
|
};
|
|
@@ -2946,9 +2960,6 @@ var LITERAL_DEFAULTS = {
|
|
|
2946
2960
|
minStringLength: DEFAULT_MIN_STRING_LENGTH,
|
|
2947
2961
|
minNumberDigits: DEFAULT_MIN_NUMBER_DIGITS
|
|
2948
2962
|
};
|
|
2949
|
-
function isStringArray(value) {
|
|
2950
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2951
|
-
}
|
|
2952
2963
|
function isNonNegativeInteger(value) {
|
|
2953
2964
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
2954
2965
|
}
|
|
@@ -2960,24 +2971,12 @@ function validate7(value) {
|
|
|
2960
2971
|
if (candidate[LEGACY_LITERAL_ALLOWLIST_FIELD] !== void 0) {
|
|
2961
2972
|
return { ok: false, error: LEGACY_LITERAL_ALLOWLIST_ERROR };
|
|
2962
2973
|
}
|
|
2963
|
-
const presets = candidate
|
|
2964
|
-
if (presets
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
if (unknownPreset !== void 0) {
|
|
2970
|
-
return { ok: false, error: `${LITERAL_SECTION}.presets: unrecognized preset "${unknownPreset}"` };
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
const include = candidate["include"];
|
|
2974
|
-
if (include !== void 0 && !isStringArray(include)) {
|
|
2975
|
-
return { ok: false, error: `${LITERAL_SECTION}.include must be an array of strings` };
|
|
2976
|
-
}
|
|
2977
|
-
const exclude = candidate["exclude"];
|
|
2978
|
-
if (exclude !== void 0 && !isStringArray(exclude)) {
|
|
2979
|
-
return { ok: false, error: `${LITERAL_SECTION}.exclude must be an array of strings` };
|
|
2980
|
-
}
|
|
2974
|
+
const presets = readPresetList(candidate);
|
|
2975
|
+
if (!presets.ok) return presets;
|
|
2976
|
+
const include = readStringList(candidate, LITERAL_STRING_LIST_FIELDS.INCLUDE);
|
|
2977
|
+
if (!include.ok) return include;
|
|
2978
|
+
const exclude = readStringList(candidate, LITERAL_STRING_LIST_FIELDS.EXCLUDE);
|
|
2979
|
+
if (!exclude.ok) return exclude;
|
|
2981
2980
|
const minStringLength = candidate["minStringLength"] ?? LITERAL_DEFAULTS.minStringLength;
|
|
2982
2981
|
if (!isNonNegativeInteger(minStringLength)) {
|
|
2983
2982
|
return {
|
|
@@ -2995,14 +2994,36 @@ function validate7(value) {
|
|
|
2995
2994
|
return {
|
|
2996
2995
|
ok: true,
|
|
2997
2996
|
value: {
|
|
2998
|
-
presets,
|
|
2999
|
-
include,
|
|
3000
|
-
exclude,
|
|
2997
|
+
presets: presets.value,
|
|
2998
|
+
include: include.value,
|
|
2999
|
+
exclude: exclude.value,
|
|
3001
3000
|
minStringLength,
|
|
3002
3001
|
minNumberDigits
|
|
3003
3002
|
}
|
|
3004
3003
|
};
|
|
3005
3004
|
}
|
|
3005
|
+
function readPresetList(candidate) {
|
|
3006
|
+
const presets = candidate["presets"];
|
|
3007
|
+
if (presets === void 0) return { ok: true, value: void 0 };
|
|
3008
|
+
if (!isStringArray(presets)) {
|
|
3009
|
+
return { ok: false, error: `${LITERAL_SECTION}.presets must be an array of strings` };
|
|
3010
|
+
}
|
|
3011
|
+
for (const id of presets) {
|
|
3012
|
+
if (!PRESET_REGISTRY.has(id)) {
|
|
3013
|
+
return { ok: false, error: `${LITERAL_SECTION}.presets: unrecognized preset "${id}"` };
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
return { ok: true, value: presets };
|
|
3017
|
+
}
|
|
3018
|
+
function readStringList(candidate, field) {
|
|
3019
|
+
const value = candidate[field];
|
|
3020
|
+
if (value === void 0) return { ok: true, value: void 0 };
|
|
3021
|
+
if (!isStringArray(value)) return { ok: false, error: `${LITERAL_SECTION}.${field} must be an array of strings` };
|
|
3022
|
+
return { ok: true, value };
|
|
3023
|
+
}
|
|
3024
|
+
function isStringArray(value) {
|
|
3025
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
3026
|
+
}
|
|
3006
3027
|
var literalConfigDescriptor = {
|
|
3007
3028
|
section: LITERAL_SECTION,
|
|
3008
3029
|
defaults: LITERAL_DEFAULTS,
|
|
@@ -3990,9 +4011,8 @@ function nonEmptyString(value) {
|
|
|
3990
4011
|
// src/domains/worktree/occupancy-store.ts
|
|
3991
4012
|
import { join as join6 } from "path";
|
|
3992
4013
|
var OCCUPANCY_STATUS = {
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
STALE: "stale"
|
|
4014
|
+
FREE: "free",
|
|
4015
|
+
RUNNING: "running"
|
|
3996
4016
|
};
|
|
3997
4017
|
var OCCUPANCY_CLAIM = {
|
|
3998
4018
|
FILE_EXTENSION: ".claim",
|
|
@@ -4022,13 +4042,13 @@ function claimTempFilePath(claimPath, writeToken) {
|
|
|
4022
4042
|
return { ok: true, value: `${claimPath}.${validated.value}${OCCUPANCY_CLAIM.TEMP_EXTENSION}` };
|
|
4023
4043
|
}
|
|
4024
4044
|
function classifyOccupancy(claim, probe) {
|
|
4025
|
-
if (claim === void 0) return OCCUPANCY_STATUS.
|
|
4026
|
-
if (claim.host !== probe.currentHost()) return OCCUPANCY_STATUS.
|
|
4027
|
-
if (!probe.isAlive(claim.pid)) return OCCUPANCY_STATUS.
|
|
4045
|
+
if (claim === void 0) return OCCUPANCY_STATUS.FREE;
|
|
4046
|
+
if (claim.host !== probe.currentHost()) return OCCUPANCY_STATUS.FREE;
|
|
4047
|
+
if (!probe.isAlive(claim.pid)) return OCCUPANCY_STATUS.FREE;
|
|
4048
|
+
if (claim.startedAt === unreadableStartedAt(claim.pid)) return OCCUPANCY_STATUS.RUNNING;
|
|
4028
4049
|
const liveStartTime = probe.startTimeOf(claim.pid);
|
|
4029
|
-
if (
|
|
4030
|
-
|
|
4031
|
-
return OCCUPANCY_STATUS.OCCUPIED;
|
|
4050
|
+
if (liveStartTime !== void 0 && liveStartTime !== claim.startedAt) return OCCUPANCY_STATUS.FREE;
|
|
4051
|
+
return OCCUPANCY_STATUS.RUNNING;
|
|
4032
4052
|
}
|
|
4033
4053
|
function unreadableStartedAt(pid) {
|
|
4034
4054
|
return `${OCCUPANCY_CLAIM.UNREADABLE_STARTED_AT_PREFIX}${pid}`;
|
|
@@ -4070,11 +4090,6 @@ async function removeClaim(worktreesDir, name, options) {
|
|
|
4070
4090
|
return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_REMOVE_FAILED, toErrorMessage2(error)) };
|
|
4071
4091
|
}
|
|
4072
4092
|
}
|
|
4073
|
-
async function readOccupancy(worktreesDir, name, probe, options) {
|
|
4074
|
-
const claimResult = await readClaim(worktreesDir, name, options);
|
|
4075
|
-
if (!claimResult.ok) return claimResult;
|
|
4076
|
-
return { ok: true, value: classifyOccupancy(claimResult.value, probe) };
|
|
4077
|
-
}
|
|
4078
4093
|
function serializeClaim(record6) {
|
|
4079
4094
|
return JSON.stringify({
|
|
4080
4095
|
sessionId: record6.sessionId,
|
|
@@ -4102,7 +4117,10 @@ function formatOccupancyError(code, detail) {
|
|
|
4102
4117
|
return `${code}${ERROR_DETAIL_SEPARATOR2}${detail}`;
|
|
4103
4118
|
}
|
|
4104
4119
|
function toErrorMessage2(error) {
|
|
4105
|
-
|
|
4120
|
+
if (error instanceof Error) return error.message;
|
|
4121
|
+
if (typeof error === "string") return error;
|
|
4122
|
+
if (typeof error === "number" || typeof error === "bigint" || typeof error === "boolean") return error.toString();
|
|
4123
|
+
return JSON.stringify(error) ?? "unknown error";
|
|
4106
4124
|
}
|
|
4107
4125
|
|
|
4108
4126
|
// src/domains/worktree/worktree-name.ts
|
|
@@ -4252,27 +4270,25 @@ var defaultWorktreePoolProbe = {
|
|
|
4252
4270
|
errored: true,
|
|
4253
4271
|
bareRepository: false,
|
|
4254
4272
|
linkedWorktrees: false,
|
|
4255
|
-
|
|
4273
|
+
running: 0,
|
|
4274
|
+
free: 0
|
|
4256
4275
|
};
|
|
4276
|
+
const root = await detectGitCommonDirProductRoot();
|
|
4257
4277
|
const facts = await gatherGitFacts();
|
|
4258
|
-
if (!facts?.worktreeListRead) return errored;
|
|
4278
|
+
if (!root.isGitRepo || !facts?.worktreeListRead) return errored;
|
|
4259
4279
|
const bareRepository = facts.commonDirIsBare;
|
|
4260
4280
|
const paths = facts.worktreeRoots;
|
|
4261
4281
|
const linkedWorktrees = !bareRepository && paths.length > 1;
|
|
4262
|
-
const
|
|
4263
|
-
|
|
4264
|
-
let
|
|
4282
|
+
const worktreesDir = worktreesScopeDir(root.productDir);
|
|
4283
|
+
let running = 0;
|
|
4284
|
+
let free = 0;
|
|
4265
4285
|
for (const path7 of paths) {
|
|
4266
|
-
const
|
|
4267
|
-
if (!
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
if (occupancy === OCCUPANCY_STATUS.STALE) {
|
|
4271
|
-
staleClaim = true;
|
|
4272
|
-
break;
|
|
4273
|
-
}
|
|
4286
|
+
const claim = await readClaim(worktreesDir, worktreeClaimName(path7), { fs: defaultOccupancyFileSystem });
|
|
4287
|
+
if (!claim.ok) return errored;
|
|
4288
|
+
if (classifyOccupancy(claim.value, defaultProcessTable) === OCCUPANCY_STATUS.RUNNING) running += 1;
|
|
4289
|
+
else free += 1;
|
|
4274
4290
|
}
|
|
4275
|
-
return { errored: false, bareRepository, linkedWorktrees,
|
|
4291
|
+
return { errored: false, bareRepository, linkedWorktrees, running, free };
|
|
4276
4292
|
}
|
|
4277
4293
|
};
|
|
4278
4294
|
var defaultSessionEnvironmentProbe = {
|
|
@@ -4281,22 +4297,21 @@ var defaultSessionEnvironmentProbe = {
|
|
|
4281
4297
|
const sessionIdentity = resolveAgentSessionId(process.env) !== void 0;
|
|
4282
4298
|
const spx = resolveSpx();
|
|
4283
4299
|
if (spx === null) {
|
|
4284
|
-
return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false
|
|
4300
|
+
return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false };
|
|
4285
4301
|
}
|
|
4286
4302
|
const status = await runCapture(spx, ["worktree", "status", "--format", "json"]);
|
|
4287
4303
|
if (!status.ok) {
|
|
4288
|
-
return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false
|
|
4304
|
+
return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false };
|
|
4289
4305
|
}
|
|
4290
4306
|
const occupancy = worktreeStatusOf(status.stdout);
|
|
4291
4307
|
if (occupancy === null) {
|
|
4292
|
-
return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false
|
|
4308
|
+
return { errored: true, hookPresent, sessionIdentity, worktreeClaimed: false };
|
|
4293
4309
|
}
|
|
4294
4310
|
return {
|
|
4295
4311
|
errored: false,
|
|
4296
4312
|
hookPresent,
|
|
4297
4313
|
sessionIdentity,
|
|
4298
|
-
worktreeClaimed: occupancy === OCCUPANCY_STATUS.
|
|
4299
|
-
roundTripStale: occupancy === OCCUPANCY_STATUS.STALE
|
|
4314
|
+
worktreeClaimed: occupancy === OCCUPANCY_STATUS.RUNNING
|
|
4300
4315
|
};
|
|
4301
4316
|
}
|
|
4302
4317
|
};
|
|
@@ -4310,7 +4325,7 @@ async function claimedSessionIds() {
|
|
|
4310
4325
|
for (const path7 of facts.worktreeRoots) {
|
|
4311
4326
|
const claim = await readClaim(worktreesDir, worktreeClaimName(path7), { fs: defaultOccupancyFileSystem });
|
|
4312
4327
|
if (!claim.ok || claim.value === void 0) continue;
|
|
4313
|
-
if (classifyOccupancy(claim.value, defaultProcessTable) === OCCUPANCY_STATUS.
|
|
4328
|
+
if (classifyOccupancy(claim.value, defaultProcessTable) === OCCUPANCY_STATUS.RUNNING) {
|
|
4314
4329
|
sessionIds.add(normalizeAgentSessionToken(claim.value.sessionId));
|
|
4315
4330
|
}
|
|
4316
4331
|
}
|
|
@@ -4525,8 +4540,7 @@ function record2(verdict, bucket, reading) {
|
|
|
4525
4540
|
readings: {
|
|
4526
4541
|
hook: String(reading.hookPresent),
|
|
4527
4542
|
identity: String(reading.sessionIdentity),
|
|
4528
|
-
claimed: String(reading.worktreeClaimed)
|
|
4529
|
-
stale: String(reading.roundTripStale)
|
|
4543
|
+
claimed: String(reading.worktreeClaimed)
|
|
4530
4544
|
},
|
|
4531
4545
|
remediation: REMEDIATION2[verdict]
|
|
4532
4546
|
};
|
|
@@ -4535,7 +4549,7 @@ function classifySessionEnvironment(reading) {
|
|
|
4535
4549
|
if (!reading.hookPresent) {
|
|
4536
4550
|
return record2(SESSION_ENVIRONMENT_VERDICT.NOT_APPLICABLE, VERDICT_BUCKET.NOT_APPLICABLE, reading);
|
|
4537
4551
|
}
|
|
4538
|
-
if (reading.errored
|
|
4552
|
+
if (reading.errored) {
|
|
4539
4553
|
return record2(SESSION_ENVIRONMENT_VERDICT.UNKNOWN, VERDICT_BUCKET.UNKNOWN, reading);
|
|
4540
4554
|
}
|
|
4541
4555
|
if (reading.sessionIdentity && reading.worktreeClaimed) {
|
|
@@ -4561,7 +4575,7 @@ var SESSION_STORE_VERDICT = {
|
|
|
4561
4575
|
};
|
|
4562
4576
|
var REMEDIATION3 = {
|
|
4563
4577
|
[SESSION_STORE_VERDICT.CONSISTENT]: "Session store is consistent; no action needed.",
|
|
4564
|
-
[SESSION_STORE_VERDICT.ORPHANED_CLAIMS]: "Release or reclaim doing sessions whose backing worktree
|
|
4578
|
+
[SESSION_STORE_VERDICT.ORPHANED_CLAIMS]: "Release or reclaim doing sessions whose backing worktree reads free or is absent (spx session release).",
|
|
4565
4579
|
[SESSION_STORE_VERDICT.UNKNOWN]: "Re-run diagnose; if it persists, inspect spx session list and spx worktree status."
|
|
4566
4580
|
};
|
|
4567
4581
|
function record3(verdict, bucket, reading) {
|
|
@@ -4686,13 +4700,11 @@ function spxReachabilityRunner(probe) {
|
|
|
4686
4700
|
// src/domains/diagnose/checks/worktree-pool.ts
|
|
4687
4701
|
var WORKTREE_POOL_VERDICT = {
|
|
4688
4702
|
COMPLIANT: "compliant",
|
|
4689
|
-
STALE_CLAIMS: "stale-claims",
|
|
4690
4703
|
NON_COMPLIANT: "non-compliant",
|
|
4691
4704
|
UNKNOWN: "unknown"
|
|
4692
4705
|
};
|
|
4693
4706
|
var REMEDIATION5 = {
|
|
4694
4707
|
[WORKTREE_POOL_VERDICT.COMPLIANT]: "Worktree layout is compliant; no action needed.",
|
|
4695
|
-
[WORKTREE_POOL_VERDICT.STALE_CLAIMS]: "Release stale worktree claims (spx worktree release) or remove dead worktrees.",
|
|
4696
4708
|
[WORKTREE_POOL_VERDICT.NON_COMPLIANT]: "Linked worktrees require a bare-repository pool; convert the layout or remove the linked worktrees.",
|
|
4697
4709
|
[WORKTREE_POOL_VERDICT.UNKNOWN]: "Re-run diagnose; if it persists, inspect git worktree list and spx worktree status."
|
|
4698
4710
|
};
|
|
@@ -4704,7 +4716,8 @@ function record5(verdict, bucket, reading) {
|
|
|
4704
4716
|
readings: {
|
|
4705
4717
|
bare: String(reading.bareRepository),
|
|
4706
4718
|
linked: String(reading.linkedWorktrees),
|
|
4707
|
-
|
|
4719
|
+
running: String(reading.running),
|
|
4720
|
+
free: String(reading.free)
|
|
4708
4721
|
},
|
|
4709
4722
|
remediation: REMEDIATION5[verdict]
|
|
4710
4723
|
};
|
|
@@ -4716,9 +4729,6 @@ function classifyWorktreePool(reading) {
|
|
|
4716
4729
|
if (!reading.bareRepository && reading.linkedWorktrees) {
|
|
4717
4730
|
return record5(WORKTREE_POOL_VERDICT.NON_COMPLIANT, VERDICT_BUCKET.BROKEN, reading);
|
|
4718
4731
|
}
|
|
4719
|
-
if (reading.staleClaim) {
|
|
4720
|
-
return record5(WORKTREE_POOL_VERDICT.STALE_CLAIMS, VERDICT_BUCKET.DEGRADED, reading);
|
|
4721
|
-
}
|
|
4722
4732
|
return record5(WORKTREE_POOL_VERDICT.COMPLIANT, VERDICT_BUCKET.HEALTHY, reading);
|
|
4723
4733
|
}
|
|
4724
4734
|
function worktreePoolRunner(probe) {
|
|
@@ -4736,7 +4746,7 @@ var DEL_CHAR_CODE = 127;
|
|
|
4736
4746
|
var HEX_RADIX = 16;
|
|
4737
4747
|
var HEX_PAD = 2;
|
|
4738
4748
|
function formatHexEscape(code) {
|
|
4739
|
-
return
|
|
4749
|
+
return String.raw`\x${code.toString(HEX_RADIX).padStart(HEX_PAD, "0")}`;
|
|
4740
4750
|
}
|
|
4741
4751
|
function nonStringSentinel(type) {
|
|
4742
4752
|
return `<non-string:${type}>`;
|
|
@@ -4824,7 +4834,7 @@ import { appendFile as nodeAppendFile } from "fs/promises";
|
|
|
4824
4834
|
// src/domains/worktree/controlling-process.ts
|
|
4825
4835
|
var CONTROLLING_PID_ENV = "SPX_WORKTREE_CONTROLLING_PID";
|
|
4826
4836
|
var AGENT_RUNTIME_NAMES = ["claude", "codex"];
|
|
4827
|
-
var AGENT_COMMAND_PATTERN = new RegExp(
|
|
4837
|
+
var AGENT_COMMAND_PATTERN = new RegExp(String.raw`\b(?:${AGENT_RUNTIME_NAMES.join("|")})\b`, "i");
|
|
4828
4838
|
var CONTROLLING_PROCESS_ERROR = {
|
|
4829
4839
|
UNRESOLVED: "worktree controlling process could not be resolved"
|
|
4830
4840
|
};
|
|
@@ -6300,9 +6310,7 @@ var NoSessionsAvailableError = class extends SessionError {
|
|
|
6300
6310
|
var SessionLegacyFrontmatterInputError = class extends SessionError {
|
|
6301
6311
|
constructor() {
|
|
6302
6312
|
super(
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
printf '%s\\n' '{"priority":"high","goal":"...","next_step":"..."}' '# Body' | spx session handoff`
|
|
6313
|
+
"Invalid handoff input: stdin opens with the YAML-frontmatter delimiter `---`. Use the JSON-prefix wire format: a single JSON object holding caller-supplied fields followed by the body bytes verbatim. Example:\n\n" + String.raw` printf '%s\n' '{"priority":"high","goal":"...","next_step":"..."}' '# Body' ` + "| spx session handoff"
|
|
6306
6314
|
);
|
|
6307
6315
|
this.name = "SessionLegacyFrontmatterInputError";
|
|
6308
6316
|
}
|
|
@@ -6725,7 +6733,7 @@ function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
|
|
|
6725
6733
|
}
|
|
6726
6734
|
function formatShowOutput(content, options) {
|
|
6727
6735
|
const metadata = parseSessionMetadata(content);
|
|
6728
|
-
const
|
|
6736
|
+
const headerLines2 = [
|
|
6729
6737
|
`${SESSION_SHOW_LABEL.STATUS}: ${options.status}`,
|
|
6730
6738
|
`${SESSION_SHOW_LABEL.PRIORITY}: ${metadata.priority}`,
|
|
6731
6739
|
`${SESSION_SHOW_LABEL.GIT_REF}: ${metadata.git_ref}`,
|
|
@@ -6734,7 +6742,7 @@ function formatShowOutput(content, options) {
|
|
|
6734
6742
|
`${SESSION_SHOW_LABEL.CREATED}: ${metadata.created_at ?? ""}`,
|
|
6735
6743
|
`${SESSION_SHOW_LABEL.AGENT_SESSION}: ${metadata.agent_session_id ?? ""}`
|
|
6736
6744
|
];
|
|
6737
|
-
const header =
|
|
6745
|
+
const header = headerLines2.join("\n");
|
|
6738
6746
|
const separator = "\n" + SESSION_SHOW_SEPARATOR_CHAR.repeat(SESSION_SHOW_SEPARATOR_WIDTH) + "\n\n";
|
|
6739
6747
|
return header + separator + content;
|
|
6740
6748
|
}
|
|
@@ -7494,13 +7502,13 @@ Workflow:
|
|
|
7494
7502
|
4. archive - Move session to archive
|
|
7495
7503
|
5. delete - Remove session permanently
|
|
7496
7504
|
`;
|
|
7497
|
-
var HANDOFF_FRONTMATTER_HELP = `
|
|
7505
|
+
var HANDOFF_FRONTMATTER_HELP = String.raw`
|
|
7498
7506
|
Usage:
|
|
7499
7507
|
Pipe a JSON header followed by the body bytes to stdin.
|
|
7500
7508
|
|
|
7501
7509
|
The header is a single JSON object holding caller-supplied structured fields.
|
|
7502
7510
|
A single LF or CRLF after the header is consumed as a separator. The body
|
|
7503
|
-
is the remaining bytes verbatim
|
|
7511
|
+
is the remaining bytes verbatim — no YAML, no escape rules, no ambiguity
|
|
7504
7512
|
from leading characters like '#' or '---'.
|
|
7505
7513
|
|
|
7506
7514
|
JSON Header Fields:
|
|
@@ -7523,11 +7531,11 @@ Output Tags (for automation):
|
|
|
7523
7531
|
<SESSION_FILE>/path/to/file</SESSION_FILE> Absolute path to created file
|
|
7524
7532
|
|
|
7525
7533
|
Canonical Invocation:
|
|
7526
|
-
printf '%s
|
|
7527
|
-
'{"priority":"high","goal":"Fix login","next_step":"Run validation","specs":[],"files":[]}'
|
|
7528
|
-
'# Fix login'
|
|
7529
|
-
''
|
|
7530
|
-
'Body text
|
|
7534
|
+
printf '%s\n' \
|
|
7535
|
+
'{"priority":"high","goal":"Fix login","next_step":"Run validation","specs":[],"files":[]}' \
|
|
7536
|
+
'# Fix login' \
|
|
7537
|
+
'' \
|
|
7538
|
+
'Body text — # symbols, --- delimiters, and code fences are literal.' \
|
|
7531
7539
|
| spx session handoff
|
|
7532
7540
|
`;
|
|
7533
7541
|
var PICKUP_SELECTION_HELP = `
|
|
@@ -9224,92 +9232,111 @@ async function runNodeCommand(options, deps) {
|
|
|
9224
9232
|
return { dispatch, runFile, recorded };
|
|
9225
9233
|
}
|
|
9226
9234
|
|
|
9227
|
-
// src/
|
|
9228
|
-
|
|
9229
|
-
function createNodeOutcomeResolver(deps) {
|
|
9230
|
-
let evidence;
|
|
9231
|
-
const currentInputsByCoveredPaths = /* @__PURE__ */ new Map();
|
|
9232
|
-
const sharedEvidence = () => evidence ??= loadResolverEvidence(deps);
|
|
9233
|
-
const refreshEvidence = () => {
|
|
9234
|
-
evidence = loadResolverEvidence(deps);
|
|
9235
|
-
};
|
|
9236
|
-
const currentInputsFor = (coveredPaths) => {
|
|
9237
|
-
const cacheKey = coveredPathCollectionKey(coveredPaths);
|
|
9238
|
-
const cached = currentInputsByCoveredPaths.get(cacheKey);
|
|
9239
|
-
if (cached !== void 0) {
|
|
9240
|
-
return cached;
|
|
9241
|
-
}
|
|
9242
|
-
const current = currentStalenessInputs(deps.productDir, coveredPaths, deps);
|
|
9243
|
-
currentInputsByCoveredPaths.set(cacheKey, current);
|
|
9244
|
-
return current;
|
|
9245
|
-
};
|
|
9246
|
-
return async (nodeId) => {
|
|
9247
|
-
const { discoveredTestPaths, terminalRuns } = await sharedEvidence();
|
|
9248
|
-
const nodePath = `${SPEC_TREE_CONFIG.ROOT_DIRECTORY}${PATH_SEPARATOR}${nodeId}`;
|
|
9249
|
-
const nodeTestPaths = filterNodeTestPaths(discoveredTestPaths, nodePath);
|
|
9250
|
-
const usable = await usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor);
|
|
9251
|
-
if (usable !== void 0) {
|
|
9252
|
-
return usable;
|
|
9253
|
-
}
|
|
9254
|
-
const { dispatch, recorded } = await runNodeCommand({ productDir: deps.productDir, nodePath }, deps);
|
|
9255
|
-
refreshEvidence();
|
|
9256
|
-
return outcomesCoverPaths(dispatch.outcomes, nodeTestPaths) && recorded.status === TEST_RUN_STATE_STATUS.PASSED;
|
|
9257
|
-
};
|
|
9258
|
-
}
|
|
9259
|
-
function coveredPathCollectionKey(coveredPaths) {
|
|
9260
|
-
return JSON.stringify([...coveredPaths].sort(compareAsciiStrings));
|
|
9261
|
-
}
|
|
9262
|
-
async function loadResolverEvidence(deps) {
|
|
9263
|
-
const discoveredTestPaths = await discoverTestFiles(deps.productDir);
|
|
9264
|
-
const runs = await readTestingRuns(deps.productDir, deps);
|
|
9265
|
-
return { discoveredTestPaths, terminalRuns: runs.ok ? runs.value.terminalRuns : [] };
|
|
9266
|
-
}
|
|
9267
|
-
function filterNodeTestPaths(discoveredTestPaths, nodePath) {
|
|
9268
|
-
const prefix = `${nodePath}${PATH_SEPARATOR}`;
|
|
9269
|
-
return discoveredTestPaths.filter((path7) => path7.startsWith(prefix));
|
|
9270
|
-
}
|
|
9271
|
-
async function usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor) {
|
|
9272
|
-
const latest = selectLatestTerminalTestRunForNode(terminalRuns, nodeTestPaths);
|
|
9273
|
-
if (latest === void 0) {
|
|
9274
|
-
return void 0;
|
|
9275
|
-
}
|
|
9276
|
-
const runCoveredPaths = latest.state.runnerOutcomes.flatMap((outcome) => outcome.testPaths);
|
|
9277
|
-
const presentTestPaths = new Set(discoveredTestPaths);
|
|
9278
|
-
if (!runCoveredPaths.every((path7) => presentTestPaths.has(path7))) {
|
|
9279
|
-
return void 0;
|
|
9280
|
-
}
|
|
9281
|
-
const current = await currentInputsFor(runCoveredPaths);
|
|
9282
|
-
const fresh = isStalenessMatch(extractStalenessInputs(latest.state), current);
|
|
9283
|
-
return fresh && latest.state.status === TEST_RUN_STATE_STATUS.PASSED ? true : void 0;
|
|
9284
|
-
}
|
|
9235
|
+
// src/lib/node-status/ci-projection.ts
|
|
9236
|
+
import { parse as parseYaml3 } from "yaml";
|
|
9285
9237
|
|
|
9286
9238
|
// src/lib/node-status/classify.ts
|
|
9287
|
-
var
|
|
9239
|
+
var NODE_STATUS_SCHEMA_VERSION = 1;
|
|
9288
9240
|
var NODE_STATUS_JSON_INDENT = 2;
|
|
9241
|
+
var NODE_STATUS_FIELD = {
|
|
9242
|
+
SCHEMA_VERSION: "schemaVersion",
|
|
9243
|
+
VERIFICATION: "verification",
|
|
9244
|
+
OVERALL: "overall"
|
|
9245
|
+
};
|
|
9246
|
+
var NODE_STATUS_VERIFICATION_MECHANISM = {
|
|
9247
|
+
TEST: "test",
|
|
9248
|
+
EVAL: "eval",
|
|
9249
|
+
AUDIT: "audit"
|
|
9250
|
+
};
|
|
9251
|
+
var NODE_STATUS_EVIDENCE_OUTCOME = {
|
|
9252
|
+
PASSED: "passed",
|
|
9253
|
+
FAILED: "failed",
|
|
9254
|
+
NOT_RUN: "not-run"
|
|
9255
|
+
};
|
|
9256
|
+
var NODE_STATUS_MECHANISM_OVERALL = {
|
|
9257
|
+
PASSED: NODE_STATUS_EVIDENCE_OUTCOME.PASSED,
|
|
9258
|
+
FAILED: NODE_STATUS_EVIDENCE_OUTCOME.FAILED,
|
|
9259
|
+
PARTIAL: "partial",
|
|
9260
|
+
NOT_RUN: NODE_STATUS_EVIDENCE_OUTCOME.NOT_RUN
|
|
9261
|
+
};
|
|
9289
9262
|
function classifyNodeStatus(facts) {
|
|
9290
|
-
if (!facts.
|
|
9263
|
+
if (!facts.hasVerificationReferences) return SPEC_TREE_NODE_STATE.DECLARED;
|
|
9291
9264
|
if (facts.isExcluded) return SPEC_TREE_NODE_STATE.SPECIFIED;
|
|
9292
|
-
if (facts.
|
|
9265
|
+
if (verificationPassed(facts.verification)) return SPEC_TREE_NODE_STATE.PASSING;
|
|
9293
9266
|
return SPEC_TREE_NODE_STATE.FAILING;
|
|
9294
9267
|
}
|
|
9295
|
-
function
|
|
9296
|
-
return
|
|
9268
|
+
function createNodeStatusFile(verification) {
|
|
9269
|
+
return {
|
|
9270
|
+
[NODE_STATUS_FIELD.SCHEMA_VERSION]: NODE_STATUS_SCHEMA_VERSION,
|
|
9271
|
+
[NODE_STATUS_FIELD.VERIFICATION]: verification
|
|
9272
|
+
};
|
|
9273
|
+
}
|
|
9274
|
+
function hasNodeStatusVerificationReferences(verification) {
|
|
9275
|
+
return Object.values(verification).some(
|
|
9276
|
+
(mechanism) => Object.keys(mechanism).some((reference) => reference !== NODE_STATUS_FIELD.OVERALL)
|
|
9277
|
+
);
|
|
9278
|
+
}
|
|
9279
|
+
function createNodeStatusMechanismRecord(outcomes) {
|
|
9280
|
+
return {
|
|
9281
|
+
[NODE_STATUS_FIELD.OVERALL]: rollupNodeStatusMechanism(outcomes),
|
|
9282
|
+
...outcomes
|
|
9283
|
+
};
|
|
9284
|
+
}
|
|
9285
|
+
function rollupNodeStatusMechanism(outcomes) {
|
|
9286
|
+
const values = Object.values(outcomes);
|
|
9287
|
+
if (values.length === 0) return NODE_STATUS_MECHANISM_OVERALL.NOT_RUN;
|
|
9288
|
+
if (values.includes(NODE_STATUS_EVIDENCE_OUTCOME.FAILED)) {
|
|
9289
|
+
return NODE_STATUS_MECHANISM_OVERALL.FAILED;
|
|
9290
|
+
}
|
|
9291
|
+
if (values.every((outcome) => outcome === NODE_STATUS_EVIDENCE_OUTCOME.PASSED)) {
|
|
9292
|
+
return NODE_STATUS_MECHANISM_OVERALL.PASSED;
|
|
9293
|
+
}
|
|
9294
|
+
if (values.every((outcome) => outcome === NODE_STATUS_EVIDENCE_OUTCOME.NOT_RUN)) {
|
|
9295
|
+
return NODE_STATUS_MECHANISM_OVERALL.NOT_RUN;
|
|
9296
|
+
}
|
|
9297
|
+
return NODE_STATUS_MECHANISM_OVERALL.PARTIAL;
|
|
9298
|
+
}
|
|
9299
|
+
function serializeNodeStatus(status) {
|
|
9300
|
+
return `${JSON.stringify(status, null, NODE_STATUS_JSON_INDENT)}
|
|
9297
9301
|
`;
|
|
9298
9302
|
}
|
|
9303
|
+
function verificationPassed(verification) {
|
|
9304
|
+
if (verification === void 0) return false;
|
|
9305
|
+
const mechanisms = Object.values(verification);
|
|
9306
|
+
return mechanisms.length > 0 && mechanisms.every((mechanism) => mechanism[NODE_STATUS_FIELD.OVERALL] === NODE_STATUS_MECHANISM_OVERALL.PASSED);
|
|
9307
|
+
}
|
|
9299
9308
|
|
|
9300
9309
|
// src/lib/node-status/provider.ts
|
|
9301
9310
|
import { join as join20 } from "path";
|
|
9302
9311
|
|
|
9312
|
+
// src/lib/node-status/exclude.ts
|
|
9313
|
+
function isNodeStatusEntryExcluded(ignoreReader, node) {
|
|
9314
|
+
const reference = node.ref?.path;
|
|
9315
|
+
if (reference === void 0) return false;
|
|
9316
|
+
return ignoreReader.isUnderIgnoreSource(reference);
|
|
9317
|
+
}
|
|
9318
|
+
|
|
9303
9319
|
// src/lib/node-status/read.ts
|
|
9304
9320
|
import { readFileSync as readFileSync2 } from "fs";
|
|
9305
9321
|
import { join as join19 } from "path";
|
|
9306
9322
|
var NODE_STATUS_FILENAME = "spx.status.json";
|
|
9307
|
-
var
|
|
9323
|
+
var NODE_STATUS_MECHANISMS = new Set(Object.values(NODE_STATUS_VERIFICATION_MECHANISM));
|
|
9324
|
+
var NODE_STATUS_EVIDENCE_OUTCOMES = new Set(Object.values(NODE_STATUS_EVIDENCE_OUTCOME));
|
|
9325
|
+
var NODE_STATUS_OVERALL_VALUES = new Set(Object.values(NODE_STATUS_MECHANISM_OVERALL));
|
|
9308
9326
|
function isNodeError2(error) {
|
|
9309
9327
|
return error instanceof Error && "code" in error;
|
|
9310
9328
|
}
|
|
9311
|
-
function
|
|
9312
|
-
return typeof value === "
|
|
9329
|
+
function isObject(value) {
|
|
9330
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9331
|
+
}
|
|
9332
|
+
function isNodeStatusMechanism(value) {
|
|
9333
|
+
return NODE_STATUS_MECHANISMS.has(value);
|
|
9334
|
+
}
|
|
9335
|
+
function isNodeStatusEvidenceOutcome(value) {
|
|
9336
|
+
return typeof value === "string" && NODE_STATUS_EVIDENCE_OUTCOMES.has(value);
|
|
9337
|
+
}
|
|
9338
|
+
function isNodeStatusMechanismOverall(value) {
|
|
9339
|
+
return typeof value === "string" && NODE_STATUS_OVERALL_VALUES.has(value);
|
|
9313
9340
|
}
|
|
9314
9341
|
function readNodeStatus(nodeDir) {
|
|
9315
9342
|
const filePath = join19(nodeDir, NODE_STATUS_FILENAME);
|
|
@@ -9322,14 +9349,67 @@ function readNodeStatus(nodeDir) {
|
|
|
9322
9349
|
}
|
|
9323
9350
|
throw error;
|
|
9324
9351
|
}
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9352
|
+
return parseNodeStatusFile(JSON.parse(content), filePath);
|
|
9353
|
+
}
|
|
9354
|
+
function parseNodeStatusFile(candidate, source) {
|
|
9355
|
+
if (!isObject(candidate)) {
|
|
9356
|
+
throw new Error(`Invalid ${NODE_STATUS_FILENAME} at ${source}: expected a JSON object`);
|
|
9357
|
+
}
|
|
9358
|
+
if (candidate[NODE_STATUS_FIELD.SCHEMA_VERSION] !== NODE_STATUS_SCHEMA_VERSION) {
|
|
9359
|
+
throw new Error(
|
|
9360
|
+
`Invalid ${NODE_STATUS_FILENAME} at ${source}: schemaVersion must be ${NODE_STATUS_SCHEMA_VERSION}`
|
|
9361
|
+
);
|
|
9362
|
+
}
|
|
9363
|
+
const verification = parseVerification(candidate[NODE_STATUS_FIELD.VERIFICATION], source);
|
|
9364
|
+
return {
|
|
9365
|
+
[NODE_STATUS_FIELD.SCHEMA_VERSION]: NODE_STATUS_SCHEMA_VERSION,
|
|
9366
|
+
[NODE_STATUS_FIELD.VERIFICATION]: verification
|
|
9367
|
+
};
|
|
9368
|
+
}
|
|
9369
|
+
function parseVerification(candidate, source) {
|
|
9370
|
+
if (!isObject(candidate)) {
|
|
9371
|
+
throw new Error(`Invalid ${NODE_STATUS_FILENAME} at ${source}: verification must be an object`);
|
|
9372
|
+
}
|
|
9373
|
+
const verification = {};
|
|
9374
|
+
for (const [mechanism, rawRecord] of Object.entries(candidate)) {
|
|
9375
|
+
if (!isNodeStatusMechanism(mechanism)) {
|
|
9376
|
+
throw new Error(`Invalid ${NODE_STATUS_FILENAME} at ${source}: unknown verification mechanism "${mechanism}"`);
|
|
9377
|
+
}
|
|
9378
|
+
verification[mechanism] = parseMechanismRecord(rawRecord, source, mechanism);
|
|
9379
|
+
}
|
|
9380
|
+
return verification;
|
|
9381
|
+
}
|
|
9382
|
+
function parseMechanismRecord(candidate, source, mechanism) {
|
|
9383
|
+
if (!isObject(candidate)) {
|
|
9384
|
+
throw new Error(`Invalid ${NODE_STATUS_FILENAME} at ${source}: verification.${mechanism} must be an object`);
|
|
9385
|
+
}
|
|
9386
|
+
const overall = candidate[NODE_STATUS_FIELD.OVERALL];
|
|
9387
|
+
if (!isNodeStatusMechanismOverall(overall)) {
|
|
9388
|
+
throw new Error(
|
|
9389
|
+
`Invalid ${NODE_STATUS_FILENAME} at ${source}: verification.${mechanism}.overall is invalid`
|
|
9390
|
+
);
|
|
9391
|
+
}
|
|
9392
|
+
const parsed = {
|
|
9393
|
+
[NODE_STATUS_FIELD.OVERALL]: overall
|
|
9394
|
+
};
|
|
9395
|
+
const outcomes = {};
|
|
9396
|
+
for (const [reference, outcome] of Object.entries(candidate)) {
|
|
9397
|
+
if (reference === NODE_STATUS_FIELD.OVERALL) continue;
|
|
9398
|
+
if (!isNodeStatusEvidenceOutcome(outcome)) {
|
|
9399
|
+
throw new Error(
|
|
9400
|
+
`Invalid ${NODE_STATUS_FILENAME} at ${source}: verification.${mechanism}.${reference} is invalid`
|
|
9401
|
+
);
|
|
9402
|
+
}
|
|
9403
|
+
outcomes[reference] = outcome;
|
|
9404
|
+
parsed[reference] = outcome;
|
|
9405
|
+
}
|
|
9406
|
+
const expectedOverall = rollupNodeStatusMechanism(outcomes);
|
|
9407
|
+
if (overall !== expectedOverall) {
|
|
9328
9408
|
throw new Error(
|
|
9329
|
-
`Invalid ${NODE_STATUS_FILENAME} at ${
|
|
9409
|
+
`Invalid ${NODE_STATUS_FILENAME} at ${source}: verification.${mechanism}.overall does not match evidence outcomes`
|
|
9330
9410
|
);
|
|
9331
9411
|
}
|
|
9332
|
-
return
|
|
9412
|
+
return parsed;
|
|
9333
9413
|
}
|
|
9334
9414
|
|
|
9335
9415
|
// src/lib/node-status/provider.ts
|
|
@@ -9337,15 +9417,25 @@ function nodeDirectory(productDir, node) {
|
|
|
9337
9417
|
return join20(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, node.id);
|
|
9338
9418
|
}
|
|
9339
9419
|
function createNodeStatusProvider(productDir) {
|
|
9420
|
+
const ignoreReader = createIgnoreSourceReader(productDir, {
|
|
9421
|
+
ignoreSourceFilename: IGNORE_SOURCE_FILENAME_DEFAULT,
|
|
9422
|
+
specTreeRootSegment: SPEC_TREE_CONFIG.ROOT_DIRECTORY
|
|
9423
|
+
});
|
|
9340
9424
|
return {
|
|
9341
9425
|
stateForNode(node) {
|
|
9342
|
-
|
|
9426
|
+
const status = readNodeStatus(nodeDirectory(productDir, node));
|
|
9427
|
+
if (status === void 0) return void 0;
|
|
9428
|
+
return classifyNodeStatus({
|
|
9429
|
+
hasVerificationReferences: hasNodeStatusVerificationReferences(status.verification),
|
|
9430
|
+
isExcluded: isNodeStatusEntryExcluded(ignoreReader, node),
|
|
9431
|
+
verification: status.verification
|
|
9432
|
+
});
|
|
9343
9433
|
}
|
|
9344
9434
|
};
|
|
9345
9435
|
}
|
|
9346
9436
|
|
|
9347
9437
|
// src/lib/node-status/update.ts
|
|
9348
|
-
import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
|
|
9438
|
+
import { mkdir as mkdir4, readdir as readdir7, rm, writeFile as writeFile2 } from "fs/promises";
|
|
9349
9439
|
import { dirname as dirname6, join as join21 } from "path";
|
|
9350
9440
|
var NODE_STATUS_TEXT_ENCODING = "utf8";
|
|
9351
9441
|
async function updateNodeStatus(options) {
|
|
@@ -9355,38 +9445,166 @@ async function updateNodeStatus(options) {
|
|
|
9355
9445
|
ignoreSourceFilename: IGNORE_SOURCE_FILENAME_DEFAULT,
|
|
9356
9446
|
specTreeRootSegment: SPEC_TREE_CONFIG.ROOT_DIRECTORY
|
|
9357
9447
|
});
|
|
9358
|
-
const
|
|
9448
|
+
const evidenceByNode = collectEvidenceByNode(snapshot);
|
|
9449
|
+
const liveStatusPaths = /* @__PURE__ */ new Set();
|
|
9359
9450
|
for (const node of snapshot.allNodes) {
|
|
9360
|
-
const
|
|
9361
|
-
|
|
9362
|
-
|
|
9451
|
+
const evidence = evidenceByNode.get(node.id) ?? [];
|
|
9452
|
+
const verification = await resolveVerification(node, {
|
|
9453
|
+
evidence,
|
|
9454
|
+
isExcluded: isNodeStatusEntryExcluded(ignoreReader, node),
|
|
9363
9455
|
resolveOutcome
|
|
9364
9456
|
});
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9457
|
+
const statusPath = nodeStatusPath(productDir, node.id);
|
|
9458
|
+
liveStatusPaths.add(statusPath);
|
|
9459
|
+
await writeNodeStatus(statusPath, verification);
|
|
9460
|
+
}
|
|
9461
|
+
await removeStaleNodeStatusFiles(productDir, liveStatusPaths);
|
|
9462
|
+
}
|
|
9463
|
+
async function resolveVerification(node, input) {
|
|
9464
|
+
if (input.evidence.length === 0) return {};
|
|
9465
|
+
if (input.isExcluded) return createTestVerification(input.evidence, NODE_STATUS_EVIDENCE_OUTCOME.NOT_RUN);
|
|
9466
|
+
return createTestVerificationFromOutcomes(
|
|
9467
|
+
input.evidence,
|
|
9468
|
+
await input.resolveOutcome(node.id, evidencePaths(input.evidence))
|
|
9469
|
+
);
|
|
9371
9470
|
}
|
|
9372
|
-
function
|
|
9373
|
-
const
|
|
9471
|
+
function collectEvidenceByNode(snapshot) {
|
|
9472
|
+
const evidenceByNode = /* @__PURE__ */ new Map();
|
|
9374
9473
|
for (const entry of snapshot.entries) {
|
|
9375
9474
|
if (entry.type === SPEC_TREE_ENTRY_TYPE.EVIDENCE) {
|
|
9376
|
-
|
|
9475
|
+
const entries = evidenceByNode.get(entry.parentId) ?? [];
|
|
9476
|
+
entries.push(entry);
|
|
9477
|
+
evidenceByNode.set(entry.parentId, entries);
|
|
9377
9478
|
}
|
|
9378
9479
|
}
|
|
9379
|
-
return
|
|
9480
|
+
return evidenceByNode;
|
|
9380
9481
|
}
|
|
9381
|
-
function
|
|
9382
|
-
const
|
|
9383
|
-
|
|
9384
|
-
return ignoreReader.isUnderIgnoreSource(reference);
|
|
9482
|
+
function createTestVerification(evidence, outcome) {
|
|
9483
|
+
const outcomes = Object.fromEntries(evidence.map((entry) => [evidencePath(entry), outcome]));
|
|
9484
|
+
return createTestVerificationFromOutcomes(evidence, outcomes);
|
|
9385
9485
|
}
|
|
9386
|
-
|
|
9387
|
-
const
|
|
9486
|
+
function createTestVerificationFromOutcomes(evidence, resolvedOutcomes) {
|
|
9487
|
+
const outcomes = {};
|
|
9488
|
+
for (const entry of evidence) {
|
|
9489
|
+
const path7 = evidencePath(entry);
|
|
9490
|
+
outcomes[path7] = resolvedOutcomes[path7] ?? NODE_STATUS_EVIDENCE_OUTCOME.NOT_RUN;
|
|
9491
|
+
}
|
|
9492
|
+
return { [NODE_STATUS_VERIFICATION_MECHANISM.TEST]: createNodeStatusMechanismRecord(outcomes) };
|
|
9493
|
+
}
|
|
9494
|
+
function evidencePaths(evidence) {
|
|
9495
|
+
return evidence.map(evidencePath);
|
|
9496
|
+
}
|
|
9497
|
+
function evidencePath(entry) {
|
|
9498
|
+
return entry.ref?.path ?? entry.id;
|
|
9499
|
+
}
|
|
9500
|
+
async function writeNodeStatus(filePath, verification) {
|
|
9388
9501
|
await mkdir4(dirname6(filePath), { recursive: true });
|
|
9389
|
-
await writeFile2(filePath, serializeNodeStatus(
|
|
9502
|
+
await writeFile2(filePath, serializeNodeStatus(createNodeStatusFile(verification)), NODE_STATUS_TEXT_ENCODING);
|
|
9503
|
+
}
|
|
9504
|
+
function nodeStatusPath(productDir, nodeId) {
|
|
9505
|
+
return join21(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY, nodeId, NODE_STATUS_FILENAME);
|
|
9506
|
+
}
|
|
9507
|
+
async function removeStaleNodeStatusFiles(productDir, liveStatusPaths) {
|
|
9508
|
+
const statusPaths = await collectNodeStatusFiles(join21(productDir, SPEC_TREE_CONFIG.ROOT_DIRECTORY));
|
|
9509
|
+
await Promise.all(statusPaths.filter((path7) => !liveStatusPaths.has(path7)).map((path7) => rm(path7, { force: true })));
|
|
9510
|
+
}
|
|
9511
|
+
async function collectNodeStatusFiles(directory) {
|
|
9512
|
+
const entries = await readDirectoryEntries(directory);
|
|
9513
|
+
const statusPaths = [];
|
|
9514
|
+
for (const entry of entries) {
|
|
9515
|
+
const entryPath = join21(directory, entry.name);
|
|
9516
|
+
if (entry.isDirectory()) {
|
|
9517
|
+
statusPaths.push(...await collectNodeStatusFiles(entryPath));
|
|
9518
|
+
} else if (entry.isFile() && entry.name === NODE_STATUS_FILENAME) {
|
|
9519
|
+
statusPaths.push(entryPath);
|
|
9520
|
+
}
|
|
9521
|
+
}
|
|
9522
|
+
return statusPaths;
|
|
9523
|
+
}
|
|
9524
|
+
async function readDirectoryEntries(directory) {
|
|
9525
|
+
try {
|
|
9526
|
+
return await readdir7(directory, { withFileTypes: true });
|
|
9527
|
+
} catch (error) {
|
|
9528
|
+
if (isNodeError3(error) && error.code === "ENOENT") return [];
|
|
9529
|
+
throw error;
|
|
9530
|
+
}
|
|
9531
|
+
}
|
|
9532
|
+
function isNodeError3(error) {
|
|
9533
|
+
return error instanceof Error && "code" in error;
|
|
9534
|
+
}
|
|
9535
|
+
|
|
9536
|
+
// src/commands/spec/node-outcome-resolver.ts
|
|
9537
|
+
var PATH_SEPARATOR = "/";
|
|
9538
|
+
var SUCCESS_EXIT_CODE2 = 0;
|
|
9539
|
+
function createNodeOutcomeResolver(deps) {
|
|
9540
|
+
let evidence;
|
|
9541
|
+
const currentInputsByCoveredPaths = /* @__PURE__ */ new Map();
|
|
9542
|
+
const sharedEvidence = () => evidence ??= loadResolverEvidence(deps);
|
|
9543
|
+
const refreshEvidence = () => {
|
|
9544
|
+
evidence = loadResolverEvidence(deps);
|
|
9545
|
+
};
|
|
9546
|
+
const currentInputsFor = (coveredPaths) => {
|
|
9547
|
+
const cacheKey = coveredPathCollectionKey(coveredPaths);
|
|
9548
|
+
const cached = currentInputsByCoveredPaths.get(cacheKey);
|
|
9549
|
+
if (cached !== void 0) {
|
|
9550
|
+
return cached;
|
|
9551
|
+
}
|
|
9552
|
+
const current = currentStalenessInputs(deps.productDir, coveredPaths, deps);
|
|
9553
|
+
currentInputsByCoveredPaths.set(cacheKey, current);
|
|
9554
|
+
return current;
|
|
9555
|
+
};
|
|
9556
|
+
return async (nodeId, evidencePaths2) => {
|
|
9557
|
+
const { discoveredTestPaths, terminalRuns } = await sharedEvidence();
|
|
9558
|
+
const nodePath = `${SPEC_TREE_CONFIG.ROOT_DIRECTORY}${PATH_SEPARATOR}${nodeId}`;
|
|
9559
|
+
const nodeTestPaths = filterNodeTestPaths(discoveredTestPaths, nodePath, evidencePaths2);
|
|
9560
|
+
const usable = await usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor);
|
|
9561
|
+
if (usable !== void 0) {
|
|
9562
|
+
return usable;
|
|
9563
|
+
}
|
|
9564
|
+
const { dispatch } = await runNodeCommand({ productDir: deps.productDir, nodePath }, deps);
|
|
9565
|
+
refreshEvidence();
|
|
9566
|
+
return outcomesForPaths(dispatch.outcomes, nodeTestPaths);
|
|
9567
|
+
};
|
|
9568
|
+
}
|
|
9569
|
+
function coveredPathCollectionKey(coveredPaths) {
|
|
9570
|
+
return JSON.stringify([...coveredPaths].sort(compareAsciiStrings));
|
|
9571
|
+
}
|
|
9572
|
+
async function loadResolverEvidence(deps) {
|
|
9573
|
+
const discoveredTestPaths = await discoverTestFiles(deps.productDir);
|
|
9574
|
+
const runs = await readTestingRuns(deps.productDir, deps);
|
|
9575
|
+
return { discoveredTestPaths, terminalRuns: runs.ok ? runs.value.terminalRuns : [] };
|
|
9576
|
+
}
|
|
9577
|
+
function filterNodeTestPaths(discoveredTestPaths, nodePath, evidencePaths2) {
|
|
9578
|
+
const prefix = `${nodePath}${PATH_SEPARATOR}`;
|
|
9579
|
+
const discovered = new Set(discoveredTestPaths.filter((path7) => path7.startsWith(prefix)));
|
|
9580
|
+
return evidencePaths2.filter((path7) => discovered.has(path7));
|
|
9581
|
+
}
|
|
9582
|
+
async function usableRecordedOutcome(discoveredTestPaths, terminalRuns, nodeTestPaths, currentInputsFor) {
|
|
9583
|
+
const latest = selectLatestTerminalTestRunForNode(terminalRuns, nodeTestPaths);
|
|
9584
|
+
if (latest === void 0) {
|
|
9585
|
+
return void 0;
|
|
9586
|
+
}
|
|
9587
|
+
const runCoveredPaths = latest.state.runnerOutcomes.flatMap((outcome) => outcome.testPaths);
|
|
9588
|
+
const presentTestPaths = new Set(discoveredTestPaths);
|
|
9589
|
+
if (!runCoveredPaths.every((path7) => presentTestPaths.has(path7))) {
|
|
9590
|
+
return void 0;
|
|
9591
|
+
}
|
|
9592
|
+
const current = await currentInputsFor(runCoveredPaths);
|
|
9593
|
+
const fresh = isStalenessMatch(extractStalenessInputs(latest.state), current);
|
|
9594
|
+
return fresh && latest.state.status === TEST_RUN_STATE_STATUS.PASSED ? outcomesForPaths(latest.state.runnerOutcomes, nodeTestPaths) : void 0;
|
|
9595
|
+
}
|
|
9596
|
+
function outcomesForPaths(outcomes, nodeTestPaths) {
|
|
9597
|
+
return Object.fromEntries(
|
|
9598
|
+
nodeTestPaths.map((path7) => [path7, outcomeForPath(outcomes, path7)])
|
|
9599
|
+
);
|
|
9600
|
+
}
|
|
9601
|
+
function outcomeForPath(outcomes, path7) {
|
|
9602
|
+
const covering = outcomes.filter((outcome) => outcome.testPaths.includes(path7));
|
|
9603
|
+
if (covering.length === 0) return NODE_STATUS_EVIDENCE_OUTCOME.NOT_RUN;
|
|
9604
|
+
if (covering.every((outcome) => outcome.exitCode === SUCCESS_EXIT_CODE2)) {
|
|
9605
|
+
return NODE_STATUS_EVIDENCE_OUTCOME.PASSED;
|
|
9606
|
+
}
|
|
9607
|
+
return NODE_STATUS_EVIDENCE_OUTCOME.FAILED;
|
|
9390
9608
|
}
|
|
9391
9609
|
|
|
9392
9610
|
// src/commands/spec/status.ts
|
|
@@ -12516,7 +12734,7 @@ async function circularCommand(options, deps = defaultCircularCommandDeps) {
|
|
|
12516
12734
|
|
|
12517
12735
|
// src/validation/steps/knip.ts
|
|
12518
12736
|
import { existsSync as existsSync4 } from "fs";
|
|
12519
|
-
import { mkdir as mkdir5, mkdtemp as mkdtemp2, rm, writeFile as writeFile3 } from "fs/promises";
|
|
12737
|
+
import { mkdir as mkdir5, mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
12520
12738
|
import { isAbsolute as isAbsolute5, join as join28 } from "path";
|
|
12521
12739
|
var defaultKnipProcessRunner = lifecycleProcessRunner;
|
|
12522
12740
|
var KNIP_COMMAND_TOKENS = {
|
|
@@ -12529,7 +12747,7 @@ var defaultKnipDeps = {
|
|
|
12529
12747
|
existsSync: existsSync4,
|
|
12530
12748
|
mkdir: mkdir5,
|
|
12531
12749
|
mkdtemp: mkdtemp2,
|
|
12532
|
-
rm,
|
|
12750
|
+
rm: rm2,
|
|
12533
12751
|
writeFile: writeFile3
|
|
12534
12752
|
};
|
|
12535
12753
|
async function validateKnip2(context, runner = defaultKnipProcessRunner, deps = defaultKnipDeps) {
|
|
@@ -13141,18 +13359,18 @@ var ignoreSourceLayer = makeLayer(
|
|
|
13141
13359
|
);
|
|
13142
13360
|
|
|
13143
13361
|
// src/lib/file-inclusion/pipeline.ts
|
|
13144
|
-
import { readdir as
|
|
13362
|
+
import { readdir as readdir8 } from "fs/promises";
|
|
13145
13363
|
import { join as join31, relative as relative6, sep as sep2 } from "path";
|
|
13146
13364
|
var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
|
|
13147
|
-
function
|
|
13365
|
+
function isNodeError4(err) {
|
|
13148
13366
|
return err instanceof Error && "code" in err;
|
|
13149
13367
|
}
|
|
13150
13368
|
async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
|
|
13151
13369
|
let dirEntries;
|
|
13152
13370
|
try {
|
|
13153
|
-
dirEntries = await
|
|
13371
|
+
dirEntries = await readdir8(absoluteDir, { withFileTypes: true });
|
|
13154
13372
|
} catch (err) {
|
|
13155
|
-
if (
|
|
13373
|
+
if (isNodeError4(err) && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
|
|
13156
13374
|
return;
|
|
13157
13375
|
}
|
|
13158
13376
|
throw err;
|
|
@@ -14588,6 +14806,10 @@ var WORKTREE_STATUS_FORMAT = {
|
|
|
14588
14806
|
JSON: "json",
|
|
14589
14807
|
TEXT: "text"
|
|
14590
14808
|
};
|
|
14809
|
+
var WORKTREE_STATUS_RENDER = {
|
|
14810
|
+
FREE: "-",
|
|
14811
|
+
RUNNING_PID_PREFIX: "PID "
|
|
14812
|
+
};
|
|
14591
14813
|
async function statusCommand2(options) {
|
|
14592
14814
|
const multiTargetRequest = options.worktrees !== void 0 && options.worktrees.length > 1;
|
|
14593
14815
|
const targets = await resolveStatusTargets(options);
|
|
@@ -14595,9 +14817,13 @@ async function statusCommand2(options) {
|
|
|
14595
14817
|
const records = [];
|
|
14596
14818
|
for (const target of targets.value) {
|
|
14597
14819
|
const worktreesDir = await resolveWorktreesDir({ ...options, cwd: target.worktreeRoot });
|
|
14598
|
-
const
|
|
14599
|
-
if (!
|
|
14600
|
-
|
|
14820
|
+
const claimResult = await readClaim(worktreesDir, target.name, { fs: options.fs });
|
|
14821
|
+
if (!claimResult.ok) return claimResult;
|
|
14822
|
+
const claim = claimResult.value;
|
|
14823
|
+
const status = classifyOccupancy(claim, options.processTable);
|
|
14824
|
+
records.push(
|
|
14825
|
+
status === OCCUPANCY_STATUS.RUNNING && claim !== void 0 ? { worktree: target.name, status, pid: claim.pid, session: claim.sessionId, host: claim.host } : { worktree: target.name, status }
|
|
14826
|
+
);
|
|
14601
14827
|
}
|
|
14602
14828
|
return { ok: true, value: renderStatus(records, options.format, multiTargetRequest) };
|
|
14603
14829
|
}
|
|
@@ -14609,10 +14835,13 @@ async function resolveStatusTargets(options) {
|
|
|
14609
14835
|
return { ok: true, value: [target.value] };
|
|
14610
14836
|
}
|
|
14611
14837
|
const targets = [];
|
|
14838
|
+
const seenRoots = /* @__PURE__ */ new Set();
|
|
14612
14839
|
let firstError;
|
|
14613
14840
|
for (const worktree of requested) {
|
|
14614
14841
|
const target = await resolveTargetWorktree({ ...options, worktree });
|
|
14615
14842
|
if (target.ok) {
|
|
14843
|
+
if (seenRoots.has(target.value.worktreeRoot)) continue;
|
|
14844
|
+
seenRoots.add(target.value.worktreeRoot);
|
|
14616
14845
|
targets.push(target.value);
|
|
14617
14846
|
} else {
|
|
14618
14847
|
firstError ??= target.error;
|
|
@@ -14630,7 +14859,10 @@ function renderStatus(records, format2, multiTargetRequest) {
|
|
|
14630
14859
|
return records.map(renderTextStatus).join("\n");
|
|
14631
14860
|
}
|
|
14632
14861
|
function renderTextStatus(record6) {
|
|
14633
|
-
|
|
14862
|
+
if (record6.status === OCCUPANCY_STATUS.RUNNING) {
|
|
14863
|
+
return `${record6.worktree} ${WORKTREE_STATUS_RENDER.RUNNING_PID_PREFIX}${record6.pid} (${record6.session} @ ${record6.host})`;
|
|
14864
|
+
}
|
|
14865
|
+
return `${record6.worktree} ${WORKTREE_STATUS_RENDER.FREE}`;
|
|
14634
14866
|
}
|
|
14635
14867
|
|
|
14636
14868
|
// src/lib/worktree-path-info.ts
|
|
@@ -14678,7 +14910,7 @@ function registerWorktreeCommands(worktreeCmd) {
|
|
|
14678
14910
|
});
|
|
14679
14911
|
if (!result.ok) handleError3(result.error);
|
|
14680
14912
|
});
|
|
14681
|
-
worktreeCmd.command(`${WORKTREE_CLI.STATUS} ${WORKTREE_CLI.WORKTREE_ARGUMENT}`).description("Report a worktree's occupancy (
|
|
14913
|
+
worktreeCmd.command(`${WORKTREE_CLI.STATUS} ${WORKTREE_CLI.WORKTREE_ARGUMENT}`).description("Report a worktree's occupancy (running | free)").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) => {
|
|
14682
14914
|
const result = await statusCommand2({
|
|
14683
14915
|
cwd: process.cwd(),
|
|
14684
14916
|
fs: defaultOccupancyFileSystem,
|