@staff0rd/assist 0.434.0 → 0.434.1

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/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.434.0",
9
+ version: "0.434.1",
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
- async function drainDaemon() {
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
- savePersistedSessions([]);
25315
- console.log("Sessions daemon is not running; cleared persisted sessions");
25344
+ clearPersistedSessionsOnDrain();
25316
25345
  return;
25317
25346
  }
25318
- const count7 = await requestDrain(socket);
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 requestDrain(socket) {
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 new Promise((resolve20) => {
25326
- const timer = setTimeout(() => resolve20(0), DRAIN_TIMEOUT_MS);
25327
- const lines = createInterface6({ input: socket });
25328
- lines.on("error", () => {
25329
- });
25330
- lines.on("line", (line) => {
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 existsSync58 } from "fs";
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 gitOrNull(cwd, [
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 gitOrNull(cwd, [
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 ?? "0") > 0;
25938
+ return !ahead.ok || Number(ahead.out) > 0;
25841
25939
  }
25842
- const remoteBranches = await gitOrNull(cwd, [
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
- return treeDurability(await collectTreeState(cwd, pushOnCommit(cwd)));
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 (!existsSync58(worktreePath)) {
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
- if (s.status !== "done") s.pty?.kill();
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) void reapWorktree(s.worktree.path);
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/killPtyTree.ts
25941
- function killPtyTree(pty2) {
25942
- if (process.platform === "win32") {
25943
- pty2.kill();
25944
- return;
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
- process.kill(-pty2.pid, "SIGHUP");
26042
+ return loadConfigFrom(cwd).worktree ?? DISABLED;
25948
26043
  } catch {
25949
- try {
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 existsSync59, watch } from "fs";
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 || !existsSync59(common)) return void 0;
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/resolveDoneDurability.ts
25987
- async function resolveDoneDurability(session, finalize, notify2) {
25988
- const worktree = session.worktree;
25989
- if (!worktree) {
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(worktree.path);
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(worktree.path);
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(`session ${session.id} stopped; reap blocked: ${reason}`);
26009
- session.undurable = { reason };
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 && session.worktree)
26013
- session.gitWatcher = watchGitState(session.worktree.path, () => {
26116
+ if (!session.gitWatcher)
26117
+ session.gitWatcher = watchGitState(tree.path, () => {
26014
26118
  if (session.status !== "stopped") return;
26015
- void resolveDoneDurability(session, finalize, notify2);
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
- if (!s?.worktree) {
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 discardTree = async () => {
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"} ${s.worktree.path}`
26144
+ `session ${id} closing: ${discard ? "discarding" : "checking durability of"} ${tree.path}`
26040
26145
  );
26041
26146
  if (s.pty) {
26042
- s.pendingDismiss = discard ? () => void discardTree() : () => void resolveDoneDurability(s, removeCard, notify2);
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 discardTree();
26052
- else void resolveDoneDurability(s, removeCard, notify2);
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 resolveDoneDurability(
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 !== "waiting") session.undurable = void 0;
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 existsSync60 } from "fs";
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 && !existsSync60(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 existsSync61, mkdirSync as mkdirSync25, watch as watch2 } from "fs";
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 (existsSync61(path71)) read();
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.worktree) continue;
27252
- void resolveDoneDurability(
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 existsSync65 } from "fs";
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 existsSync64, mkdirSync as mkdirSync26 } from "fs";
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 existsSync63 } from "fs";
27405
+ import { existsSync as existsSync64 } from "fs";
27296
27406
  import { join as join70 } from "path";
27297
27407
  function detectInstallCommand(repoRoot) {
27298
- if (!existsSync63(join70(repoRoot, "package.json"))) return null;
27299
- if (existsSync63(join70(repoRoot, "pnpm-lock.yaml"))) return "pnpm install";
27300
- if (existsSync63(join70(repoRoot, "yarn.lock"))) return "yarn install";
27301
- if (existsSync63(join70(repoRoot, "bun.lockb"))) return "bun install";
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 (!existsSync64(src)) continue;
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 (!existsSync65(path71)) {
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 (!existsSync65(clone)) continue;
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 (existsSync65(clone)) await pruneStaleWorktreeBranch(clone, branch2);
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 existsSync66, watch as watch3 } from "fs";
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 (!existsSync66(dir)) return;
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 existsSync67, readFileSync as readFileSync49 } from "fs";
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 (!existsSync67(sessionsFile)) return false;
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 existsSync68 } from "fs";
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) || existsSync68(candidate) || boundTreeRoots2.has(candidate) || branches.has(basename16(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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.434.0",
3
+ "version": "0.434.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {