claude-code-session-manager 0.71.0 → 0.73.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/dist/assets/{AgentLibrary-B1lh6YEM.js → AgentLibrary-BowrgaXK.js} +1 -1
- package/dist/assets/{History-B1jNdKFk.js → History-B8rJXcxk.js} +1 -1
- package/dist/assets/{Hooks-BWHFkF7z.js → Hooks-D-KzXjDk.js} +1 -1
- package/dist/assets/{HostBilko-CYQt0b-t.js → HostBilko-BSJhBE9I.js} +1 -1
- package/dist/assets/{Library-DZVKlOBQ.js → Library-CtqgqUc6.js} +1 -1
- package/dist/assets/{ListDetail-rBF1ZQv8.js → ListDetail-D7k4TXvO.js} +1 -1
- package/dist/assets/{MarkdownEditor-zS0QmHxL.js → MarkdownEditor-DFm0I9B_.js} +1 -1
- package/dist/assets/{McpServers-BA54nZee.js → McpServers-DDK04FMq.js} +1 -1
- package/dist/assets/{Memory-DN97rjG5.js → Memory-B1p59Mas.js} +1 -1
- package/dist/assets/{Panel-DLn9L0Cv.js → Panel-BJpqcDzp.js} +1 -1
- package/dist/assets/{Permissions-BnDRq-EM.js → Permissions-CFC3RusZ.js} +1 -1
- package/dist/assets/{Plugins-aPC4a4Bi.js → Plugins-TeCQMr6X.js} +2 -2
- package/dist/assets/{ProvenanceBadge-Bc1gskID.js → ProvenanceBadge-BfIzSRrY.js} +1 -1
- package/dist/assets/{SaveBar-CjvWkRaQ.js → SaveBar-7SmNcZB-.js} +1 -1
- package/dist/assets/{Scheduler-86oyi_P2.js → Scheduler-B9ovEniK.js} +1 -1
- package/dist/assets/{ScopeSwitcher-DJvnhSk3.js → ScopeSwitcher-KmemfTLa.js} +1 -1
- package/dist/assets/Settings-CxZijd66.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CZjMRk3z.js → SkillReferenceGraph-BhACzgW7.js} +1 -1
- package/dist/assets/{Skills-EueUDa9X.js → Skills-CFZU6ltE.js} +1 -1
- package/dist/assets/{SystemPrompt-BQQudmyk.js → SystemPrompt-3tHpyoge.js} +1 -1
- package/dist/assets/{TagLibrary-BEAqaJlp.js → TagLibrary-BoJHe-aY.js} +1 -1
- package/dist/assets/{TiptapBody-DKmVDGB5.js → TiptapBody-DzlxVXaw.js} +1 -1
- package/dist/assets/{Toggle-B7Qb5e2O.js → Toggle-DKmoif0X.js} +1 -1
- package/dist/assets/{index-l-hKv9ou.js → index-Dtv0whpT.js} +671 -671
- package/dist/assets/{index-CNmekpRK.css → index-N_v_4fqC.css} +1 -1
- package/dist/assets/{settingsSchema-DEW4x9LG.js → settingsSchema-CW-0QfXi.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/lib/activeSessions.cjs +10 -1
- package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +125 -0
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +156 -0
- package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +65 -0
- package/src/main/__tests__/stop-signal-anchor.test.cjs +75 -0
- package/src/main/chatRunner.cjs +11 -1
- package/src/main/index.cjs +12 -6
- package/src/main/ipcSchemas.cjs +35 -0
- package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +193 -0
- package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +136 -0
- package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +130 -0
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +115 -0
- package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +79 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +400 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +126 -0
- package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +82 -0
- package/src/main/lib/epicSpawnCwd.cjs +95 -0
- package/src/main/lib/epicWorktreeBoot.cjs +49 -0
- package/src/main/lib/epicWorktreeMerge.cjs +63 -0
- package/src/main/lib/epicWorktreeMint.cjs +54 -0
- package/src/main/lib/epicWorktreeProjectConfig.cjs +102 -0
- package/src/main/lib/gitWorktree.cjs +446 -0
- package/src/main/lib/jobWorktree.cjs +36 -293
- package/src/main/lib/promptSessionSchema.cjs +15 -0
- package/src/main/lib/queueStore.cjs +18 -0
- package/src/main/lib/terminalRunOutcome.cjs +14 -2
- package/src/main/pty.cjs +11 -1
- package/src/main/queueOps.cjs +40 -12
- package/src/main/scheduler.cjs +14 -0
- package/src/preload/api.d.ts +59 -0
- package/src/preload/index.cjs +22 -0
- package/dist/assets/Settings-B7_1p_xA.js +0 -3
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* epicSpawnCwd.cjs — resolves the cwd a child process spawned FOR an Epic's
|
|
5
|
+
* session should actually launch in: the Epic's isolated `git worktree`
|
|
6
|
+
* checkout (`worktree.dir`) when one was created for it, else the Epic's
|
|
7
|
+
* real project cwd unchanged (PRD 1033, wiring the primitives PRD 1032
|
|
8
|
+
* built).
|
|
9
|
+
*
|
|
10
|
+
* Mirrors agentModelResolve.cjs's `findAgentTypeByClaudeSessionId` shape and
|
|
11
|
+
* reuses the same join: an Epic-backed pty.cjs tab / chatRunner.cjs run is
|
|
12
|
+
* keyed by `claudeSessionId` (Tab ID = claudeSessionId, CLAUDE.md), so the
|
|
13
|
+
* matching PromptSession record — and its `worktree` field, when present —
|
|
14
|
+
* is found by scanning `cwd`'s own active-index.json for that id. Plain sync
|
|
15
|
+
* fs (via epicMint.cjs's readActiveIndex), no Electron deps, so callers can
|
|
16
|
+
* use it from a synchronous spawn path without restructuring into async.
|
|
17
|
+
*
|
|
18
|
+
* ---------- the ops-root hazard (read before touching cwd plumbing) --------
|
|
19
|
+
*
|
|
20
|
+
* This resolves ONLY the value a caller hands to a child process's own `cwd`
|
|
21
|
+
* spawn option. `cwd` itself — used for every ops-root read/write
|
|
22
|
+
* (active-index.json, prompt-sessions, scheduler state, opsErrorLog, model
|
|
23
|
+
* resolution) — must keep flowing unchanged everywhere else; see
|
|
24
|
+
* gitWorktree.cjs's own header comment for the full incident history this
|
|
25
|
+
* invariant guards against. Never substitute this function's return value
|
|
26
|
+
* for `cwd` outside a literal spawn-cwd argument.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const { readActiveIndex } = require('./epicMint.cjs');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* True when `dir` is a directory that exists RIGHT NOW.
|
|
34
|
+
*
|
|
35
|
+
* `worktree.dir` is an absolute path under os.tmpdir() that gets PERSISTED
|
|
36
|
+
* onto the Epic record in active-index.json — a file this project tracks in
|
|
37
|
+
* git. It therefore outlives the checkout it names, in three ordinary ways:
|
|
38
|
+
*
|
|
39
|
+
* 1. Reboot / tmp sweep. Most Linux distros and macOS clear /tmp on boot
|
|
40
|
+
* (this host: `D /tmp 1777 root root 30d` in tmpfiles.d — `D` clears at
|
|
41
|
+
* boot AND ages entries out at 30d). Epics are deliberately long-lived
|
|
42
|
+
* (that is exactly why the epic kind has no short-lived cap and why boot
|
|
43
|
+
* reconciliation spares still-active Epics), so an active Epic routinely
|
|
44
|
+
* outlives its own checkout.
|
|
45
|
+
* 2. Another machine. active-index.json is committed, so a clone carries a
|
|
46
|
+
* worktree.dir naming a path built from the ORIGINAL machine's tmpdir and
|
|
47
|
+
* a sha1 of the ORIGINAL machine's project cwd. It cannot exist here.
|
|
48
|
+
* 3. Manual cleanup — `git worktree prune`, an rm -rf, a disk-space sweep.
|
|
49
|
+
*
|
|
50
|
+
* Without this check the spawn path handed a vanished directory straight to
|
|
51
|
+
* node-pty / child_process as its cwd, which fails the spawn outright: the
|
|
52
|
+
* Epic's Terminal and Chat both break with an opaque ENOENT instead of simply
|
|
53
|
+
* falling back to the project's shared tree the way every other failure mode
|
|
54
|
+
* in this chain does.
|
|
55
|
+
*/
|
|
56
|
+
function isUsableDir(dir, statImpl) {
|
|
57
|
+
try {
|
|
58
|
+
return statImpl(dir).isDirectory();
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {{ cwd: string, claudeSessionId: string, deps?: object }} opts
|
|
66
|
+
* @returns {string} `worktree.dir` when the matching Epic has one AND it still
|
|
67
|
+
* exists on disk, else `cwd` unchanged.
|
|
68
|
+
*/
|
|
69
|
+
function resolveEpicSpawnCwd({ cwd, claudeSessionId, deps = {} } = {}) {
|
|
70
|
+
if (!cwd || !claudeSessionId) return cwd;
|
|
71
|
+
try {
|
|
72
|
+
const load = deps.readActiveIndex || readActiveIndex;
|
|
73
|
+
const statSync = deps.statSync || fs.statSync;
|
|
74
|
+
const { sessions } = load(cwd);
|
|
75
|
+
for (const session of Object.values(sessions)) {
|
|
76
|
+
if (session && session.claudeSessionId === claudeSessionId) {
|
|
77
|
+
const dir = session.worktree?.dir;
|
|
78
|
+
if (!dir) return cwd;
|
|
79
|
+
if (!isUsableDir(dir, statSync)) {
|
|
80
|
+
// Falling back is strictly better than failing the spawn: the
|
|
81
|
+
// session still runs, just un-isolated, which is the same degraded
|
|
82
|
+
// mode a project that never had a worktree runs in.
|
|
83
|
+
console.log(`[epicSpawnCwd] worktree dir gone (${dir}) — falling back to ${cwd}`);
|
|
84
|
+
return cwd;
|
|
85
|
+
}
|
|
86
|
+
return dir;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
return cwd;
|
|
91
|
+
}
|
|
92
|
+
return cwd;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { resolveEpicSpawnCwd };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* epicWorktreeBoot.cjs — boot reconciliation for Epic git worktrees (PRD
|
|
5
|
+
* 1033, wiring gitWorktree.cjs's `reconcileWorktreesOnBoot` isLive predicate
|
|
6
|
+
* that PRD 1032 built but left unwired).
|
|
7
|
+
*
|
|
8
|
+
* A worktree that survives an app crash/host reboot must not leak disk or a
|
|
9
|
+
* dangling branch forever — but unlike a scheduler job worktree (always safe
|
|
10
|
+
* to reap at boot; a job worktree found at boot is by definition from a run
|
|
11
|
+
* that didn't finish), an Epic worktree backs a session a human may still be
|
|
12
|
+
* mid-way through. Reaping it out from under a still-`active` Epic would
|
|
13
|
+
* silently drop that Epic's isolation (and, worse, any uncommitted work sat
|
|
14
|
+
* in the checkout) the moment the app restarts.
|
|
15
|
+
*
|
|
16
|
+
* Per project cwd, reads that project's own active-index.json (the same file
|
|
17
|
+
* gitWorktree.cjs's own header comment calls the "ops-root" — resolved here
|
|
18
|
+
* via epicMint.cjs's readActiveIndex, never a worktree path) and only reaps
|
|
19
|
+
* a worktree whose owning Epic is NOT `active` in it. At boot there is by
|
|
20
|
+
* definition no live attached PTY/chat process yet (the app process is
|
|
21
|
+
* fresh), so the AC's "no live attached process" half of the gate holds
|
|
22
|
+
* automatically — this predicate covers the other half.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const gitWorktree = require('./gitWorktree.cjs');
|
|
26
|
+
const { readActiveIndex } = require('./epicMint.cjs');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string[]} cwds - known project cwds to sweep.
|
|
30
|
+
* @param {{ readActiveIndex?: Function, reconcileWorktreesOnBoot?: Function }} [deps]
|
|
31
|
+
* @returns {Promise<void>}
|
|
32
|
+
*/
|
|
33
|
+
async function reconcileEpicWorktreesOnBoot(cwds, deps = {}) {
|
|
34
|
+
const load = deps.readActiveIndex || readActiveIndex;
|
|
35
|
+
const reconcile = deps.reconcileWorktreesOnBoot || gitWorktree.reconcileWorktreesOnBoot;
|
|
36
|
+
const list = Array.isArray(cwds) ? cwds.filter(Boolean) : [];
|
|
37
|
+
for (const cwd of list) {
|
|
38
|
+
let sessions = {};
|
|
39
|
+
try {
|
|
40
|
+
sessions = load(cwd).sessions || {};
|
|
41
|
+
} catch {
|
|
42
|
+
sessions = {}; // no active-index.json yet, or unreadable — nothing is "live"
|
|
43
|
+
}
|
|
44
|
+
const isLive = (epicId) => !!sessions[epicId] && sessions[epicId].status === 'active';
|
|
45
|
+
await reconcile([cwd], { kind: 'epic', isLive });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { reconcileEpicWorktreesOnBoot };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* epicWorktreeMerge.cjs — IPC entry point that folds an Epic's isolated git
|
|
5
|
+
* worktree branch (gitWorktree.cjs's `integrateEpicBranch`, kind: 'epic')
|
|
6
|
+
* back into its owning project's main tree (PRD 1034, third link of the
|
|
7
|
+
* epic-worktree-isolation chain started by PRD 1032/1033).
|
|
8
|
+
*
|
|
9
|
+
* This is the ONLY point where that per-Epic isolation resolves back into
|
|
10
|
+
* the shared tree — never mid-session (see gitWorktree.cjs's own header
|
|
11
|
+
* comment for the ff-only -> merge-commit -> abort-and-flag algorithm this
|
|
12
|
+
* reuses verbatim). Callers reach it two ways: an explicit "merge to main"
|
|
13
|
+
* action (renderer state's `mergeEpicToMain`), and `markCompleted`'s
|
|
14
|
+
* merge-before-archive checkpoint — both funnel through this one handler so
|
|
15
|
+
* the integration strategy is never duplicated.
|
|
16
|
+
*
|
|
17
|
+
* On success the worktree checkout is torn down (cleanupEpicWorktree) since
|
|
18
|
+
* its content now safely lives on the main tree. On a real conflict, the
|
|
19
|
+
* branch and worktree directory are left intact — never silently discarded
|
|
20
|
+
* — so a human can still open a Terminal into `worktree.dir` (PRD 1033's
|
|
21
|
+
* existing epicSpawnCwd.cjs wiring — it keys off `worktree.dir` alone, not
|
|
22
|
+
* `worktree.status`, so a conflicted Epic's Terminal spawn keeps landing
|
|
23
|
+
* there with no new code path needed) and resolve it manually before
|
|
24
|
+
* re-running this same action.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { integrateEpicBranch, cleanupEpicWorktree } = require('./gitWorktree.cjs');
|
|
28
|
+
const { validatePath } = require('../config.cjs');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* mergeEpicToMainViaIpc(cwd, epicId, branch, dir) → Promise<MergeOutcome>
|
|
32
|
+
*
|
|
33
|
+
* `MergeOutcome` is `{ ok: true, status: 'merged', integrated: boolean }` on
|
|
34
|
+
* a clean merge (or a legitimate no-new-commits no-op — either way nothing
|
|
35
|
+
* is left to resolve, so the worktree is cleaned up), or
|
|
36
|
+
* `{ ok: false, status: 'needs_merge_resolution', reason: string }` on a
|
|
37
|
+
* real conflict — never throws (integrateEpicBranch/cleanupEpicWorktree
|
|
38
|
+
* never do either).
|
|
39
|
+
*/
|
|
40
|
+
async function mergeEpicToMainViaIpc(cwd, epicId, branch, dir) {
|
|
41
|
+
validatePath(cwd);
|
|
42
|
+
const outcome = await integrateEpicBranch({ cwd, branch, epicId });
|
|
43
|
+
if (!outcome.ok) {
|
|
44
|
+
console.log(`[epicWorktreeMerge] ${epicId}: needs manual resolution (${outcome.reason})`);
|
|
45
|
+
return { ok: false, status: 'needs_merge_resolution', reason: outcome.reason };
|
|
46
|
+
}
|
|
47
|
+
// Clean ff-only/merge-commit landing, or a legitimate no-new-commits
|
|
48
|
+
// no-op — either way the branch's content is safely reflected (or was
|
|
49
|
+
// never ahead), so the checkout has nothing left to guard.
|
|
50
|
+
await cleanupEpicWorktree({ cwd, dir, branch, keepBranch: false });
|
|
51
|
+
return { ok: true, status: 'merged', integrated: outcome.integrated };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function registerEpicWorktreeMergeHandlers() {
|
|
55
|
+
const { ipcMain } = require('electron');
|
|
56
|
+
const { schemas: s, validated: v } = require('../ipcSchemas.cjs');
|
|
57
|
+
ipcMain.handle(
|
|
58
|
+
'promptSessions:merge-to-main',
|
|
59
|
+
v(s.promptSessionsMergeToMain, ({ cwd, epicId, branch, dir }) => mergeEpicToMainViaIpc(cwd, epicId, branch, dir)),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { mergeEpicToMainViaIpc, registerEpicWorktreeMergeHandlers };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* epicWorktreeMint.cjs — IPC entry point that creates an Epic's isolated git
|
|
5
|
+
* worktree (gitWorktree.cjs's createEpicWorktree, kind: 'epic') at the
|
|
6
|
+
* `proposed -> active` transition (PRD 1033, second link of the
|
|
7
|
+
* epic-worktree-isolation chain started by PRD 1032).
|
|
8
|
+
*
|
|
9
|
+
* That transition itself lives in the renderer (state/promptSessions.ts's
|
|
10
|
+
* approveProposed) and is synchronous — every caller relies on getting the
|
|
11
|
+
* approved session back immediately to start its chat send in the same tick
|
|
12
|
+
* (see that function's own comment). Worktree creation is real git/fs work
|
|
13
|
+
* that can only run main-process side, so approveProposed fires this IPC
|
|
14
|
+
* call fire-and-forget and patches `worktree` onto the session once it
|
|
15
|
+
* resolves — the transition itself is never blocked or delayed by it, and a
|
|
16
|
+
* failure (not a repo / dirty base tree / disabled / cap reached) simply
|
|
17
|
+
* leaves `worktree` unset, exactly as if this call had never been made.
|
|
18
|
+
*
|
|
19
|
+
* Mirrors promptSessionsCreateEpic.cjs's shape: `validatePath(cwd)` closes
|
|
20
|
+
* the same "arbitrary renderer-supplied cwd" gap that handler documents,
|
|
21
|
+
* since this is likewise reachable from the renderer over IPC.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { createEpicWorktree } = require('./gitWorktree.cjs');
|
|
25
|
+
const { validatePath } = require('../config.cjs');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* createEpicWorktreeViaIpc(cwd, epicId) → Promise<EpicWorktree | null>
|
|
29
|
+
*
|
|
30
|
+
* Returns the `worktree` field shape (`{ dir, branch, baseCwd, status }`)
|
|
31
|
+
* ready to persist onto the Epic record on success, or `null` on any
|
|
32
|
+
* failure — createEpicWorktree/createWorktree never throw, so this never
|
|
33
|
+
* does either.
|
|
34
|
+
*/
|
|
35
|
+
async function createEpicWorktreeViaIpc(cwd, epicId) {
|
|
36
|
+
validatePath(cwd);
|
|
37
|
+
const result = await createEpicWorktree({ cwd, epicId });
|
|
38
|
+
if (!result.ok) {
|
|
39
|
+
console.log(`[epicWorktreeMint] ${epicId}: not isolated (${result.reason})`);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return { dir: result.dir, branch: result.branch, baseCwd: result.baseCwd, status: 'active' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function registerEpicWorktreeMintHandlers() {
|
|
46
|
+
const { ipcMain } = require('electron');
|
|
47
|
+
const { schemas: s, validated: v } = require('../ipcSchemas.cjs');
|
|
48
|
+
ipcMain.handle(
|
|
49
|
+
'promptSessions:create-worktree',
|
|
50
|
+
v(s.promptSessionsCreateWorktree, ({ cwd, epicId }) => createEpicWorktreeViaIpc(cwd, epicId)),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { createEpicWorktreeViaIpc, registerEpicWorktreeMintHandlers };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* epicWorktreeProjectConfig.cjs — per-project UI toggle for Epic worktree
|
|
5
|
+
* isolation (PRD 1035, final link of the epic-worktree-isolation chain).
|
|
6
|
+
*
|
|
7
|
+
* `SM_EPIC_WORKTREE_DISABLE` (gitWorktree.cjs) is an env var, checked once at
|
|
8
|
+
* process start — there was previously no way to turn the feature off for
|
|
9
|
+
* one project from the app itself. This module is that per-project knob:
|
|
10
|
+
* a single JSON file mapping project cwd -> disabled, read by
|
|
11
|
+
* gitWorktree.cjs's `isWorktreeDisabled('epic', cwd)` on every worktree
|
|
12
|
+
* creation attempt, and read/written by Settings.tsx's per-project toggle
|
|
13
|
+
* over the two IPC handlers registered below.
|
|
14
|
+
*
|
|
15
|
+
* Lives under `~/.claude/session-manager/` — machine-runtime bookkeeping,
|
|
16
|
+
* the same tier as scheduler-machine.json (see queueStore.cjs's header
|
|
17
|
+
* comment) — NOT under any project's `session-manager-operations/`, so the
|
|
18
|
+
* single-writer OWNERS law (opsOwnership.cjs) doesn't apply here; this file
|
|
19
|
+
* only ever has one writer (this module) by construction.
|
|
20
|
+
*
|
|
21
|
+
* Plain Node (no Electron deps in the read/write helpers) so gitWorktree.cjs
|
|
22
|
+
* — itself Electron-free — can require this lazily without pulling Electron
|
|
23
|
+
* into contexts (tests, watchdog scripts) that don't have it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const os = require('node:os');
|
|
29
|
+
|
|
30
|
+
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'epic-worktree-project-config.json');
|
|
31
|
+
|
|
32
|
+
// Resolved per-call (not a frozen const) so tests can point this at a
|
|
33
|
+
// throwaway tmpdir file via SM_EPIC_WORKTREE_PROJECT_CONFIG_PATH instead of
|
|
34
|
+
// mutating the real machine-level file at DEFAULT_CONFIG_PATH.
|
|
35
|
+
function configPath() {
|
|
36
|
+
return process.env.SM_EPIC_WORKTREE_PROJECT_CONFIG_PATH || DEFAULT_CONFIG_PATH;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readConfig() {
|
|
40
|
+
try {
|
|
41
|
+
const raw = fs.readFileSync(configPath(), 'utf8');
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
44
|
+
} catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeConfig(config) {
|
|
50
|
+
const file = configPath();
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
53
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2));
|
|
54
|
+
fs.renameSync(tmp, file);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** True when the project at `cwd` has explicitly turned Epic worktree isolation off. */
|
|
58
|
+
function isEpicWorktreeDisabledForProject(cwd) {
|
|
59
|
+
if (!cwd || typeof cwd !== 'string') return false;
|
|
60
|
+
return readConfig()[cwd] === true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Persists (or clears, when `disabled` is false) the per-project toggle. */
|
|
64
|
+
function setEpicWorktreeDisabledForProject(cwd, disabled) {
|
|
65
|
+
if (!cwd || typeof cwd !== 'string') throw new Error('setEpicWorktreeDisabledForProject: cwd is required');
|
|
66
|
+
const config = readConfig();
|
|
67
|
+
if (disabled) {
|
|
68
|
+
config[cwd] = true;
|
|
69
|
+
} else {
|
|
70
|
+
delete config[cwd];
|
|
71
|
+
}
|
|
72
|
+
writeConfig(config);
|
|
73
|
+
return disabled;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function registerEpicWorktreeProjectConfigHandlers() {
|
|
77
|
+
const { ipcMain } = require('electron');
|
|
78
|
+
const { schemas: s, validated: v } = require('../ipcSchemas.cjs');
|
|
79
|
+
const { validatePath } = require('../config.cjs');
|
|
80
|
+
ipcMain.handle(
|
|
81
|
+
'promptSessions:get-worktree-disabled',
|
|
82
|
+
v(s.promptSessionsGetWorktreeDisabled, ({ cwd }) => {
|
|
83
|
+
validatePath(cwd);
|
|
84
|
+
return { disabled: isEpicWorktreeDisabledForProject(cwd) };
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
ipcMain.handle(
|
|
88
|
+
'promptSessions:set-worktree-disabled',
|
|
89
|
+
v(s.promptSessionsSetWorktreeDisabled, ({ cwd, disabled }) => {
|
|
90
|
+
validatePath(cwd);
|
|
91
|
+
return { disabled: setEpicWorktreeDisabledForProject(cwd, disabled) };
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
DEFAULT_CONFIG_PATH,
|
|
98
|
+
configPath,
|
|
99
|
+
isEpicWorktreeDisabledForProject,
|
|
100
|
+
setEpicWorktreeDisabledForProject,
|
|
101
|
+
registerEpicWorktreeProjectConfigHandlers,
|
|
102
|
+
};
|