@staff0rd/assist 0.434.0 → 0.435.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/commands/sessions/web/bundle.js +2 -2
- package/dist/index.js +235 -125
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@staff0rd/assist",
|
|
9
|
-
version: "0.
|
|
9
|
+
version: "0.435.0",
|
|
10
10
|
type: "module",
|
|
11
11
|
main: "dist/index.js",
|
|
12
12
|
bin: {
|
|
@@ -25245,7 +25245,7 @@ var persistedSessionSchema = z5.object({
|
|
|
25245
25245
|
assistArgs: z5.array(z5.string()).optional(),
|
|
25246
25246
|
activity: activitySchema.optional(),
|
|
25247
25247
|
starred: z5.boolean().optional(),
|
|
25248
|
-
undurable: z5.object({ reason: z5.string() }).optional()
|
|
25248
|
+
undurable: z5.object({ reason: z5.string(), removesTree: z5.boolean().optional() }).optional()
|
|
25249
25249
|
});
|
|
25250
25250
|
|
|
25251
25251
|
// src/commands/sessions/daemon/toPersistedSession.ts
|
|
@@ -25304,40 +25304,95 @@ function logPersist(live) {
|
|
|
25304
25304
|
);
|
|
25305
25305
|
}
|
|
25306
25306
|
|
|
25307
|
+
// src/commands/sessions/daemon/clearPersistedSessionsOnDrain.ts
|
|
25308
|
+
function clearPersistedSessionsOnDrain() {
|
|
25309
|
+
const held = loadPersistedSessions().filter((s) => s.status === "stopped");
|
|
25310
|
+
savePersistedSessions(held);
|
|
25311
|
+
console.log(
|
|
25312
|
+
held.length > 0 ? `Sessions daemon is not running; cleared persisted sessions (kept ${held.length} stopped session(s) holding unpushed work)` : "Sessions daemon is not running; cleared persisted sessions"
|
|
25313
|
+
);
|
|
25314
|
+
}
|
|
25315
|
+
|
|
25316
|
+
// src/commands/sessions/daemon/readDaemonMessage.ts
|
|
25317
|
+
function readDaemonMessage(lines, timeoutMs, fallback, match) {
|
|
25318
|
+
return new Promise((resolve20) => {
|
|
25319
|
+
const finish = (value) => {
|
|
25320
|
+
clearTimeout(timer);
|
|
25321
|
+
lines.off("line", onLine);
|
|
25322
|
+
resolve20(value);
|
|
25323
|
+
};
|
|
25324
|
+
const timer = setTimeout(() => finish(fallback), timeoutMs);
|
|
25325
|
+
const onLine = (line) => {
|
|
25326
|
+
try {
|
|
25327
|
+
const matched = match(JSON.parse(line));
|
|
25328
|
+
if (matched !== void 0) finish(matched);
|
|
25329
|
+
} catch {
|
|
25330
|
+
}
|
|
25331
|
+
};
|
|
25332
|
+
lines.on("line", onLine);
|
|
25333
|
+
});
|
|
25334
|
+
}
|
|
25335
|
+
|
|
25307
25336
|
// src/commands/sessions/daemon/drainDaemon.ts
|
|
25308
25337
|
var DRAIN_TIMEOUT_MS = 5e3;
|
|
25309
|
-
|
|
25338
|
+
var LIST_TIMEOUT_MS = 2e3;
|
|
25339
|
+
async function drainDaemon(options2 = {}) {
|
|
25310
25340
|
let socket;
|
|
25311
25341
|
try {
|
|
25312
25342
|
socket = await connectToDaemon();
|
|
25313
25343
|
} catch {
|
|
25314
|
-
|
|
25315
|
-
console.log("Sessions daemon is not running; cleared persisted sessions");
|
|
25344
|
+
clearPersistedSessionsOnDrain();
|
|
25316
25345
|
return;
|
|
25317
25346
|
}
|
|
25318
|
-
const
|
|
25347
|
+
const lines = createInterface6({ input: socket });
|
|
25348
|
+
lines.on("error", () => {
|
|
25349
|
+
});
|
|
25350
|
+
const live = await liveSessions(lines);
|
|
25351
|
+
if (live.length > 0 && options2.yes !== true) {
|
|
25352
|
+
reportLive(live);
|
|
25353
|
+
if (!await confirmDrain()) {
|
|
25354
|
+
socket.destroy();
|
|
25355
|
+
return;
|
|
25356
|
+
}
|
|
25357
|
+
}
|
|
25358
|
+
const count7 = await requestDrain(socket, lines);
|
|
25319
25359
|
socket.destroy();
|
|
25320
25360
|
console.log(`Drained ${count7} session(s)`);
|
|
25321
25361
|
}
|
|
25322
|
-
function
|
|
25362
|
+
function reportLive(live) {
|
|
25363
|
+
console.log(`${live.length} live session(s) would be closed:`);
|
|
25364
|
+
for (const s of live)
|
|
25365
|
+
console.log(` - ${s.name} [${s.status}]${s.cwd ? ` in ${s.cwd}` : ""}`);
|
|
25366
|
+
}
|
|
25367
|
+
async function confirmDrain() {
|
|
25368
|
+
if (!process.stdin.isTTY) {
|
|
25369
|
+
console.log("Refusing to close live sessions; re-run with --yes");
|
|
25370
|
+
process.exitCode = 1;
|
|
25371
|
+
return false;
|
|
25372
|
+
}
|
|
25373
|
+
if (await promptConfirm("Close them all?", false)) return true;
|
|
25374
|
+
console.log("Drain cancelled");
|
|
25375
|
+
return false;
|
|
25376
|
+
}
|
|
25377
|
+
function liveSessions(lines) {
|
|
25378
|
+
return readDaemonMessage(
|
|
25379
|
+
lines,
|
|
25380
|
+
LIST_TIMEOUT_MS,
|
|
25381
|
+
[],
|
|
25382
|
+
(data) => data.type === "sessions" ? (data.sessions ?? []).filter(
|
|
25383
|
+
(s) => s.status === "running" || s.status === "waiting"
|
|
25384
|
+
) : void 0
|
|
25385
|
+
);
|
|
25386
|
+
}
|
|
25387
|
+
function requestDrain(socket, lines) {
|
|
25323
25388
|
socket.write(`${JSON.stringify({ type: "drain" })}
|
|
25324
25389
|
`);
|
|
25325
|
-
return
|
|
25326
|
-
|
|
25327
|
-
|
|
25328
|
-
|
|
25329
|
-
|
|
25330
|
-
|
|
25331
|
-
try {
|
|
25332
|
-
const data = JSON.parse(line);
|
|
25333
|
-
if (data.type === "drained") {
|
|
25334
|
-
clearTimeout(timer);
|
|
25335
|
-
resolve20(data.count ?? 0);
|
|
25336
|
-
}
|
|
25337
|
-
} catch {
|
|
25338
|
-
}
|
|
25339
|
-
});
|
|
25340
|
-
});
|
|
25390
|
+
return readDaemonMessage(
|
|
25391
|
+
lines,
|
|
25392
|
+
DRAIN_TIMEOUT_MS,
|
|
25393
|
+
0,
|
|
25394
|
+
(data) => data.type === "drained" ? data.count ?? 0 : void 0
|
|
25395
|
+
);
|
|
25341
25396
|
}
|
|
25342
25397
|
|
|
25343
25398
|
// src/commands/sessions/daemon/runDaemon.ts
|
|
@@ -25747,8 +25802,40 @@ function createRunSession(id, runName, runArgs, cwd, meta = serverRunMeta(runNam
|
|
|
25747
25802
|
};
|
|
25748
25803
|
}
|
|
25749
25804
|
|
|
25805
|
+
// src/commands/sessions/daemon/killPtyTree.ts
|
|
25806
|
+
function killPtyTree(pty2) {
|
|
25807
|
+
if (process.platform === "win32") {
|
|
25808
|
+
pty2.kill();
|
|
25809
|
+
return;
|
|
25810
|
+
}
|
|
25811
|
+
try {
|
|
25812
|
+
process.kill(-pty2.pid, "SIGHUP");
|
|
25813
|
+
} catch {
|
|
25814
|
+
try {
|
|
25815
|
+
pty2.kill();
|
|
25816
|
+
} catch {
|
|
25817
|
+
}
|
|
25818
|
+
}
|
|
25819
|
+
}
|
|
25820
|
+
|
|
25821
|
+
// src/commands/sessions/daemon/worktree/treeUnderClose.ts
|
|
25822
|
+
function treeUnderClose(session) {
|
|
25823
|
+
if (session.worktree) return { path: session.worktree.path, removable: true };
|
|
25824
|
+
if (session.cwd) return { path: session.cwd, removable: false };
|
|
25825
|
+
return void 0;
|
|
25826
|
+
}
|
|
25827
|
+
|
|
25828
|
+
// src/commands/sessions/daemon/worktree/otherTreeHolders.ts
|
|
25829
|
+
function otherTreeHolders(sessions, session) {
|
|
25830
|
+
const tree = treeUnderClose(session)?.path;
|
|
25831
|
+
if (!tree) return [];
|
|
25832
|
+
return [...sessions.values()].filter(
|
|
25833
|
+
(s) => s.id !== session.id && treeUnderClose(s)?.path === tree
|
|
25834
|
+
);
|
|
25835
|
+
}
|
|
25836
|
+
|
|
25750
25837
|
// src/commands/sessions/daemon/worktree/reapWorktree.ts
|
|
25751
|
-
import { existsSync as
|
|
25838
|
+
import { existsSync as existsSync59 } from "fs";
|
|
25752
25839
|
|
|
25753
25840
|
// src/commands/sessions/daemon/worktree/git.ts
|
|
25754
25841
|
import { execFile as execFile7, execFileSync as execFileSync12 } from "child_process";
|
|
@@ -25779,6 +25866,16 @@ async function gitOrNull(cwd, args) {
|
|
|
25779
25866
|
return null;
|
|
25780
25867
|
}
|
|
25781
25868
|
}
|
|
25869
|
+
async function gitResult(cwd, args) {
|
|
25870
|
+
try {
|
|
25871
|
+
return { ok: true, out: (await git3(cwd, args)).trim() };
|
|
25872
|
+
} catch (error) {
|
|
25873
|
+
return {
|
|
25874
|
+
ok: false,
|
|
25875
|
+
error: error instanceof Error ? error.message : String(error)
|
|
25876
|
+
};
|
|
25877
|
+
}
|
|
25878
|
+
}
|
|
25782
25879
|
function gitSync(cwd, args) {
|
|
25783
25880
|
const { file, argv, options: options2 } = gitInvocation(cwd, args);
|
|
25784
25881
|
return execFileSync12(file, argv, {
|
|
@@ -25818,6 +25915,7 @@ function gitCommonDir(cwd) {
|
|
|
25818
25915
|
}
|
|
25819
25916
|
|
|
25820
25917
|
// src/commands/sessions/daemon/worktree/treeDurability.ts
|
|
25918
|
+
import { existsSync as existsSync58 } from "fs";
|
|
25821
25919
|
function treeDurability(state) {
|
|
25822
25920
|
if (state.dirty) return { durable: false, reason: "uncommitted changes" };
|
|
25823
25921
|
if (!state.push && state.localOnlyCommits)
|
|
@@ -25825,35 +25923,27 @@ function treeDurability(state) {
|
|
|
25825
25923
|
return { durable: true };
|
|
25826
25924
|
}
|
|
25827
25925
|
async function hasLocalOnlyCommits(cwd) {
|
|
25828
|
-
const upstream = await
|
|
25926
|
+
const upstream = await gitResult(cwd, [
|
|
25829
25927
|
"rev-parse",
|
|
25830
25928
|
"--abbrev-ref",
|
|
25831
25929
|
"--symbolic-full-name",
|
|
25832
25930
|
"@{upstream}"
|
|
25833
25931
|
]);
|
|
25834
|
-
if (upstream) {
|
|
25835
|
-
const ahead = await
|
|
25932
|
+
if (upstream.ok && upstream.out) {
|
|
25933
|
+
const ahead = await gitResult(cwd, [
|
|
25836
25934
|
"rev-list",
|
|
25837
25935
|
"--count",
|
|
25838
25936
|
"@{upstream}..HEAD"
|
|
25839
25937
|
]);
|
|
25840
|
-
return Number(ahead
|
|
25938
|
+
return !ahead.ok || Number(ahead.out) > 0;
|
|
25841
25939
|
}
|
|
25842
|
-
const remoteBranches = await
|
|
25940
|
+
const remoteBranches = await gitResult(cwd, [
|
|
25843
25941
|
"branch",
|
|
25844
25942
|
"-r",
|
|
25845
25943
|
"--contains",
|
|
25846
25944
|
"HEAD"
|
|
25847
25945
|
]);
|
|
25848
|
-
return !remoteBranches;
|
|
25849
|
-
}
|
|
25850
|
-
async function collectTreeState(cwd, push) {
|
|
25851
|
-
const status3 = await gitOrNull(cwd, ["status", "--porcelain"]);
|
|
25852
|
-
return {
|
|
25853
|
-
dirty: status3 !== null,
|
|
25854
|
-
push,
|
|
25855
|
-
localOnlyCommits: await hasLocalOnlyCommits(cwd)
|
|
25856
|
-
};
|
|
25946
|
+
return !remoteBranches.ok || remoteBranches.out === "";
|
|
25857
25947
|
}
|
|
25858
25948
|
function pushOnCommit(cwd) {
|
|
25859
25949
|
try {
|
|
@@ -25863,12 +25953,20 @@ function pushOnCommit(cwd) {
|
|
|
25863
25953
|
}
|
|
25864
25954
|
}
|
|
25865
25955
|
async function checkDurability(cwd) {
|
|
25866
|
-
|
|
25956
|
+
if (!existsSync58(cwd)) return { durable: true };
|
|
25957
|
+
const status3 = await gitResult(cwd, ["status", "--porcelain"]);
|
|
25958
|
+
if (!status3.ok)
|
|
25959
|
+
return { durable: false, reason: `tree state unreadable: ${status3.error}` };
|
|
25960
|
+
return treeDurability({
|
|
25961
|
+
dirty: status3.out !== "",
|
|
25962
|
+
push: pushOnCommit(cwd),
|
|
25963
|
+
localOnlyCommits: await hasLocalOnlyCommits(cwd)
|
|
25964
|
+
});
|
|
25867
25965
|
}
|
|
25868
25966
|
|
|
25869
25967
|
// src/commands/sessions/daemon/worktree/reapWorktree.ts
|
|
25870
25968
|
async function reapWorktree(worktreePath, force = false) {
|
|
25871
|
-
if (!
|
|
25969
|
+
if (!existsSync59(worktreePath)) {
|
|
25872
25970
|
daemonLog(`worktree ${worktreePath} already gone; skipping reap`);
|
|
25873
25971
|
return false;
|
|
25874
25972
|
}
|
|
@@ -25915,7 +26013,8 @@ async function deleteBranch(clone, branch2) {
|
|
|
25915
26013
|
function dismissSession(sessions, id) {
|
|
25916
26014
|
const s = sessions.get(id);
|
|
25917
26015
|
if (!s) return false;
|
|
25918
|
-
|
|
26016
|
+
const sharedWith = otherTreeHolders(sessions, s);
|
|
26017
|
+
if (s.status !== "done" && s.pty) killPtyTree(s.pty);
|
|
25919
26018
|
s.activityWatcher?.close();
|
|
25920
26019
|
s.transcriptWatcher?.close();
|
|
25921
26020
|
s.gitWatcher?.close();
|
|
@@ -25923,36 +26022,39 @@ function dismissSession(sessions, id) {
|
|
|
25923
26022
|
if (s.activity?.itemId != null) releaseLock(s.activity.itemId);
|
|
25924
26023
|
sessions.delete(id);
|
|
25925
26024
|
daemonLog(`session ${id} dismissed (${s.name})`);
|
|
25926
|
-
if (s.worktree)
|
|
26025
|
+
if (!s.worktree) return true;
|
|
26026
|
+
if (sharedWith.length > 0)
|
|
26027
|
+
daemonLog(
|
|
26028
|
+
`worktree ${s.worktree.path} kept: still held by session(s) ${sharedWith.map((h) => h.id).join(", ")}`
|
|
26029
|
+
);
|
|
26030
|
+
else void reapWorktree(s.worktree.path);
|
|
25927
26031
|
return true;
|
|
25928
26032
|
}
|
|
25929
|
-
function drainSessions(sessions, onDrained) {
|
|
25930
|
-
const names = [...sessions.values()].map((s) => s.name);
|
|
25931
|
-
const ids = [...sessions.keys()];
|
|
25932
|
-
for (const id of ids) dismissSession(sessions, id);
|
|
25933
|
-
onDrained();
|
|
25934
|
-
daemonLog(
|
|
25935
|
-
ids.length > 0 ? `drained ${ids.length} session(s): ${names.join(", ")}` : "drained 0 sessions"
|
|
25936
|
-
);
|
|
25937
|
-
return ids.length;
|
|
25938
|
-
}
|
|
25939
26033
|
|
|
25940
|
-
// src/commands/sessions/daemon/
|
|
25941
|
-
|
|
25942
|
-
|
|
25943
|
-
|
|
25944
|
-
|
|
25945
|
-
|
|
26034
|
+
// src/commands/sessions/daemon/worktree/worktreeConfigFor.ts
|
|
26035
|
+
var DISABLED = {
|
|
26036
|
+
enabled: false,
|
|
26037
|
+
install: true,
|
|
26038
|
+
copy: [".env", "settings.local.json", ".claude/settings.local.json"]
|
|
26039
|
+
};
|
|
26040
|
+
function worktreeConfigFor(cwd) {
|
|
25946
26041
|
try {
|
|
25947
|
-
|
|
26042
|
+
return loadConfigFrom(cwd).worktree ?? DISABLED;
|
|
25948
26043
|
} catch {
|
|
25949
|
-
|
|
25950
|
-
pty2.kill();
|
|
25951
|
-
} catch {
|
|
25952
|
-
}
|
|
26044
|
+
return DISABLED;
|
|
25953
26045
|
}
|
|
25954
26046
|
}
|
|
25955
26047
|
|
|
26048
|
+
// src/commands/sessions/daemon/worktree/closeGateApplies.ts
|
|
26049
|
+
function closeGateApplies(sessions, session) {
|
|
26050
|
+
if (otherTreeHolders(sessions, session).length > 0) return false;
|
|
26051
|
+
if (session.worktree) return true;
|
|
26052
|
+
if (!session.cwd) return false;
|
|
26053
|
+
if (session.status !== "running" && session.status !== "waiting")
|
|
26054
|
+
return false;
|
|
26055
|
+
return worktreeConfigFor(session.cwd).enabled === true;
|
|
26056
|
+
}
|
|
26057
|
+
|
|
25956
26058
|
// src/commands/sessions/daemon/setStatus.ts
|
|
25957
26059
|
function setStatus2(session, newStatus) {
|
|
25958
26060
|
const now = Date.now();
|
|
@@ -25964,11 +26066,11 @@ function setStatus2(session, newStatus) {
|
|
|
25964
26066
|
}
|
|
25965
26067
|
|
|
25966
26068
|
// src/commands/sessions/daemon/worktree/watchGitState.ts
|
|
25967
|
-
import { existsSync as
|
|
26069
|
+
import { existsSync as existsSync60, watch } from "fs";
|
|
25968
26070
|
var DEBOUNCE_MS = 500;
|
|
25969
26071
|
function watchGitState(cwd, onChange) {
|
|
25970
26072
|
const common = gitCommonDir(cwd);
|
|
25971
|
-
if (!common || !
|
|
26073
|
+
if (!common || !existsSync60(common)) return void 0;
|
|
25972
26074
|
let timer = null;
|
|
25973
26075
|
try {
|
|
25974
26076
|
return watch(common, { recursive: true }, () => {
|
|
@@ -25983,19 +26085,19 @@ function watchGitState(cwd, onChange) {
|
|
|
25983
26085
|
}
|
|
25984
26086
|
}
|
|
25985
26087
|
|
|
25986
|
-
// src/commands/sessions/daemon/worktree/
|
|
25987
|
-
async function
|
|
25988
|
-
const
|
|
25989
|
-
if (!
|
|
26088
|
+
// src/commands/sessions/daemon/worktree/resolveCloseDurability.ts
|
|
26089
|
+
async function resolveCloseDurability(session, finalize, notify2) {
|
|
26090
|
+
const tree = treeUnderClose(session);
|
|
26091
|
+
if (!tree) {
|
|
25990
26092
|
finalize();
|
|
25991
26093
|
return;
|
|
25992
26094
|
}
|
|
25993
|
-
const durability = await checkDurability(
|
|
26095
|
+
const durability = await checkDurability(tree.path);
|
|
25994
26096
|
if (!durability.durable) {
|
|
25995
|
-
holdStopped(session, durability.reason, finalize, notify2);
|
|
26097
|
+
holdStopped(session, tree, durability.reason, finalize, notify2);
|
|
25996
26098
|
return;
|
|
25997
26099
|
}
|
|
25998
|
-
await reapWorktree(
|
|
26100
|
+
if (tree.removable) await reapWorktree(tree.path);
|
|
25999
26101
|
session.worktree = void 0;
|
|
26000
26102
|
session.undurable = void 0;
|
|
26001
26103
|
session.closing = void 0;
|
|
@@ -26003,16 +26105,18 @@ async function resolveDoneDurability(session, finalize, notify2) {
|
|
|
26003
26105
|
session.gitWatcher = void 0;
|
|
26004
26106
|
finalize();
|
|
26005
26107
|
}
|
|
26006
|
-
function holdStopped(session, reason, finalize, notify2) {
|
|
26108
|
+
function holdStopped(session, tree, reason, finalize, notify2) {
|
|
26007
26109
|
if (session.undurable?.reason !== reason)
|
|
26008
|
-
daemonLog(
|
|
26009
|
-
|
|
26110
|
+
daemonLog(
|
|
26111
|
+
`session ${session.id} stopped; ${tree.removable ? "reap" : "close"} blocked in ${tree.path}: ${reason}`
|
|
26112
|
+
);
|
|
26113
|
+
session.undurable = { reason, removesTree: tree.removable };
|
|
26010
26114
|
session.closing = void 0;
|
|
26011
26115
|
setStatus2(session, "stopped");
|
|
26012
|
-
if (!session.gitWatcher
|
|
26013
|
-
session.gitWatcher = watchGitState(
|
|
26116
|
+
if (!session.gitWatcher)
|
|
26117
|
+
session.gitWatcher = watchGitState(tree.path, () => {
|
|
26014
26118
|
if (session.status !== "stopped") return;
|
|
26015
|
-
void
|
|
26119
|
+
void resolveCloseDurability(session, finalize, notify2);
|
|
26016
26120
|
});
|
|
26017
26121
|
notify2();
|
|
26018
26122
|
}
|
|
@@ -26020,14 +26124,15 @@ function holdStopped(session, reason, finalize, notify2) {
|
|
|
26020
26124
|
// src/commands/sessions/daemon/dismissSessionGated.ts
|
|
26021
26125
|
function dismissSessionGated(sessions, id, notify2, discard = false) {
|
|
26022
26126
|
const s = sessions.get(id);
|
|
26023
|
-
|
|
26127
|
+
const tree = s && treeUnderClose(s);
|
|
26128
|
+
if (!s || !tree || !closeGateApplies(sessions, s)) {
|
|
26024
26129
|
if (dismissSession(sessions, id)) notify2();
|
|
26025
26130
|
return;
|
|
26026
26131
|
}
|
|
26027
26132
|
const removeCard = () => {
|
|
26028
26133
|
if (dismissSession(sessions, id)) notify2();
|
|
26029
26134
|
};
|
|
26030
|
-
const
|
|
26135
|
+
const discardWork = async () => {
|
|
26031
26136
|
if (s.worktree) {
|
|
26032
26137
|
await reapWorktree(s.worktree.path, true);
|
|
26033
26138
|
s.worktree = void 0;
|
|
@@ -26036,10 +26141,10 @@ function dismissSessionGated(sessions, id, notify2, discard = false) {
|
|
|
26036
26141
|
};
|
|
26037
26142
|
s.closing = true;
|
|
26038
26143
|
daemonLog(
|
|
26039
|
-
`session ${id} closing: ${discard ? "discarding" : "checking durability of"} ${
|
|
26144
|
+
`session ${id} closing: ${discard ? "discarding" : "checking durability of"} ${tree.path}`
|
|
26040
26145
|
);
|
|
26041
26146
|
if (s.pty) {
|
|
26042
|
-
s.pendingDismiss = discard ? () => void
|
|
26147
|
+
s.pendingDismiss = discard ? () => void discardWork() : () => void resolveCloseDurability(s, removeCard, notify2);
|
|
26043
26148
|
daemonLog(
|
|
26044
26149
|
`session ${id} ${discard ? "discard" : "dismiss"} requested: killing process tree first`
|
|
26045
26150
|
);
|
|
@@ -26048,8 +26153,27 @@ function dismissSessionGated(sessions, id, notify2, discard = false) {
|
|
|
26048
26153
|
return;
|
|
26049
26154
|
}
|
|
26050
26155
|
notify2();
|
|
26051
|
-
if (discard) void
|
|
26052
|
-
else void
|
|
26156
|
+
if (discard) void discardWork();
|
|
26157
|
+
else void resolveCloseDurability(s, removeCard, notify2);
|
|
26158
|
+
}
|
|
26159
|
+
|
|
26160
|
+
// src/commands/sessions/daemon/drainSessions.ts
|
|
26161
|
+
function drainSessions(sessions, notify2) {
|
|
26162
|
+
const drained = [...sessions.values()].map((s) => ({
|
|
26163
|
+
id: s.id,
|
|
26164
|
+
name: s.name
|
|
26165
|
+
}));
|
|
26166
|
+
for (const { id } of drained) dismissSessionGated(sessions, id, notify2);
|
|
26167
|
+
notify2();
|
|
26168
|
+
daemonLog(
|
|
26169
|
+
drained.length > 0 ? `drained ${drained.length} session(s): ${drained.map((s) => s.name).join(", ")}` : "drained 0 sessions"
|
|
26170
|
+
);
|
|
26171
|
+
const gated = [...sessions.values()];
|
|
26172
|
+
if (gated.length > 0)
|
|
26173
|
+
daemonLog(
|
|
26174
|
+
`${gated.length} drained session(s) closing under the durability gate: ${gated.map((s) => s.name).join(", ")} \u2014 any holding unpushed work stay as stopped cards`
|
|
26175
|
+
);
|
|
26176
|
+
return drained.length;
|
|
26053
26177
|
}
|
|
26054
26178
|
|
|
26055
26179
|
// src/shared/db/recordPhaseActiveMs.ts
|
|
@@ -26464,7 +26588,7 @@ function applyStatusChange(session, status3, exitCode, dismiss, notify2, reuseFo
|
|
|
26464
26588
|
`session ${session.id} closing: checking durability of ${session.worktree.path} before reap`
|
|
26465
26589
|
);
|
|
26466
26590
|
notify2();
|
|
26467
|
-
void
|
|
26591
|
+
void resolveCloseDurability(
|
|
26468
26592
|
session,
|
|
26469
26593
|
() => finishStatusChange(session, "done", exitCode, deps2),
|
|
26470
26594
|
notify2
|
|
@@ -26477,7 +26601,7 @@ function finishStatusChange(session, status3, exitCode, deps2) {
|
|
|
26477
26601
|
daemonLog(`session ${session.id} status: ${session.status} -> ${status3}`);
|
|
26478
26602
|
void flushPhaseActiveMs(session);
|
|
26479
26603
|
setStatus2(session, status3);
|
|
26480
|
-
if (status3 !== "
|
|
26604
|
+
if (status3 !== "stopped") session.undurable = void 0;
|
|
26481
26605
|
const autoRun = shouldAutoRun(session);
|
|
26482
26606
|
if (autoRun.run) {
|
|
26483
26607
|
deps2.reuseForRun(session, autoRun.itemId);
|
|
@@ -26509,10 +26633,10 @@ function emitSessionOutput(session, clients, data) {
|
|
|
26509
26633
|
}
|
|
26510
26634
|
|
|
26511
26635
|
// src/commands/sessions/daemon/exitReason.ts
|
|
26512
|
-
import { existsSync as
|
|
26636
|
+
import { existsSync as existsSync61 } from "fs";
|
|
26513
26637
|
function exitReason(session, exitCode) {
|
|
26514
26638
|
const base = `process exited with code ${exitCode}`;
|
|
26515
|
-
if (session.cwd && !
|
|
26639
|
+
if (session.cwd && !existsSync61(session.cwd))
|
|
26516
26640
|
return `${base}: working directory ${session.cwd} no longer exists`;
|
|
26517
26641
|
return base;
|
|
26518
26642
|
}
|
|
@@ -26530,7 +26654,7 @@ function handleFailedResume(session, exitCode, onStatusChange) {
|
|
|
26530
26654
|
}
|
|
26531
26655
|
|
|
26532
26656
|
// src/commands/sessions/daemon/watchActivity.ts
|
|
26533
|
-
import { existsSync as
|
|
26657
|
+
import { existsSync as existsSync62, mkdirSync as mkdirSync25, watch as watch2 } from "fs";
|
|
26534
26658
|
import { dirname as dirname29 } from "path";
|
|
26535
26659
|
|
|
26536
26660
|
// src/commands/sessions/daemon/applyReviewPause.ts
|
|
@@ -26611,7 +26735,7 @@ function watchActivity(session, notify2, onClaudeSessionId) {
|
|
|
26611
26735
|
if (timer) clearTimeout(timer);
|
|
26612
26736
|
timer = setTimeout(read, DEBOUNCE_MS2);
|
|
26613
26737
|
});
|
|
26614
|
-
if (
|
|
26738
|
+
if (existsSync62(path71)) read();
|
|
26615
26739
|
}
|
|
26616
26740
|
function refreshActivity(session) {
|
|
26617
26741
|
if (session.commandType !== "assist" || !session.cwd) return;
|
|
@@ -27248,8 +27372,8 @@ function restoreAll(spawn12, sessions) {
|
|
|
27248
27372
|
// src/commands/sessions/daemon/worktree/rearmStoppedSessions.ts
|
|
27249
27373
|
function rearmStoppedSessions(sessions, notify2) {
|
|
27250
27374
|
for (const s of sessions.values()) {
|
|
27251
|
-
if (s.status !== "stopped" || !s
|
|
27252
|
-
void
|
|
27375
|
+
if (s.status !== "stopped" || !treeUnderClose(s)) continue;
|
|
27376
|
+
void resolveCloseDurability(
|
|
27253
27377
|
s,
|
|
27254
27378
|
() => {
|
|
27255
27379
|
if (dismissSession(sessions, s.id)) notify2();
|
|
@@ -27260,23 +27384,9 @@ function rearmStoppedSessions(sessions, notify2) {
|
|
|
27260
27384
|
}
|
|
27261
27385
|
|
|
27262
27386
|
// src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
|
|
27263
|
-
import { existsSync as
|
|
27387
|
+
import { existsSync as existsSync66 } from "fs";
|
|
27264
27388
|
import { basename as basename14 } from "path";
|
|
27265
27389
|
|
|
27266
|
-
// src/commands/sessions/daemon/worktree/worktreeConfigFor.ts
|
|
27267
|
-
var DISABLED = {
|
|
27268
|
-
enabled: false,
|
|
27269
|
-
install: true,
|
|
27270
|
-
copy: [".env", "settings.local.json", ".claude/settings.local.json"]
|
|
27271
|
-
};
|
|
27272
|
-
function worktreeConfigFor(cwd) {
|
|
27273
|
-
try {
|
|
27274
|
-
return loadConfigFrom(cwd).worktree ?? DISABLED;
|
|
27275
|
-
} catch {
|
|
27276
|
-
return DISABLED;
|
|
27277
|
-
}
|
|
27278
|
-
}
|
|
27279
|
-
|
|
27280
27390
|
// src/commands/sessions/daemon/worktree/detectExistingWorktree.ts
|
|
27281
27391
|
function detectExistingWorktree(cwd) {
|
|
27282
27392
|
const repoRoot = findRepoRoot(cwd) ?? cwd;
|
|
@@ -27288,17 +27398,17 @@ function detectExistingWorktree(cwd) {
|
|
|
27288
27398
|
|
|
27289
27399
|
// src/commands/sessions/daemon/worktree/seedWorktree.ts
|
|
27290
27400
|
import { execFile as execFile8 } from "child_process";
|
|
27291
|
-
import { copyFileSync, existsSync as
|
|
27401
|
+
import { copyFileSync, existsSync as existsSync65, mkdirSync as mkdirSync26 } from "fs";
|
|
27292
27402
|
import { dirname as dirname30, join as join71 } from "path";
|
|
27293
27403
|
|
|
27294
27404
|
// src/commands/sessions/daemon/worktree/resolveInstallCommand.ts
|
|
27295
|
-
import { existsSync as
|
|
27405
|
+
import { existsSync as existsSync64 } from "fs";
|
|
27296
27406
|
import { join as join70 } from "path";
|
|
27297
27407
|
function detectInstallCommand(repoRoot) {
|
|
27298
|
-
if (!
|
|
27299
|
-
if (
|
|
27300
|
-
if (
|
|
27301
|
-
if (
|
|
27408
|
+
if (!existsSync64(join70(repoRoot, "package.json"))) return null;
|
|
27409
|
+
if (existsSync64(join70(repoRoot, "pnpm-lock.yaml"))) return "pnpm install";
|
|
27410
|
+
if (existsSync64(join70(repoRoot, "yarn.lock"))) return "yarn install";
|
|
27411
|
+
if (existsSync64(join70(repoRoot, "bun.lockb"))) return "bun install";
|
|
27302
27412
|
return "npm install";
|
|
27303
27413
|
}
|
|
27304
27414
|
function resolveInstallCommand(repoRoot, install) {
|
|
@@ -27317,7 +27427,7 @@ function seedWorktree(worktreePath, clone, onSeeded = () => {
|
|
|
27317
27427
|
function copyConfigFiles(worktreePath, clone, copy) {
|
|
27318
27428
|
for (const rel of copy) {
|
|
27319
27429
|
const src = join71(clone, rel);
|
|
27320
|
-
if (!
|
|
27430
|
+
if (!existsSync65(src)) continue;
|
|
27321
27431
|
const dest = join71(worktreePath, rel);
|
|
27322
27432
|
try {
|
|
27323
27433
|
mkdirSync26(dirname30(dest), { recursive: true });
|
|
@@ -27407,7 +27517,7 @@ async function pruneWorktrees(liveCwds) {
|
|
|
27407
27517
|
const orphanBranches = [];
|
|
27408
27518
|
for (const { path: path71, clone } of readWorktreeRegistry()) {
|
|
27409
27519
|
if (accounted.has(path71)) continue;
|
|
27410
|
-
if (!
|
|
27520
|
+
if (!existsSync66(path71)) {
|
|
27411
27521
|
clonesToPrune.add(clone);
|
|
27412
27522
|
orphanBranches.push({ clone, branch: basename14(path71) });
|
|
27413
27523
|
continue;
|
|
@@ -27416,7 +27526,7 @@ async function pruneWorktrees(liveCwds) {
|
|
|
27416
27526
|
await reapWorktree(path71);
|
|
27417
27527
|
}
|
|
27418
27528
|
for (const clone of clonesToPrune) {
|
|
27419
|
-
if (!
|
|
27529
|
+
if (!existsSync66(clone)) continue;
|
|
27420
27530
|
try {
|
|
27421
27531
|
await git3(clone, ["worktree", "prune"]);
|
|
27422
27532
|
daemonLog(`worktree bookkeeping pruned for clone ${clone}`);
|
|
@@ -27424,7 +27534,7 @@ async function pruneWorktrees(liveCwds) {
|
|
|
27424
27534
|
}
|
|
27425
27535
|
}
|
|
27426
27536
|
for (const { clone, branch: branch2 } of orphanBranches)
|
|
27427
|
-
if (
|
|
27537
|
+
if (existsSync66(clone)) await pruneStaleWorktreeBranch(clone, branch2);
|
|
27428
27538
|
}
|
|
27429
27539
|
async function pruneStaleWorktreeBranch(clone, branch2) {
|
|
27430
27540
|
if (!listLocalBranches(clone).includes(branch2)) return;
|
|
@@ -27589,7 +27699,7 @@ function treeSpawnContext(sessions, spawnWith, notify2, clients, onStatusChange)
|
|
|
27589
27699
|
}
|
|
27590
27700
|
|
|
27591
27701
|
// src/commands/sessions/daemon/watchTranscript.ts
|
|
27592
|
-
import { existsSync as
|
|
27702
|
+
import { existsSync as existsSync67, watch as watch3 } from "fs";
|
|
27593
27703
|
|
|
27594
27704
|
// src/commands/sessions/daemon/reconcileTranscriptStatus.ts
|
|
27595
27705
|
async function resolveTranscriptPath(session) {
|
|
@@ -27624,7 +27734,7 @@ function watchTranscript(session, onStatusChange) {
|
|
|
27624
27734
|
if (!session.cwd || !session.claudeSessionId) return;
|
|
27625
27735
|
if (session.watchedTranscriptId === session.claudeSessionId) return;
|
|
27626
27736
|
const dir = projectDirForCwd(session.cwd);
|
|
27627
|
-
if (!
|
|
27737
|
+
if (!existsSync67(dir)) return;
|
|
27628
27738
|
if (session.transcriptWatcher) {
|
|
27629
27739
|
session.transcriptWatcher.close();
|
|
27630
27740
|
session.transcriptWatcher = void 0;
|
|
@@ -27775,13 +27885,13 @@ async function defaultConnect() {
|
|
|
27775
27885
|
}
|
|
27776
27886
|
|
|
27777
27887
|
// src/commands/sessions/daemon/hasPersistedWindowsSessions.ts
|
|
27778
|
-
import { existsSync as
|
|
27888
|
+
import { existsSync as existsSync68, readFileSync as readFileSync49 } from "fs";
|
|
27779
27889
|
import { posix } from "path";
|
|
27780
27890
|
function hasPersistedWindowsSessions() {
|
|
27781
27891
|
const sessionsFile = windowsSessionsFileFromWsl();
|
|
27782
27892
|
if (!sessionsFile) return false;
|
|
27783
27893
|
try {
|
|
27784
|
-
if (!
|
|
27894
|
+
if (!existsSync68(sessionsFile)) return false;
|
|
27785
27895
|
const data = JSON.parse(readFileSync49(sessionsFile, "utf8"));
|
|
27786
27896
|
return Array.isArray(data) && data.length > 0;
|
|
27787
27897
|
} catch (error) {
|
|
@@ -28369,7 +28479,7 @@ function resumeSession(id, sessionId, cwd, name) {
|
|
|
28369
28479
|
}
|
|
28370
28480
|
|
|
28371
28481
|
// src/commands/sessions/daemon/worktree/createWorktree.ts
|
|
28372
|
-
import { existsSync as
|
|
28482
|
+
import { existsSync as existsSync69 } from "fs";
|
|
28373
28483
|
import { basename as basename16, dirname as dirname31 } from "path";
|
|
28374
28484
|
|
|
28375
28485
|
// src/commands/sessions/daemon/worktree/planAllocation.ts
|
|
@@ -28394,7 +28504,7 @@ function createWorktree(clone, root, boundTreeRoots2) {
|
|
|
28394
28504
|
const path71 = nextWorktreePath(
|
|
28395
28505
|
clone,
|
|
28396
28506
|
base,
|
|
28397
|
-
(candidate) => registered.has(candidate) ||
|
|
28507
|
+
(candidate) => registered.has(candidate) || existsSync69(candidate) || boundTreeRoots2.has(candidate) || branches.has(basename16(candidate))
|
|
28398
28508
|
);
|
|
28399
28509
|
const trunk = cloneTrunkBranch(clone);
|
|
28400
28510
|
gitSync(clone, ["config", "push.default", "upstream"]);
|
|
@@ -29121,8 +29231,8 @@ function registerDaemon(program2) {
|
|
|
29121
29231
|
"Restart the sessions daemon, resuming previously running claude sessions"
|
|
29122
29232
|
).action(restartDaemon);
|
|
29123
29233
|
cmd.command("drain").description(
|
|
29124
|
-
"Remove all sessions from the local daemon for a clean slate (does not affect the Windows daemon)"
|
|
29125
|
-
).action(drainDaemon);
|
|
29234
|
+
"Remove all sessions from the local daemon for a clean slate (does not affect the Windows daemon); prompts before closing live sessions, and a session holding unpushed work is stopped, not removed"
|
|
29235
|
+
).option("--yes", "Close live sessions without confirming").action((options2) => drainDaemon(options2));
|
|
29126
29236
|
}
|
|
29127
29237
|
|
|
29128
29238
|
// src/commands/sessions/registerSetStatusCommand.ts
|