@outcomeeng/spx 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +447 -48
- package/dist/cli.js.map +1 -1
- package/package.json +3 -3
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"
|
|
@@ -1109,6 +1110,16 @@ function worktreeScopeDir(productDir) {
|
|
|
1109
1110
|
function sessionsScopeDir(productDir) {
|
|
1110
1111
|
return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.SESSIONS_SCOPE);
|
|
1111
1112
|
}
|
|
1113
|
+
async function resolveWorktreesScopeDir(options = {}) {
|
|
1114
|
+
const gitResult = await detectGitCommonDirProductRoot(options.cwd, options.deps);
|
|
1115
|
+
return {
|
|
1116
|
+
worktreesDir: worktreesScopeDir(gitResult.productDir),
|
|
1117
|
+
warning: gitResult.warning
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
function worktreesScopeDir(productDir) {
|
|
1121
|
+
return join2(productDir, STATE_STORE_PATH.SPX_DIR, STATE_STORE_PATH.WORKTREES_SCOPE);
|
|
1122
|
+
}
|
|
1112
1123
|
function composeScopeDir(baseScopeDir, ...tokens) {
|
|
1113
1124
|
const segments = [];
|
|
1114
1125
|
for (const token of tokens) {
|
|
@@ -2516,8 +2527,8 @@ var HIDDEN_PREFIX_LAYER = "hidden-prefix";
|
|
|
2516
2527
|
var LAYER2 = HIDDEN_PREFIX_LAYER;
|
|
2517
2528
|
function hiddenPrefixPredicate(path6, config) {
|
|
2518
2529
|
const segments = path6.split("/");
|
|
2519
|
-
const
|
|
2520
|
-
const matched =
|
|
2530
|
+
const basename5 = segments[segments.length - 1] ?? "";
|
|
2531
|
+
const matched = basename5.startsWith(config.hiddenPrefix);
|
|
2521
2532
|
return { matched, layer: LAYER2 };
|
|
2522
2533
|
}
|
|
2523
2534
|
|
|
@@ -4865,17 +4876,17 @@ async function readStdin() {
|
|
|
4865
4876
|
if (process.stdin.isTTY) {
|
|
4866
4877
|
return void 0;
|
|
4867
4878
|
}
|
|
4868
|
-
return new Promise((
|
|
4879
|
+
return new Promise((resolve6) => {
|
|
4869
4880
|
let data = "";
|
|
4870
4881
|
process.stdin.setEncoding("utf-8");
|
|
4871
4882
|
process.stdin.on("data", (chunk) => {
|
|
4872
4883
|
data += chunk;
|
|
4873
4884
|
});
|
|
4874
4885
|
process.stdin.on("end", () => {
|
|
4875
|
-
|
|
4886
|
+
resolve6(data.length === 0 ? void 0 : data);
|
|
4876
4887
|
});
|
|
4877
4888
|
process.stdin.on("error", () => {
|
|
4878
|
-
|
|
4889
|
+
resolve6(void 0);
|
|
4879
4890
|
});
|
|
4880
4891
|
});
|
|
4881
4892
|
}
|
|
@@ -7813,39 +7824,39 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7813
7824
|
const token = _scanner.scan();
|
|
7814
7825
|
switch (_scanner.getTokenError()) {
|
|
7815
7826
|
case 4:
|
|
7816
|
-
|
|
7827
|
+
handleError3(
|
|
7817
7828
|
14
|
|
7818
7829
|
/* ParseErrorCode.InvalidUnicode */
|
|
7819
7830
|
);
|
|
7820
7831
|
break;
|
|
7821
7832
|
case 5:
|
|
7822
|
-
|
|
7833
|
+
handleError3(
|
|
7823
7834
|
15
|
|
7824
7835
|
/* ParseErrorCode.InvalidEscapeCharacter */
|
|
7825
7836
|
);
|
|
7826
7837
|
break;
|
|
7827
7838
|
case 3:
|
|
7828
|
-
|
|
7839
|
+
handleError3(
|
|
7829
7840
|
13
|
|
7830
7841
|
/* ParseErrorCode.UnexpectedEndOfNumber */
|
|
7831
7842
|
);
|
|
7832
7843
|
break;
|
|
7833
7844
|
case 1:
|
|
7834
7845
|
if (!disallowComments) {
|
|
7835
|
-
|
|
7846
|
+
handleError3(
|
|
7836
7847
|
11
|
|
7837
7848
|
/* ParseErrorCode.UnexpectedEndOfComment */
|
|
7838
7849
|
);
|
|
7839
7850
|
}
|
|
7840
7851
|
break;
|
|
7841
7852
|
case 2:
|
|
7842
|
-
|
|
7853
|
+
handleError3(
|
|
7843
7854
|
12
|
|
7844
7855
|
/* ParseErrorCode.UnexpectedEndOfString */
|
|
7845
7856
|
);
|
|
7846
7857
|
break;
|
|
7847
7858
|
case 6:
|
|
7848
|
-
|
|
7859
|
+
handleError3(
|
|
7849
7860
|
16
|
|
7850
7861
|
/* ParseErrorCode.InvalidCharacter */
|
|
7851
7862
|
);
|
|
@@ -7855,7 +7866,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7855
7866
|
case 12:
|
|
7856
7867
|
case 13:
|
|
7857
7868
|
if (disallowComments) {
|
|
7858
|
-
|
|
7869
|
+
handleError3(
|
|
7859
7870
|
10
|
|
7860
7871
|
/* ParseErrorCode.InvalidCommentToken */
|
|
7861
7872
|
);
|
|
@@ -7864,7 +7875,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7864
7875
|
}
|
|
7865
7876
|
break;
|
|
7866
7877
|
case 16:
|
|
7867
|
-
|
|
7878
|
+
handleError3(
|
|
7868
7879
|
1
|
|
7869
7880
|
/* ParseErrorCode.InvalidSymbol */
|
|
7870
7881
|
);
|
|
@@ -7877,7 +7888,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7877
7888
|
}
|
|
7878
7889
|
}
|
|
7879
7890
|
}
|
|
7880
|
-
function
|
|
7891
|
+
function handleError3(error, skipUntilAfter = [], skipUntil = []) {
|
|
7881
7892
|
onError(error);
|
|
7882
7893
|
if (skipUntilAfter.length + skipUntil.length > 0) {
|
|
7883
7894
|
let token = _scanner.getToken();
|
|
@@ -7909,7 +7920,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7909
7920
|
const tokenValue = _scanner.getTokenValue();
|
|
7910
7921
|
let value = Number(tokenValue);
|
|
7911
7922
|
if (isNaN(value)) {
|
|
7912
|
-
|
|
7923
|
+
handleError3(
|
|
7913
7924
|
2
|
|
7914
7925
|
/* ParseErrorCode.InvalidNumberFormat */
|
|
7915
7926
|
);
|
|
@@ -7934,7 +7945,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7934
7945
|
}
|
|
7935
7946
|
function parseProperty() {
|
|
7936
7947
|
if (_scanner.getToken() !== 10) {
|
|
7937
|
-
|
|
7948
|
+
handleError3(3, [], [
|
|
7938
7949
|
2,
|
|
7939
7950
|
5
|
|
7940
7951
|
/* SyntaxKind.CommaToken */
|
|
@@ -7946,14 +7957,14 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7946
7957
|
onSeparator(":");
|
|
7947
7958
|
scanNext();
|
|
7948
7959
|
if (!parseValue()) {
|
|
7949
|
-
|
|
7960
|
+
handleError3(4, [], [
|
|
7950
7961
|
2,
|
|
7951
7962
|
5
|
|
7952
7963
|
/* SyntaxKind.CommaToken */
|
|
7953
7964
|
]);
|
|
7954
7965
|
}
|
|
7955
7966
|
} else {
|
|
7956
|
-
|
|
7967
|
+
handleError3(5, [], [
|
|
7957
7968
|
2,
|
|
7958
7969
|
5
|
|
7959
7970
|
/* SyntaxKind.CommaToken */
|
|
@@ -7969,7 +7980,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7969
7980
|
while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
|
|
7970
7981
|
if (_scanner.getToken() === 5) {
|
|
7971
7982
|
if (!needsComma) {
|
|
7972
|
-
|
|
7983
|
+
handleError3(4, [], []);
|
|
7973
7984
|
}
|
|
7974
7985
|
onSeparator(",");
|
|
7975
7986
|
scanNext();
|
|
@@ -7977,10 +7988,10 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7977
7988
|
break;
|
|
7978
7989
|
}
|
|
7979
7990
|
} else if (needsComma) {
|
|
7980
|
-
|
|
7991
|
+
handleError3(6, [], []);
|
|
7981
7992
|
}
|
|
7982
7993
|
if (!parseProperty()) {
|
|
7983
|
-
|
|
7994
|
+
handleError3(4, [], [
|
|
7984
7995
|
2,
|
|
7985
7996
|
5
|
|
7986
7997
|
/* SyntaxKind.CommaToken */
|
|
@@ -7990,7 +8001,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
7990
8001
|
}
|
|
7991
8002
|
onObjectEnd();
|
|
7992
8003
|
if (_scanner.getToken() !== 2) {
|
|
7993
|
-
|
|
8004
|
+
handleError3(7, [
|
|
7994
8005
|
2
|
|
7995
8006
|
/* SyntaxKind.CloseBraceToken */
|
|
7996
8007
|
], []);
|
|
@@ -8007,7 +8018,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
8007
8018
|
while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
|
|
8008
8019
|
if (_scanner.getToken() === 5) {
|
|
8009
8020
|
if (!needsComma) {
|
|
8010
|
-
|
|
8021
|
+
handleError3(4, [], []);
|
|
8011
8022
|
}
|
|
8012
8023
|
onSeparator(",");
|
|
8013
8024
|
scanNext();
|
|
@@ -8015,7 +8026,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
8015
8026
|
break;
|
|
8016
8027
|
}
|
|
8017
8028
|
} else if (needsComma) {
|
|
8018
|
-
|
|
8029
|
+
handleError3(6, [], []);
|
|
8019
8030
|
}
|
|
8020
8031
|
if (isFirstElement) {
|
|
8021
8032
|
_jsonPath.push(0);
|
|
@@ -8024,7 +8035,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
8024
8035
|
_jsonPath[_jsonPath.length - 1]++;
|
|
8025
8036
|
}
|
|
8026
8037
|
if (!parseValue()) {
|
|
8027
|
-
|
|
8038
|
+
handleError3(4, [], [
|
|
8028
8039
|
4,
|
|
8029
8040
|
5
|
|
8030
8041
|
/* SyntaxKind.CommaToken */
|
|
@@ -8037,7 +8048,7 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
8037
8048
|
_jsonPath.pop();
|
|
8038
8049
|
}
|
|
8039
8050
|
if (_scanner.getToken() !== 4) {
|
|
8040
|
-
|
|
8051
|
+
handleError3(8, [
|
|
8041
8052
|
4
|
|
8042
8053
|
/* SyntaxKind.CloseBracketToken */
|
|
8043
8054
|
], []);
|
|
@@ -8063,15 +8074,15 @@ function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
|
8063
8074
|
if (options.allowEmptyContent) {
|
|
8064
8075
|
return true;
|
|
8065
8076
|
}
|
|
8066
|
-
|
|
8077
|
+
handleError3(4, [], []);
|
|
8067
8078
|
return false;
|
|
8068
8079
|
}
|
|
8069
8080
|
if (!parseValue()) {
|
|
8070
|
-
|
|
8081
|
+
handleError3(4, [], []);
|
|
8071
8082
|
return false;
|
|
8072
8083
|
}
|
|
8073
8084
|
if (_scanner.getToken() !== 17) {
|
|
8074
|
-
|
|
8085
|
+
handleError3(9, [], []);
|
|
8075
8086
|
}
|
|
8076
8087
|
return true;
|
|
8077
8088
|
}
|
|
@@ -8526,13 +8537,13 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
|
8526
8537
|
knipProcess.stderr?.on("data", (data) => {
|
|
8527
8538
|
knipError += data.toString();
|
|
8528
8539
|
});
|
|
8529
|
-
return new Promise((
|
|
8540
|
+
return new Promise((resolve6) => {
|
|
8530
8541
|
const resolveAfterCleanup = (result) => {
|
|
8531
8542
|
if (resultResolved) {
|
|
8532
8543
|
return;
|
|
8533
8544
|
}
|
|
8534
8545
|
resultResolved = true;
|
|
8535
|
-
void cleanupOnce().finally(() =>
|
|
8546
|
+
void cleanupOnce().finally(() => resolve6(result));
|
|
8536
8547
|
};
|
|
8537
8548
|
knipProcess.on("close", (code) => {
|
|
8538
8549
|
if (code === 0) {
|
|
@@ -8970,7 +8981,7 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
|
|
|
8970
8981
|
scope,
|
|
8971
8982
|
scopeConfig: context.scopeConfig
|
|
8972
8983
|
});
|
|
8973
|
-
return new Promise((
|
|
8984
|
+
return new Promise((resolve6) => {
|
|
8974
8985
|
const localBin = join26(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
|
|
8975
8986
|
const binary = existsSync5(localBin) ? localBin : "npx";
|
|
8976
8987
|
const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
|
|
@@ -8980,13 +8991,13 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
|
|
|
8980
8991
|
forwardValidationSubprocessOutput(eslintProcess, outputStreams);
|
|
8981
8992
|
eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
8982
8993
|
if (code === 0) {
|
|
8983
|
-
|
|
8994
|
+
resolve6({ success: true });
|
|
8984
8995
|
} else {
|
|
8985
|
-
|
|
8996
|
+
resolve6({ success: false, error: `ESLint exited with code ${code}` });
|
|
8986
8997
|
}
|
|
8987
8998
|
});
|
|
8988
8999
|
eslintProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
8989
|
-
|
|
9000
|
+
resolve6({ success: false, error: error.message });
|
|
8990
9001
|
});
|
|
8991
9002
|
});
|
|
8992
9003
|
}
|
|
@@ -9864,7 +9875,7 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9864
9875
|
if (files && files.length > 0) {
|
|
9865
9876
|
const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, projectRoot, deps);
|
|
9866
9877
|
try {
|
|
9867
|
-
return await new Promise((
|
|
9878
|
+
return await new Promise((resolve6) => {
|
|
9868
9879
|
const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
|
|
9869
9880
|
const tscBinary = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9870
9881
|
const tscArgs2 = tscBinary === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
|
|
@@ -9875,14 +9886,14 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9875
9886
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
9876
9887
|
cleanup();
|
|
9877
9888
|
if (code === 0) {
|
|
9878
|
-
|
|
9889
|
+
resolve6({ success: true, skipped: false });
|
|
9879
9890
|
} else {
|
|
9880
|
-
|
|
9891
|
+
resolve6({ success: false, error: `TypeScript exited with code ${code}` });
|
|
9881
9892
|
}
|
|
9882
9893
|
});
|
|
9883
9894
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
9884
9895
|
cleanup();
|
|
9885
|
-
|
|
9896
|
+
resolve6({ success: false, error: error.message });
|
|
9886
9897
|
});
|
|
9887
9898
|
});
|
|
9888
9899
|
} catch (error) {
|
|
@@ -9901,7 +9912,7 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9901
9912
|
const tscBin = join28(projectRoot, "node_modules", ".bin", "tsc");
|
|
9902
9913
|
tool = deps.existsSync(tscBin) ? tscBin : "npx";
|
|
9903
9914
|
tscArgs = tool === "npx" ? ["tsc", "--project", configPath] : ["--project", configPath];
|
|
9904
|
-
return new Promise((
|
|
9915
|
+
return new Promise((resolve6) => {
|
|
9905
9916
|
const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
|
|
9906
9917
|
cwd: projectRoot
|
|
9907
9918
|
});
|
|
@@ -9909,14 +9920,14 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9909
9920
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
9910
9921
|
cleanup();
|
|
9911
9922
|
if (code === 0) {
|
|
9912
|
-
|
|
9923
|
+
resolve6({ success: true, skipped: false });
|
|
9913
9924
|
} else {
|
|
9914
|
-
|
|
9925
|
+
resolve6({ success: false, error: `TypeScript exited with code ${code}` });
|
|
9915
9926
|
}
|
|
9916
9927
|
});
|
|
9917
9928
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
9918
9929
|
cleanup();
|
|
9919
|
-
|
|
9930
|
+
resolve6({ success: false, error: error.message });
|
|
9920
9931
|
});
|
|
9921
9932
|
});
|
|
9922
9933
|
} else {
|
|
@@ -9925,20 +9936,20 @@ async function validateTypeScript(context, options = {}) {
|
|
|
9925
9936
|
const rawArgs = buildTypeScriptArgs({ scope, configFile });
|
|
9926
9937
|
tscArgs = tool === "npx" ? rawArgs : rawArgs.slice(1);
|
|
9927
9938
|
}
|
|
9928
|
-
return new Promise((
|
|
9939
|
+
return new Promise((resolve6) => {
|
|
9929
9940
|
const tscProcess = spawnManagedSubprocess(runner, tool, tscArgs, {
|
|
9930
9941
|
cwd: projectRoot
|
|
9931
9942
|
});
|
|
9932
9943
|
forwardValidationSubprocessOutput(tscProcess, outputStreams);
|
|
9933
9944
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.CLOSE, (code) => {
|
|
9934
9945
|
if (code === 0) {
|
|
9935
|
-
|
|
9946
|
+
resolve6({ success: true, skipped: false });
|
|
9936
9947
|
} else {
|
|
9937
|
-
|
|
9948
|
+
resolve6({ success: false, error: `TypeScript exited with code ${code}` });
|
|
9938
9949
|
}
|
|
9939
9950
|
});
|
|
9940
9951
|
tscProcess.on(VALIDATION_SUBPROCESS_EVENTS.ERROR, (error) => {
|
|
9941
|
-
|
|
9952
|
+
resolve6({ success: false, error: error.message });
|
|
9942
9953
|
});
|
|
9943
9954
|
});
|
|
9944
9955
|
}
|
|
@@ -10523,6 +10534,393 @@ var validationDomain = {
|
|
|
10523
10534
|
}
|
|
10524
10535
|
};
|
|
10525
10536
|
|
|
10537
|
+
// src/domains/worktree/controlling-process.ts
|
|
10538
|
+
var CONTROLLING_PID_ENV = "SPX_WORKTREE_CONTROLLING_PID";
|
|
10539
|
+
var AGENT_RUNTIME_NAMES = ["claude", "codex"];
|
|
10540
|
+
var AGENT_COMMAND_PATTERN = new RegExp(`\\b(?:${AGENT_RUNTIME_NAMES.join("|")})\\b`, "i");
|
|
10541
|
+
var CONTROLLING_PROCESS_ERROR = {
|
|
10542
|
+
UNRESOLVED: "worktree controlling process could not be resolved"
|
|
10543
|
+
};
|
|
10544
|
+
var MAX_ANCESTRY_DEPTH = 64;
|
|
10545
|
+
var PID_RADIX = 10;
|
|
10546
|
+
function resolveControllingProcess(selfPid, table, env) {
|
|
10547
|
+
const host = table.currentHost();
|
|
10548
|
+
for (const pid of controllingPidCandidates(selfPid, table, env)) {
|
|
10549
|
+
const startedAt = table.startTimeOf(pid);
|
|
10550
|
+
if (startedAt !== void 0) return { ok: true, value: { pid, startedAt, host } };
|
|
10551
|
+
}
|
|
10552
|
+
return { ok: false, error: CONTROLLING_PROCESS_ERROR.UNRESOLVED };
|
|
10553
|
+
}
|
|
10554
|
+
function* controllingPidCandidates(selfPid, table, env) {
|
|
10555
|
+
const override = parsePid(env[CONTROLLING_PID_ENV]);
|
|
10556
|
+
if (override !== void 0) yield override;
|
|
10557
|
+
const agent = findAgentAncestor(selfPid, table);
|
|
10558
|
+
if (agent !== void 0) yield agent;
|
|
10559
|
+
const parent = table.parentOf(selfPid);
|
|
10560
|
+
if (parent !== void 0) yield parent;
|
|
10561
|
+
}
|
|
10562
|
+
function findAgentAncestor(selfPid, table) {
|
|
10563
|
+
let pid = table.parentOf(selfPid);
|
|
10564
|
+
for (let depth = 0; pid !== void 0 && depth < MAX_ANCESTRY_DEPTH; depth += 1) {
|
|
10565
|
+
const command = table.commandOf(pid);
|
|
10566
|
+
if (command !== void 0 && AGENT_COMMAND_PATTERN.test(command)) return pid;
|
|
10567
|
+
pid = table.parentOf(pid);
|
|
10568
|
+
}
|
|
10569
|
+
return void 0;
|
|
10570
|
+
}
|
|
10571
|
+
function parsePid(value) {
|
|
10572
|
+
if (value === void 0 || value.length === 0) return void 0;
|
|
10573
|
+
const parsed = Number.parseInt(value, PID_RADIX);
|
|
10574
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
10575
|
+
}
|
|
10576
|
+
|
|
10577
|
+
// src/domains/worktree/occupancy-store.ts
|
|
10578
|
+
import {
|
|
10579
|
+
mkdir as nodeMkdir2,
|
|
10580
|
+
readFile as nodeReadFile2,
|
|
10581
|
+
rename as nodeRename,
|
|
10582
|
+
rm as nodeRm,
|
|
10583
|
+
writeFile as nodeWriteFile2
|
|
10584
|
+
} from "fs/promises";
|
|
10585
|
+
import { join as join30 } from "path";
|
|
10586
|
+
var OCCUPANCY_STATUS = {
|
|
10587
|
+
UNCLAIMED: "unclaimed",
|
|
10588
|
+
OCCUPIED: "occupied",
|
|
10589
|
+
STALE: "stale"
|
|
10590
|
+
};
|
|
10591
|
+
var OCCUPANCY_CLAIM = {
|
|
10592
|
+
FILE_EXTENSION: ".claim",
|
|
10593
|
+
TEMP_EXTENSION: ".tmp"
|
|
10594
|
+
};
|
|
10595
|
+
var OCCUPANCY_ERROR = {
|
|
10596
|
+
INVALID_NAME: "worktree occupancy claim name must be a safe path segment",
|
|
10597
|
+
CLAIM_WRITE_FAILED: "worktree occupancy claim write failed",
|
|
10598
|
+
CLAIM_READ_FAILED: "worktree occupancy claim read failed",
|
|
10599
|
+
CLAIM_REMOVE_FAILED: "worktree occupancy claim remove failed",
|
|
10600
|
+
CLAIM_MALFORMED: "worktree occupancy claim record is malformed"
|
|
10601
|
+
};
|
|
10602
|
+
var ERROR_DETAIL_SEPARATOR2 = ": ";
|
|
10603
|
+
var defaultOccupancyFileSystem = {
|
|
10604
|
+
mkdir: async (path6, options) => {
|
|
10605
|
+
await nodeMkdir2(path6, options);
|
|
10606
|
+
},
|
|
10607
|
+
writeFile: async (path6, data) => {
|
|
10608
|
+
await nodeWriteFile2(path6, data);
|
|
10609
|
+
},
|
|
10610
|
+
rename: nodeRename,
|
|
10611
|
+
readFile: nodeReadFile2,
|
|
10612
|
+
rm: async (path6, options) => {
|
|
10613
|
+
await nodeRm(path6, options);
|
|
10614
|
+
}
|
|
10615
|
+
};
|
|
10616
|
+
function claimFileName(name) {
|
|
10617
|
+
return `${name}${OCCUPANCY_CLAIM.FILE_EXTENSION}`;
|
|
10618
|
+
}
|
|
10619
|
+
function claimFilePath(worktreesDir, name) {
|
|
10620
|
+
const validated = validateScopeToken(name);
|
|
10621
|
+
if (!validated.ok) return { ok: false, error: OCCUPANCY_ERROR.INVALID_NAME };
|
|
10622
|
+
return { ok: true, value: join30(worktreesDir, claimFileName(validated.value)) };
|
|
10623
|
+
}
|
|
10624
|
+
function classifyOccupancy(claim, probe) {
|
|
10625
|
+
if (claim === void 0) return OCCUPANCY_STATUS.UNCLAIMED;
|
|
10626
|
+
if (claim.host !== probe.currentHost()) return OCCUPANCY_STATUS.STALE;
|
|
10627
|
+
if (!probe.isAlive(claim.pid)) return OCCUPANCY_STATUS.STALE;
|
|
10628
|
+
const liveStartTime = probe.startTimeOf(claim.pid);
|
|
10629
|
+
if (liveStartTime !== void 0 && liveStartTime !== claim.startedAt) return OCCUPANCY_STATUS.STALE;
|
|
10630
|
+
return OCCUPANCY_STATUS.OCCUPIED;
|
|
10631
|
+
}
|
|
10632
|
+
async function writeClaim(worktreesDir, name, record, options = {}) {
|
|
10633
|
+
const fs7 = options.fs ?? defaultOccupancyFileSystem;
|
|
10634
|
+
const pathResult = claimFilePath(worktreesDir, name);
|
|
10635
|
+
if (!pathResult.ok) return pathResult;
|
|
10636
|
+
const claimPath = pathResult.value;
|
|
10637
|
+
const tempPath = `${claimPath}${OCCUPANCY_CLAIM.TEMP_EXTENSION}`;
|
|
10638
|
+
try {
|
|
10639
|
+
await fs7.mkdir(worktreesDir, { recursive: true });
|
|
10640
|
+
await fs7.writeFile(tempPath, serializeClaim(record));
|
|
10641
|
+
await fs7.rename(tempPath, claimPath);
|
|
10642
|
+
return { ok: true, value: claimPath };
|
|
10643
|
+
} catch (error) {
|
|
10644
|
+
return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_WRITE_FAILED, toErrorMessage3(error)) };
|
|
10645
|
+
}
|
|
10646
|
+
}
|
|
10647
|
+
async function readClaim(worktreesDir, name, options = {}) {
|
|
10648
|
+
const fs7 = options.fs ?? defaultOccupancyFileSystem;
|
|
10649
|
+
const pathResult = claimFilePath(worktreesDir, name);
|
|
10650
|
+
if (!pathResult.ok) return pathResult;
|
|
10651
|
+
let content;
|
|
10652
|
+
try {
|
|
10653
|
+
content = await fs7.readFile(pathResult.value, "utf8");
|
|
10654
|
+
} catch (error) {
|
|
10655
|
+
if (hasErrorCode(error, ERROR_CODE_NOT_FOUND)) return { ok: true, value: void 0 };
|
|
10656
|
+
return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_READ_FAILED, toErrorMessage3(error)) };
|
|
10657
|
+
}
|
|
10658
|
+
return parseClaim(content);
|
|
10659
|
+
}
|
|
10660
|
+
async function removeClaim(worktreesDir, name, options = {}) {
|
|
10661
|
+
const fs7 = options.fs ?? defaultOccupancyFileSystem;
|
|
10662
|
+
const pathResult = claimFilePath(worktreesDir, name);
|
|
10663
|
+
if (!pathResult.ok) return pathResult;
|
|
10664
|
+
try {
|
|
10665
|
+
await fs7.rm(pathResult.value, { force: true });
|
|
10666
|
+
return { ok: true, value: void 0 };
|
|
10667
|
+
} catch (error) {
|
|
10668
|
+
return { ok: false, error: formatOccupancyError(OCCUPANCY_ERROR.CLAIM_REMOVE_FAILED, toErrorMessage3(error)) };
|
|
10669
|
+
}
|
|
10670
|
+
}
|
|
10671
|
+
async function readOccupancy(worktreesDir, name, probe, options = {}) {
|
|
10672
|
+
const claimResult = await readClaim(worktreesDir, name, options);
|
|
10673
|
+
if (!claimResult.ok) return claimResult;
|
|
10674
|
+
return { ok: true, value: classifyOccupancy(claimResult.value, probe) };
|
|
10675
|
+
}
|
|
10676
|
+
function serializeClaim(record) {
|
|
10677
|
+
return JSON.stringify({
|
|
10678
|
+
sessionId: record.sessionId,
|
|
10679
|
+
host: record.host,
|
|
10680
|
+
pid: record.pid,
|
|
10681
|
+
startedAt: record.startedAt
|
|
10682
|
+
});
|
|
10683
|
+
}
|
|
10684
|
+
function parseClaim(content) {
|
|
10685
|
+
let parsed;
|
|
10686
|
+
try {
|
|
10687
|
+
parsed = JSON.parse(content);
|
|
10688
|
+
} catch {
|
|
10689
|
+
return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
|
|
10690
|
+
}
|
|
10691
|
+
if (!isWorktreeClaimRecord(parsed)) return { ok: false, error: OCCUPANCY_ERROR.CLAIM_MALFORMED };
|
|
10692
|
+
return { ok: true, value: parsed };
|
|
10693
|
+
}
|
|
10694
|
+
function isWorktreeClaimRecord(value) {
|
|
10695
|
+
if (typeof value !== "object" || value === null) return false;
|
|
10696
|
+
const candidate = value;
|
|
10697
|
+
return typeof candidate.sessionId === "string" && typeof candidate.host === "string" && typeof candidate.pid === "number" && typeof candidate.startedAt === "string";
|
|
10698
|
+
}
|
|
10699
|
+
function formatOccupancyError(code, detail) {
|
|
10700
|
+
return `${code}${ERROR_DETAIL_SEPARATOR2}${detail}`;
|
|
10701
|
+
}
|
|
10702
|
+
function toErrorMessage3(error) {
|
|
10703
|
+
return error instanceof Error ? error.message : String(error);
|
|
10704
|
+
}
|
|
10705
|
+
|
|
10706
|
+
// src/domains/worktree/process-table.ts
|
|
10707
|
+
import { spawnSync } from "child_process";
|
|
10708
|
+
import { hostname } from "os";
|
|
10709
|
+
var PS_COMMAND = "/bin/ps";
|
|
10710
|
+
var PS_FIELD = {
|
|
10711
|
+
START_TIME: "lstart",
|
|
10712
|
+
PARENT_PID: "ppid",
|
|
10713
|
+
// The full command line, not `comm` — `comm` reports only the executable
|
|
10714
|
+
// basename, so a shebang-installed agent run through an interpreter reads as
|
|
10715
|
+
// the interpreter (e.g. `node`) and the agent-runtime match misses it. `args`
|
|
10716
|
+
// carries the script path, so both native and interpreted agents are matched.
|
|
10717
|
+
COMMAND: "args"
|
|
10718
|
+
};
|
|
10719
|
+
var SIGNAL_LIVENESS_PROBE = 0;
|
|
10720
|
+
var PROCESS_EXISTS_NO_PERMISSION = "EPERM";
|
|
10721
|
+
var PID_RADIX2 = 10;
|
|
10722
|
+
var STABLE_PS_ENV = { ...process.env, TZ: "UTC", LC_ALL: "C" };
|
|
10723
|
+
function psField(pid, field) {
|
|
10724
|
+
const result = spawnSync(PS_COMMAND, ["-o", `${field}=`, "-p", String(pid)], {
|
|
10725
|
+
encoding: "utf8",
|
|
10726
|
+
env: STABLE_PS_ENV
|
|
10727
|
+
});
|
|
10728
|
+
if (result.status !== 0 || typeof result.stdout !== "string") return void 0;
|
|
10729
|
+
const value = result.stdout.trim();
|
|
10730
|
+
return value.length > 0 ? value : void 0;
|
|
10731
|
+
}
|
|
10732
|
+
var defaultProcessTable = {
|
|
10733
|
+
currentHost: () => hostname(),
|
|
10734
|
+
isAlive: (pid) => {
|
|
10735
|
+
try {
|
|
10736
|
+
process.kill(pid, SIGNAL_LIVENESS_PROBE);
|
|
10737
|
+
return true;
|
|
10738
|
+
} catch (error) {
|
|
10739
|
+
return hasErrorCode(error, PROCESS_EXISTS_NO_PERMISSION);
|
|
10740
|
+
}
|
|
10741
|
+
},
|
|
10742
|
+
startTimeOf: (pid) => psField(pid, PS_FIELD.START_TIME),
|
|
10743
|
+
parentOf: (pid) => {
|
|
10744
|
+
const value = psField(pid, PS_FIELD.PARENT_PID);
|
|
10745
|
+
if (value === void 0) return void 0;
|
|
10746
|
+
const parsed = Number.parseInt(value, PID_RADIX2);
|
|
10747
|
+
return Number.isInteger(parsed) ? parsed : void 0;
|
|
10748
|
+
},
|
|
10749
|
+
commandOf: (pid) => psField(pid, PS_FIELD.COMMAND)
|
|
10750
|
+
};
|
|
10751
|
+
|
|
10752
|
+
// src/commands/worktree/resolve.ts
|
|
10753
|
+
import { stat as stat4 } from "fs/promises";
|
|
10754
|
+
import { dirname as dirname8, resolve as resolve5 } from "path";
|
|
10755
|
+
|
|
10756
|
+
// src/domains/worktree/worktree-name.ts
|
|
10757
|
+
import { basename as basename4 } from "path";
|
|
10758
|
+
var SEPARATOR_CHARACTER = /[^a-z0-9_]/;
|
|
10759
|
+
var NAME_SEPARATOR = "-";
|
|
10760
|
+
function worktreeClaimName(worktreeRoot) {
|
|
10761
|
+
return basename4(worktreeRoot).toLowerCase().split(SEPARATOR_CHARACTER).filter((segment) => segment.length > 0).join(NAME_SEPARATOR);
|
|
10762
|
+
}
|
|
10763
|
+
|
|
10764
|
+
// src/commands/worktree/resolve.ts
|
|
10765
|
+
var WORKTREE_RESOLVE_ERROR = {
|
|
10766
|
+
NOT_A_WORKTREE: "path resolves to no worktree"
|
|
10767
|
+
};
|
|
10768
|
+
async function resolveWorktreesDir(options) {
|
|
10769
|
+
if (options.worktreesDir !== void 0) return options.worktreesDir;
|
|
10770
|
+
const resolved = await resolveWorktreesScopeDir({ cwd: options.cwd, deps: options.gitDeps });
|
|
10771
|
+
options.onWarning?.(resolved.warning);
|
|
10772
|
+
return resolved.worktreesDir;
|
|
10773
|
+
}
|
|
10774
|
+
async function resolveCurrentWorktreeName(options) {
|
|
10775
|
+
const worktree = await detectWorktreeProductRoot(options.cwd, options.gitDeps);
|
|
10776
|
+
return worktreeClaimName(worktree.productDir);
|
|
10777
|
+
}
|
|
10778
|
+
async function resolveTargetWorktree(options) {
|
|
10779
|
+
const base = options.cwd ?? process.cwd();
|
|
10780
|
+
const targetPath = options.worktree === void 0 ? base : resolve5(base, options.worktree);
|
|
10781
|
+
const targetGitPath = await isExistingNonDirectory(targetPath) ? dirname8(targetPath) : targetPath;
|
|
10782
|
+
const worktree = await detectWorktreeProductRoot(targetGitPath, options.gitDeps);
|
|
10783
|
+
if (!worktree.isGitRepo) {
|
|
10784
|
+
return { ok: false, error: `${WORKTREE_RESOLVE_ERROR.NOT_A_WORKTREE}: ${options.worktree ?? base}` };
|
|
10785
|
+
}
|
|
10786
|
+
return { ok: true, value: { name: worktreeClaimName(worktree.productDir), worktreeRoot: worktree.productDir } };
|
|
10787
|
+
}
|
|
10788
|
+
async function isExistingNonDirectory(path6) {
|
|
10789
|
+
try {
|
|
10790
|
+
const pathStats = await stat4(path6);
|
|
10791
|
+
return !pathStats.isDirectory();
|
|
10792
|
+
} catch {
|
|
10793
|
+
return false;
|
|
10794
|
+
}
|
|
10795
|
+
}
|
|
10796
|
+
|
|
10797
|
+
// src/commands/worktree/claim.ts
|
|
10798
|
+
async function claimCommand(options) {
|
|
10799
|
+
const table = options.processTable ?? defaultProcessTable;
|
|
10800
|
+
const controlling = resolveControllingProcess(options.selfPid ?? process.pid, table, options.env ?? process.env);
|
|
10801
|
+
if (!controlling.ok) return controlling;
|
|
10802
|
+
const worktreesDir = await resolveWorktreesDir(options);
|
|
10803
|
+
const name = await resolveCurrentWorktreeName(options);
|
|
10804
|
+
return writeClaim(
|
|
10805
|
+
worktreesDir,
|
|
10806
|
+
name,
|
|
10807
|
+
{
|
|
10808
|
+
sessionId: options.sessionId,
|
|
10809
|
+
host: controlling.value.host,
|
|
10810
|
+
pid: controlling.value.pid,
|
|
10811
|
+
startedAt: controlling.value.startedAt
|
|
10812
|
+
},
|
|
10813
|
+
{ fs: options.fs }
|
|
10814
|
+
);
|
|
10815
|
+
}
|
|
10816
|
+
|
|
10817
|
+
// src/commands/worktree/release.ts
|
|
10818
|
+
async function releaseCommand2(options) {
|
|
10819
|
+
const worktreesDir = await resolveWorktreesDir(options);
|
|
10820
|
+
const name = await resolveCurrentWorktreeName(options);
|
|
10821
|
+
return removeClaim(worktreesDir, name, { fs: options.fs });
|
|
10822
|
+
}
|
|
10823
|
+
|
|
10824
|
+
// src/commands/worktree/status.ts
|
|
10825
|
+
var WORKTREE_STATUS_FORMAT = {
|
|
10826
|
+
JSON: "json",
|
|
10827
|
+
TEXT: "text"
|
|
10828
|
+
};
|
|
10829
|
+
async function statusCommand2(options) {
|
|
10830
|
+
const table = options.processTable ?? defaultProcessTable;
|
|
10831
|
+
const multiTargetRequest = options.worktrees !== void 0 && options.worktrees.length > 1;
|
|
10832
|
+
const targets = await resolveStatusTargets(options);
|
|
10833
|
+
if (!targets.ok) return targets;
|
|
10834
|
+
const records = [];
|
|
10835
|
+
for (const target of targets.value) {
|
|
10836
|
+
const worktreesDir = await resolveWorktreesDir({ ...options, cwd: target.worktreeRoot });
|
|
10837
|
+
const occupancy = await readOccupancy(worktreesDir, target.name, table, { fs: options.fs });
|
|
10838
|
+
if (!occupancy.ok) return occupancy;
|
|
10839
|
+
records.push({ worktree: target.name, status: occupancy.value });
|
|
10840
|
+
}
|
|
10841
|
+
return { ok: true, value: renderStatus(records, options.format, multiTargetRequest) };
|
|
10842
|
+
}
|
|
10843
|
+
async function resolveStatusTargets(options) {
|
|
10844
|
+
const requested = options.worktrees;
|
|
10845
|
+
if (requested === void 0 || requested.length === 0) {
|
|
10846
|
+
const target = await resolveTargetWorktree(options);
|
|
10847
|
+
if (!target.ok) return target;
|
|
10848
|
+
return { ok: true, value: [target.value] };
|
|
10849
|
+
}
|
|
10850
|
+
const targets = [];
|
|
10851
|
+
let firstError;
|
|
10852
|
+
for (const worktree of requested) {
|
|
10853
|
+
const target = await resolveTargetWorktree({ ...options, worktree });
|
|
10854
|
+
if (target.ok) {
|
|
10855
|
+
targets.push(target.value);
|
|
10856
|
+
} else {
|
|
10857
|
+
firstError ??= target.error;
|
|
10858
|
+
}
|
|
10859
|
+
}
|
|
10860
|
+
if (targets.length === 0) {
|
|
10861
|
+
return { ok: false, error: firstError ?? "no worktree status targets resolved" };
|
|
10862
|
+
}
|
|
10863
|
+
return { ok: true, value: targets };
|
|
10864
|
+
}
|
|
10865
|
+
function renderStatus(records, format2, multiTargetRequest) {
|
|
10866
|
+
if (format2 === WORKTREE_STATUS_FORMAT.JSON) {
|
|
10867
|
+
return JSON.stringify(multiTargetRequest ? records : records[0]);
|
|
10868
|
+
}
|
|
10869
|
+
return records.map(renderTextStatus).join("\n");
|
|
10870
|
+
}
|
|
10871
|
+
function renderTextStatus(record) {
|
|
10872
|
+
return `${record.worktree} ${record.status}`;
|
|
10873
|
+
}
|
|
10874
|
+
|
|
10875
|
+
// src/interfaces/cli/worktree.ts
|
|
10876
|
+
var WORKTREE_CLI = {
|
|
10877
|
+
COMMAND: "worktree",
|
|
10878
|
+
CLAIM: "claim",
|
|
10879
|
+
STATUS: "status",
|
|
10880
|
+
RELEASE: "release",
|
|
10881
|
+
WORKTREE_ARGUMENT: "[worktrees...]",
|
|
10882
|
+
SESSION_ID_FLAG: "--session-id",
|
|
10883
|
+
FORMAT_FLAG: "--format",
|
|
10884
|
+
WORKTREES_DIR_FLAG: "--worktrees-dir"
|
|
10885
|
+
};
|
|
10886
|
+
var WORKTREE_DOMAIN_DESCRIPTION = "Coordinate worktree occupancy across a bare-repository pool";
|
|
10887
|
+
function handleError2(error) {
|
|
10888
|
+
console.error("Error:", error);
|
|
10889
|
+
process.exit(1);
|
|
10890
|
+
}
|
|
10891
|
+
function registerWorktreeCommands(worktreeCmd) {
|
|
10892
|
+
worktreeCmd.command(WORKTREE_CLI.CLAIM).description("Record a worktree-occupancy claim for the running worktree").requiredOption(`${WORKTREE_CLI.SESSION_ID_FLAG} <id>`, "Claiming agent session id").option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (options) => {
|
|
10893
|
+
const result = await claimCommand({
|
|
10894
|
+
sessionId: options.sessionId,
|
|
10895
|
+
worktreesDir: options.worktreesDir,
|
|
10896
|
+
onWarning: writeWarning
|
|
10897
|
+
});
|
|
10898
|
+
if (!result.ok) handleError2(result.error);
|
|
10899
|
+
});
|
|
10900
|
+
worktreeCmd.command(`${WORKTREE_CLI.STATUS} ${WORKTREE_CLI.WORKTREE_ARGUMENT}`).description("Report a worktree's occupancy (occupied | unclaimed | stale)").option(`${WORKTREE_CLI.FORMAT_FLAG} <format>`, "Output format (text|json)", WORKTREE_STATUS_FORMAT.TEXT).option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (worktrees, options) => {
|
|
10901
|
+
const result = await statusCommand2({
|
|
10902
|
+
worktrees,
|
|
10903
|
+
format: options.format,
|
|
10904
|
+
worktreesDir: options.worktreesDir,
|
|
10905
|
+
onWarning: writeWarning
|
|
10906
|
+
});
|
|
10907
|
+
if (!result.ok) handleError2(result.error);
|
|
10908
|
+
console.log(result.value);
|
|
10909
|
+
});
|
|
10910
|
+
worktreeCmd.command(WORKTREE_CLI.RELEASE).description("Release the running worktree's occupancy claim").option(`${WORKTREE_CLI.WORKTREES_DIR_FLAG} <path>`, "Explicit .spx/worktrees directory").action(async (options) => {
|
|
10911
|
+
const result = await releaseCommand2({ worktreesDir: options.worktreesDir, onWarning: writeWarning });
|
|
10912
|
+
if (!result.ok) handleError2(result.error);
|
|
10913
|
+
});
|
|
10914
|
+
}
|
|
10915
|
+
var worktreeDomain = {
|
|
10916
|
+
name: WORKTREE_CLI.COMMAND,
|
|
10917
|
+
description: WORKTREE_DOMAIN_DESCRIPTION,
|
|
10918
|
+
register: (program2) => {
|
|
10919
|
+
const worktreeCmd = program2.command(WORKTREE_CLI.COMMAND).description(WORKTREE_DOMAIN_DESCRIPTION);
|
|
10920
|
+
registerWorktreeCommands(worktreeCmd);
|
|
10921
|
+
}
|
|
10922
|
+
};
|
|
10923
|
+
|
|
10526
10924
|
// src/interfaces/cli/registry.ts
|
|
10527
10925
|
var CLI_DOMAINS = [
|
|
10528
10926
|
claudeDomain,
|
|
@@ -10531,7 +10929,8 @@ var CLI_DOMAINS = [
|
|
|
10531
10929
|
sessionDomain,
|
|
10532
10930
|
specDomain,
|
|
10533
10931
|
testingDomain,
|
|
10534
|
-
validationDomain
|
|
10932
|
+
validationDomain,
|
|
10933
|
+
worktreeDomain
|
|
10535
10934
|
];
|
|
10536
10935
|
|
|
10537
10936
|
// src/cli.ts
|