claude-code-session-manager 0.104.1 → 0.105.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 +15 -3
- package/bin/cli.cjs +0 -5
- package/dist/assets/AgentLibrary-BDtr4oI-.js +2 -0
- package/dist/assets/Button-CuXBCQDp.js +1 -0
- package/dist/assets/DataModel-DmvRipT5.js +1 -0
- package/dist/assets/{History-BP6KKda_.js → History-B-v9pwwr.js} +2 -2
- package/dist/assets/Hooks-S_0oHHlr.js +3 -0
- package/dist/assets/{Library-BprXNJCG.js → Library-BcbGJxQ3.js} +5 -5
- package/dist/assets/ListDetail-DtqHqJHt.js +1 -0
- package/dist/assets/{MarkdownEditor-1KlkRAqT.js → MarkdownEditor-B56sPp_l.js} +1 -1
- package/dist/assets/{McpServers-FUDN7EnU.js → McpServers-D5VnmxAn.js} +2 -2
- package/dist/assets/{Memory-DryJweXu.js → Memory-0yGZLyRP.js} +6 -6
- package/dist/assets/Panel-NoAADJwx.js +1 -0
- package/dist/assets/Permissions-D2Dh-ee8.js +3 -0
- package/dist/assets/Plugins-kGoUgS9b.js +2 -0
- package/dist/assets/{ProvenanceBadge-6Hj42klQ.js → ProvenanceBadge-CHwDFTX7.js} +1 -1
- package/dist/assets/{SaveBar-xSfjUPiu.js → SaveBar-BtKNMIYK.js} +1 -1
- package/dist/assets/{Scheduler-Beh3Vj-Z.js → Scheduler-B0_kMMDs.js} +7 -7
- package/dist/assets/{ScopeSwitcher-9R1ANwns.js → ScopeSwitcher-BusohWZ5.js} +1 -1
- package/dist/assets/Settings-DuRJntaE.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CYLTGabR.js → SkillReferenceGraph-BHOdIpsy.js} +1 -1
- package/dist/assets/Skills-DSTLZimI.js +3 -0
- package/dist/assets/SystemPrompt-BG7wFWdb.js +1 -0
- package/dist/assets/TagLibrary-Bx27aE3_.js +1 -0
- package/dist/assets/{TiptapBody-l-BMtrPd.js → TiptapBody-j0O8J7YI.js} +1 -1
- package/dist/assets/{Toggle-Bj2Ia7AE.js → Toggle-DzVZ6rC7.js} +1 -1
- package/dist/assets/ViewTabs-BizjuynT.js +1 -0
- package/dist/assets/claude-settings-schema-COigsajx.js +3 -0
- package/dist/assets/{index-D89EUizB.css → index-BLVzrc4k.css} +1 -1
- package/dist/assets/index-CLIaYljt.js +3073 -0
- package/dist/assets/monaco-editor-BsIlgG7c.js +917 -0
- package/dist/assets/monaco-editor-Cu5D__1H.css +1 -0
- package/dist/assets/settingsSchema-7MRAxaJk.js +1 -0
- package/dist/assets/useAgentsDirWatch-BUwfU2Kq.js +1 -0
- package/dist/assets/useConfigDirWatch-ot0VYwQC.js +1 -0
- package/dist/index.html +4 -4
- package/package.json +12 -14
- package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +12 -0
- package/plugins/session-manager-dev/skills/develop/SKILL.md +4 -2
- package/scripts/scheduler-mcp-server.cjs +7 -17
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +4 -4
- package/src/main/historyAggregator.cjs +7 -1
- package/src/main/index.cjs +34 -1
- package/src/main/lib/activeSessions.cjs +263 -71
- package/src/main/lib/agentPersonaSchema.cjs +8 -1
- package/src/main/lib/appRuntime.cjs +57 -0
- package/src/main/lib/childWithLog.cjs +10 -10
- package/src/main/lib/claudeBin.cjs +40 -16
- package/src/main/lib/cleanEnv.cjs +29 -6
- package/src/main/lib/defaultShell.cjs +36 -0
- package/src/main/lib/definitionOfDone.cjs +6 -2
- package/src/main/lib/delegationReadiness.cjs +13 -5
- package/src/main/lib/gitWorktree.cjs +6 -3
- package/src/main/lib/insideHome.cjs +15 -4
- package/src/main/lib/killTree.cjs +80 -0
- package/src/main/lib/loadGate.cjs +16 -0
- package/src/main/lib/loginShellPath.cjs +82 -0
- package/src/main/lib/machineProfile.cjs +4 -2
- package/src/main/lib/mcpStdioServer.cjs +127 -0
- package/src/main/lib/openExternalApp.cjs +62 -11
- package/src/main/lib/otlpTraceExporter.cjs +147 -0
- package/src/main/lib/prdMigration.cjs +64 -1
- package/src/main/lib/prereqs.cjs +145 -0
- package/src/main/lib/procIdentity.cjs +37 -2
- package/src/main/lib/queueHistory.cjs +97 -26
- package/src/main/lib/rcaReport.cjs +1 -0
- package/src/main/lib/reaperHelpers.cjs +5 -2
- package/src/main/lib/shippedPersonaSeeds.cjs +10 -0
- package/src/main/lib/timeoutShimScript.cjs +14 -2
- package/src/main/lib/updateCheck.cjs +41 -11
- package/src/main/lib/watchdogHelpers.cjs +23 -3
- package/src/main/lib/winSpawn.cjs +67 -0
- package/src/main/otel.cjs +18 -62
- package/src/main/pty.cjs +82 -12
- package/src/main/runVerify.cjs +42 -2
- package/src/main/scheduler.cjs +105 -20
- package/src/main/seedSchedulerMcp.cjs +11 -2
- package/src/main/usage.cjs +4 -1
- package/src/preload/api.d.ts +14 -0
- package/src/preload/index.cjs +2 -0
- package/src/seed/agents/validator.md +2 -2
- package/dist/assets/DataModel-BHcPa2nA.js +0 -1
- package/dist/assets/Hooks-CnJnddP6.js +0 -3
- package/dist/assets/Permissions-Df-LWvdk.js +0 -3
- package/dist/assets/Plugins-CIUOw9B2.js +0 -2
- package/dist/assets/Settings-gkp0WfEm.js +0 -3
- package/dist/assets/Skills-Dw5Am-f-.js +0 -3
- package/dist/assets/SystemPrompt-B3GwTLqh.js +0 -1
- package/dist/assets/TagLibrary-Dj46gOG2.js +0 -1
- package/dist/assets/ViewTabs-BQ_HbQFq.js +0 -1
- package/dist/assets/abap-DLDM7-KI.js +0 -1
- package/dist/assets/apex-DNDY2TF8.js +0 -1
- package/dist/assets/azcli-Y6nb8tq_.js +0 -1
- package/dist/assets/bat-BwHxbl9M.js +0 -1
- package/dist/assets/bicep-CFznDFnq.js +0 -2
- package/dist/assets/cameligo-Bf6VGUru.js +0 -1
- package/dist/assets/clojure-Dnu-v4kV.js +0 -1
- package/dist/assets/coffee-Bd8akH9Z.js +0 -1
- package/dist/assets/cpp-BbWJElDN.js +0 -1
- package/dist/assets/csharp-Co3qMtFm.js +0 -1
- package/dist/assets/csp-D-4FJmMZ.js +0 -1
- package/dist/assets/css-DdJfP1eB.js +0 -3
- package/dist/assets/css.worker-B4z49cGk.js +0 -93
- package/dist/assets/cssMode-DfqZGMQs.js +0 -1
- package/dist/assets/cypher-cTPe9QuQ.js +0 -1
- package/dist/assets/dart-BOtBlQCF.js +0 -1
- package/dist/assets/dockerfile-BG73LgW2.js +0 -1
- package/dist/assets/ecl-BEgZUVRK.js +0 -1
- package/dist/assets/elixir-BkW5O-1t.js +0 -1
- package/dist/assets/flow9-BeJ5waoc.js +0 -1
- package/dist/assets/freemarker2-XTPYh37h.js +0 -3
- package/dist/assets/fsharp-PahG7c26.js +0 -1
- package/dist/assets/go-acbASCJo.js +0 -1
- package/dist/assets/graphql-BxJiqAUM.js +0 -1
- package/dist/assets/handlebars-DKUF5VyH.js +0 -1
- package/dist/assets/hcl-DtV1sZF8.js +0 -1
- package/dist/assets/html-uqoqsIeI.js +0 -1
- package/dist/assets/html.worker-DtiGdgqp.js +0 -470
- package/dist/assets/htmlMode-aMTQs1su.js +0 -1
- package/dist/assets/index-DbR91Z5j.js +0 -3076
- package/dist/assets/ini-Kd9XrMLS.js +0 -1
- package/dist/assets/java-CXBNlu9o.js +0 -1
- package/dist/assets/javascript-BVxRZMds.js +0 -1
- package/dist/assets/jsonMode-D04xP2s5.js +0 -7
- package/dist/assets/julia-cl7-CwDS.js +0 -1
- package/dist/assets/kotlin-s7OhZKlX.js +0 -1
- package/dist/assets/less-9HpZscsL.js +0 -2
- package/dist/assets/lexon-OrD6JF1K.js +0 -1
- package/dist/assets/liquid-BkQHTH2P.js +0 -1
- package/dist/assets/lspLanguageFeatures-By9uLznH.js +0 -4
- package/dist/assets/lua-Cyyb5UIc.js +0 -1
- package/dist/assets/m3-B8OfTtLu.js +0 -1
- package/dist/assets/markdown-BFxVWTOG.js +0 -1
- package/dist/assets/mdx-Du1IlbjV.js +0 -1
- package/dist/assets/mips-CiqrrVzr.js +0 -1
- package/dist/assets/monaco-editor-BTnBOi8r.css +0 -1
- package/dist/assets/monaco-editor-BW5C4Iv1.js +0 -908
- package/dist/assets/msdax-DmeGPVcC.js +0 -1
- package/dist/assets/mysql-C_tMU-Nz.js +0 -1
- package/dist/assets/objective-c-BDtDVThU.js +0 -1
- package/dist/assets/pascal-vHIfCaH5.js +0 -1
- package/dist/assets/pascaligo-DtZ0uQbO.js +0 -1
- package/dist/assets/perl-Ub6l9XKa.js +0 -1
- package/dist/assets/pgsql-BlNEE0v7.js +0 -1
- package/dist/assets/php-BBUBE1dy.js +0 -1
- package/dist/assets/pla-DSh2-awV.js +0 -1
- package/dist/assets/postiats-CocnycG-.js +0 -1
- package/dist/assets/powerquery-tScXyioY.js +0 -1
- package/dist/assets/powershell-COWaemsV.js +0 -1
- package/dist/assets/protobuf-Brw8urJB.js +0 -2
- package/dist/assets/pug-8SOpv6rk.js +0 -1
- package/dist/assets/python-DSlImqXd.js +0 -1
- package/dist/assets/qsharp-Bw9ernYp.js +0 -1
- package/dist/assets/r-j7ic8hl3.js +0 -1
- package/dist/assets/razor-BmUVyvSK.js +0 -1
- package/dist/assets/redis-Bu5POkcn.js +0 -1
- package/dist/assets/redshift-Bs9aos_-.js +0 -1
- package/dist/assets/restructuredtext-CqXO7rUv.js +0 -1
- package/dist/assets/ruby-zBfavPgS.js +0 -1
- package/dist/assets/rust-BzKRNQWT.js +0 -1
- package/dist/assets/sb-BBc9UKZt.js +0 -1
- package/dist/assets/scala-D9hQfWCl.js +0 -1
- package/dist/assets/scheme-BPhDTwHR.js +0 -1
- package/dist/assets/scss-CBJaRo0y.js +0 -3
- package/dist/assets/settingsSchema-cR833AB2.js +0 -1
- package/dist/assets/shell-DiJ1NA_G.js +0 -1
- package/dist/assets/solidity-Db0IVjzk.js +0 -1
- package/dist/assets/sophia-CnS9iZB_.js +0 -1
- package/dist/assets/sparql-CJmd_6j2.js +0 -1
- package/dist/assets/sql-ClhHkBeG.js +0 -1
- package/dist/assets/st-CHwy0fLd.js +0 -1
- package/dist/assets/swift-Bqt4WxQ4.js +0 -3
- package/dist/assets/systemverilog-Bs9z6M-B.js +0 -1
- package/dist/assets/tcl-Dm6ycUr_.js +0 -1
- package/dist/assets/ts.worker-59MjiAqk.js +0 -67731
- package/dist/assets/tsMode-Btj0TTH7.js +0 -11
- package/dist/assets/twig-Csy3S7wG.js +0 -1
- package/dist/assets/typescript-Bzelq9vO.js +0 -1
- package/dist/assets/typespec-Btyra-wh.js +0 -1
- package/dist/assets/vb-Db0cS2oM.js +0 -1
- package/dist/assets/wgsl-BTesnYfV.js +0 -298
- package/dist/assets/xml-Whd9EaSd.js +0 -1
- package/dist/assets/yaml-QYf0-IN8.js +0 -1
- package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +0 -112
- package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +0 -106
- package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +0 -107
- package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
- package/scripts/postinstall.cjs +0 -52
- package/src/preload/__tests__/preload-surface.test.cjs +0 -68
|
@@ -92,7 +92,12 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
92
92
|
|
|
93
93
|
// ── date helpers ──────────────────────────────────────────────────────────────
|
|
94
94
|
|
|
95
|
-
function decodeCwd(encoded) {
|
|
95
|
+
function decodeCwd(encoded, platform = process.platform) {
|
|
96
|
+
// encodeCwd turns `C:\\Users\\Me` into `C--Users-Me`: drive letter, then `--`.
|
|
97
|
+
if (platform === 'win32') {
|
|
98
|
+
const m = /^([A-Za-z])--(.*)$/.exec(encoded);
|
|
99
|
+
if (m) return m[1] + ':\\' + m[2].replace(/-+/g, '\\');
|
|
100
|
+
}
|
|
96
101
|
return '/' + encoded.replace(/-+/g, '/');
|
|
97
102
|
}
|
|
98
103
|
|
|
@@ -944,6 +949,7 @@ module.exports = {
|
|
|
944
949
|
remote,
|
|
945
950
|
MODEL_PRICING,
|
|
946
951
|
finalizeClosedDays,
|
|
952
|
+
decodeCwd,
|
|
947
953
|
refreshIntradayToday,
|
|
948
954
|
computeIntradayBuckets,
|
|
949
955
|
// exported for tests
|
package/src/main/index.cjs
CHANGED
|
@@ -11,6 +11,7 @@ const fsp = require('node:fs/promises');
|
|
|
11
11
|
const os = require('node:os');
|
|
12
12
|
const { schemas, validated } = require('./ipcSchemas.cjs');
|
|
13
13
|
const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
|
|
14
|
+
const { applyLoginShellPath } = require('./lib/loginShellPath.cjs');
|
|
14
15
|
const { terminateLosingInstance } = require('./lib/singleInstanceGuard.cjs');
|
|
15
16
|
const { acquireSchedulerOwnership, releaseSchedulerOwnership } = require('./lib/instanceLock.cjs');
|
|
16
17
|
const { manager: ptyManager, registerPtyHandlers } = require('./pty.cjs');
|
|
@@ -101,7 +102,8 @@ const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('
|
|
|
101
102
|
const { listExchanges } = require('./exchanges.cjs');
|
|
102
103
|
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
103
104
|
const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
|
|
104
|
-
const { openInEditor, openFileInEditor, openInFinder, openInTerminal } = require('./lib/openExternalApp.cjs');
|
|
105
|
+
const { openInEditor, openFileInEditor, openInFinder, openInTerminal, runInTerminal } = require('./lib/openExternalApp.cjs');
|
|
106
|
+
const { checkPrereqs } = require('./lib/prereqs.cjs');
|
|
105
107
|
const { rebuildActiveIndex } = require('./lib/activeIndexRebuild.cjs');
|
|
106
108
|
const { activeIndexPath: promptSessionsActiveIndexPath } = require('./lib/epicMint.cjs');
|
|
107
109
|
const { allProjectCwds } = require('./lib/activeSessions.cjs');
|
|
@@ -266,6 +268,8 @@ function writeFirstPaintFailureLog() {
|
|
|
266
268
|
}
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
const { isPackagedApp } = require('./lib/appRuntime.cjs');
|
|
272
|
+
|
|
269
273
|
function resolveNpx() {
|
|
270
274
|
const isWin = process.platform === 'win32';
|
|
271
275
|
try {
|
|
@@ -373,6 +377,14 @@ async function performReboot() {
|
|
|
373
377
|
// Bounded-lifetime marker: the watchdog must not relaunch a second instance
|
|
374
378
|
// during this deliberate exit window (single-instance-lock race).
|
|
375
379
|
upgradeDrain.markRestarting();
|
|
380
|
+
if (isPackagedApp()) {
|
|
381
|
+
// Installer build: no npx — restart the app binary in place.
|
|
382
|
+
logReboot('packaged build: app.relaunch()');
|
|
383
|
+
app.relaunch();
|
|
384
|
+
runShutdownCleanup(); // app.exit bypasses will-quit
|
|
385
|
+
app.exit(0);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
376
388
|
let childOk = false;
|
|
377
389
|
try {
|
|
378
390
|
const child = relaunchViaNpx();
|
|
@@ -715,6 +727,15 @@ ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE
|
|
|
715
727
|
// on disk or the home self-check failed (e.g. macOS /Users symlink mismatch).
|
|
716
728
|
ipcMain.handle('app:claude-bin-status', () => bootClaudeBin);
|
|
717
729
|
ipcMain.handle('app:home-self-check', () => bootHomeSelfCheck);
|
|
730
|
+
ipcMain.handle('app:prereqs', async () => await checkPrereqs());
|
|
731
|
+
// Renderer sends only an id; the command always comes from a fresh main-side probe.
|
|
732
|
+
ipcMain.handle('app:prereqs-run-fix', async (_e, id) => {
|
|
733
|
+
const items = await checkPrereqs();
|
|
734
|
+
const item = typeof id === 'string' ? items.find((i) => i.id === id) : undefined;
|
|
735
|
+
if (!item) throw new Error(`unknown prereq id: ${String(id)}`);
|
|
736
|
+
if (!item.fix || !item.fix.command) throw new Error(`prereq ${id} has no fix command`);
|
|
737
|
+
return runInTerminal({ command: item.fix.command, shell: item.fix.shell });
|
|
738
|
+
});
|
|
718
739
|
// First-boot seeder outcomes ('done' | 'pending' | 'exhausted' per seeder),
|
|
719
740
|
// read live from each seeder's own marker file — see seedStatus.cjs.
|
|
720
741
|
ipcMain.handle('app:seed-status', () => getSeedStatus());
|
|
@@ -1190,6 +1211,18 @@ app.whenReady().then(async () => {
|
|
|
1190
1211
|
onSuspendWhileAlive: () => { stopSystemdInhibit(); startSystemdInhibit(); },
|
|
1191
1212
|
});
|
|
1192
1213
|
|
|
1214
|
+
// A Finder/Dock-launched packaged app inherits a minimal PATH; adopt the
|
|
1215
|
+
// login shell's so node/git/claude resolve like in the user's terminal.
|
|
1216
|
+
// Must run before resolveClaudeBin() and before any pty/scheduler/MCP spawn.
|
|
1217
|
+
if (process.platform !== 'win32' && process.env.SM_SKIP_LOGIN_SHELL_PATH !== '1') {
|
|
1218
|
+
try {
|
|
1219
|
+
const loginPath = await applyLoginShellPath();
|
|
1220
|
+
logs.writeLine({ scope: 'main', level: 'info', message: 'login-shell PATH', meta: loginPath });
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
logs.writeLine({ scope: 'main', level: 'warn', message: 'login-shell PATH failed', meta: { error: String(err && err.message || err) } });
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1193
1226
|
// Boot-time detection 1: surface `claude` binary resolution so a missing
|
|
1194
1227
|
// install becomes visible to the renderer instead of failing silently on
|
|
1195
1228
|
// first spawn attempt.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// which PRD 356-retire purges along with its capture hook.
|
|
10
10
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
|
+
const fsp = require('node:fs/promises');
|
|
12
13
|
const os = require('node:os');
|
|
13
14
|
const path = require('node:path');
|
|
14
15
|
const { KIND_CONFIG: WORKTREE_KIND_CONFIG } = require('./gitWorktree.cjs');
|
|
@@ -43,13 +44,14 @@ const TAIL_BYTES = 64 * 1024;
|
|
|
43
44
|
// Safety cap on distinct cwds to bound result set size. O(1) extra space.
|
|
44
45
|
const MAX_CWDS = 50;
|
|
45
46
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
47
|
+
// The disk scan (readdirSync of every dir under ~/.claude/projects — 2209
|
|
48
|
+
// dirs / 8.8 GB observed — plus a statSync per transcript, ~270 ms warm) is
|
|
49
|
+
// the expensive part. allProjectCwds() and activeProjectCwds() used to each
|
|
50
|
+
// pay their own full scan under separate (projectsDir, maxAgeMin, maxCwds)
|
|
51
|
+
// cache keys even though they read the same on-disk facts. Now the scan runs
|
|
52
|
+
// ONCE per projectsDir and is cached as a flat per-project record
|
|
53
|
+
// {rawCwd, newestMtimeMs}; every view (any maxAgeMin/maxCwds combination)
|
|
54
|
+
// filters/sorts that one record in memory instead of touching disk again.
|
|
53
55
|
//
|
|
54
56
|
// Invalidation: projectsDir's own mtime (bumps when a project DIRECTORY is
|
|
55
57
|
// added/removed — NOT when an existing transcript gains a line, which is
|
|
@@ -58,13 +60,20 @@ const MAX_CWDS = 50;
|
|
|
58
60
|
// scheduler/prdParser.cjs's dirCache and queueHistory.cjs's historyCacheKey.
|
|
59
61
|
// TTL sits above the 60 s dispatch-loop interval (scheduler POLL_INTERVAL_MS) so
|
|
60
62
|
// one pass never re-scans cold. Cached arrays are frozen — shared, never mutated.
|
|
63
|
+
// Stale-while-revalidate: once a projectsDir has been scanned at least once,
|
|
64
|
+
// an expired cache entry is served to the (synchronous) caller as-is, and at
|
|
65
|
+
// most one background fs.promises rescan is kicked off to refresh it. Only
|
|
66
|
+
// the very first scan of a given projectsDir pays the synchronous cost —
|
|
67
|
+
// every expiry after that is a cache read plus a fire-and-forget refresh.
|
|
61
68
|
const CACHE_TTL_MS = 120_000;
|
|
69
|
+
// Bounds how many project dirs the async rescan reads concurrently.
|
|
70
|
+
const RESCAN_CONCURRENCY = 32;
|
|
62
71
|
const EMPTY_CWDS = Object.freeze([]);
|
|
63
|
-
const
|
|
72
|
+
const rawScanCache = new Map(); // projectsDir -> { dirMtimeMs, cachedAt, records, refreshPromise }
|
|
64
73
|
|
|
65
74
|
/** Forces the next activeProjectCwds/allProjectCwds call to rescan. */
|
|
66
75
|
function bustProjectCwdCache() {
|
|
67
|
-
|
|
76
|
+
rawScanCache.clear();
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
// The ops-root folder name. A transcript's `cwd` can point INSIDE it whenever
|
|
@@ -120,42 +129,231 @@ function readTailLines(filePath, maxBytes) {
|
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
* scanOneProjectDirSync(projDir) → {rawCwd, newestMtimeMs} | null
|
|
133
|
+
* Newest *.jsonl's mtime plus its last parseable `cwd`, for one project dir.
|
|
134
|
+
*/
|
|
135
|
+
function scanOneProjectDirSync(projDir) {
|
|
136
|
+
let entries;
|
|
137
|
+
try {
|
|
138
|
+
entries = fs.readdirSync(projDir).filter((f) => f.endsWith('.jsonl'));
|
|
139
|
+
} catch { return null; }
|
|
140
|
+
|
|
141
|
+
let newestPath = null;
|
|
142
|
+
let newestMtimeMs = 0;
|
|
143
|
+
for (const tf of entries) {
|
|
144
|
+
const fp = path.join(projDir, tf);
|
|
145
|
+
try {
|
|
146
|
+
const st = fs.statSync(fp);
|
|
147
|
+
if (st.mtimeMs > newestMtimeMs) {
|
|
148
|
+
newestMtimeMs = st.mtimeMs;
|
|
149
|
+
newestPath = fp;
|
|
150
|
+
}
|
|
151
|
+
} catch { continue; }
|
|
152
|
+
}
|
|
153
|
+
if (!newestPath) return null;
|
|
154
|
+
|
|
155
|
+
const tlines = readTailLines(newestPath, TAIL_BYTES);
|
|
156
|
+
let rawCwd = null;
|
|
157
|
+
for (let i = tlines.length - 1; i >= 0; i--) {
|
|
158
|
+
let row;
|
|
159
|
+
try { row = JSON.parse(tlines[i]); } catch { continue; }
|
|
160
|
+
if (row.cwd) { rawCwd = row.cwd; break; }
|
|
161
|
+
}
|
|
162
|
+
return rawCwd ? { rawCwd, newestMtimeMs } : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* scanProjectsDir(projectsDir) → {rawCwd, newestMtimeMs}[] | null
|
|
132
167
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
168
|
+
* The first scan of a given projectsDir is synchronous (readdirSync of
|
|
169
|
+
* projectsDir, then per project dir as above) and its result is cached.
|
|
170
|
+
* A fresh cache entry (same dirMtimeMs, within TTL) is returned as-is.
|
|
171
|
+
* Once the dir's own mtime changes (a project dir was added/removed), the
|
|
172
|
+
* next call rescans synchronously and returns the new result — a stale
|
|
173
|
+
* record would otherwise hide a just-registered project from this caller.
|
|
174
|
+
* Only when dirMtimeMs is unchanged and the TTL has expired is the stale
|
|
175
|
+
* record returned as-is, with startAsyncRefresh kicking off at most one
|
|
176
|
+
* background fs.promises rescan to refresh it for the next caller. Returns
|
|
177
|
+
* null when projectsDir is missing/unreadable and no record has ever been
|
|
178
|
+
* cached for it.
|
|
136
179
|
*/
|
|
137
|
-
function
|
|
138
|
-
projectsDir = path.join(HOME, '.claude', 'projects'),
|
|
139
|
-
maxCwds = MAX_CWDS,
|
|
140
|
-
tmpDropRoots = TMP_DROP_ROOTS,
|
|
141
|
-
} = {}) {
|
|
142
|
-
const cacheKey = [projectsDir, maxAgeMin, maxCwds].join('|');
|
|
180
|
+
function scanProjectsDir(projectsDir) {
|
|
143
181
|
let dirMtimeMs;
|
|
144
182
|
try {
|
|
145
183
|
dirMtimeMs = fs.statSync(projectsDir).mtimeMs;
|
|
146
184
|
} catch {
|
|
147
|
-
// Missing/unreadable dir:
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
return
|
|
185
|
+
// Missing/unreadable dir: any prior cache entry must not survive to be
|
|
186
|
+
// handed back once the dir later appears — drop it rather than caching
|
|
187
|
+
// this empty result.
|
|
188
|
+
rawScanCache.delete(projectsDir);
|
|
189
|
+
return null;
|
|
152
190
|
}
|
|
153
|
-
const cached =
|
|
191
|
+
const cached = rawScanCache.get(projectsDir);
|
|
154
192
|
const now0 = Date.now();
|
|
155
193
|
if (cached && cached.dirMtimeMs === dirMtimeMs && now0 - cached.cachedAt < CACHE_TTL_MS) {
|
|
156
|
-
return cached.
|
|
194
|
+
return cached.records;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Same mtime, TTL expired: a prior successful scan exists and the dir
|
|
198
|
+
// itself hasn't changed, so serve it stale and refresh in the background
|
|
199
|
+
// rather than blocking this synchronous caller.
|
|
200
|
+
if (cached && cached.dirMtimeMs === dirMtimeMs) {
|
|
201
|
+
startAsyncRefresh(projectsDir);
|
|
202
|
+
return cached.records;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Either the first-ever scan of this projectsDir, or the dir's mtime
|
|
206
|
+
// changed (a project was added/removed) — must be synchronous, since a
|
|
207
|
+
// stale record would hide the change from this caller.
|
|
208
|
+
let slugs;
|
|
209
|
+
try { slugs = fs.readdirSync(projectsDir); } catch { rawScanCache.delete(projectsDir); return null; }
|
|
210
|
+
|
|
211
|
+
const records = [];
|
|
212
|
+
for (const slug of slugs) {
|
|
213
|
+
const rec = scanOneProjectDirSync(path.join(projectsDir, slug));
|
|
214
|
+
if (rec) records.push(rec);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
Object.freeze(records);
|
|
218
|
+
rawScanCache.set(projectsDir, { dirMtimeMs, cachedAt: now0, records, refreshPromise: null });
|
|
219
|
+
return records;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* readTailLinesAsync(filePath, maxBytes) → Promise<string[]>
|
|
224
|
+
* fs.promises counterpart of readTailLines, used only by the async rescan.
|
|
225
|
+
*/
|
|
226
|
+
async function readTailLinesAsync(filePath, maxBytes) {
|
|
227
|
+
let buf;
|
|
228
|
+
let fh;
|
|
229
|
+
try {
|
|
230
|
+
const stat = await fsp.stat(filePath);
|
|
231
|
+
if (stat.size === 0) return [];
|
|
232
|
+
const readSize = Math.min(maxBytes, stat.size);
|
|
233
|
+
fh = await fsp.open(filePath, 'r');
|
|
234
|
+
buf = Buffer.alloc(readSize);
|
|
235
|
+
await fh.read(buf, 0, readSize, stat.size - readSize);
|
|
236
|
+
} catch {
|
|
237
|
+
return [];
|
|
238
|
+
} finally {
|
|
239
|
+
if (fh) await fh.close().catch(() => {});
|
|
240
|
+
}
|
|
241
|
+
return buf.toString('utf8').split('\n').filter((l) => l.trim());
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* scanOneProjectDirAsync(projDir) → Promise<{rawCwd, newestMtimeMs} | null>
|
|
246
|
+
* fs.promises counterpart of scanOneProjectDirSync, used only by the async rescan.
|
|
247
|
+
*/
|
|
248
|
+
async function scanOneProjectDirAsync(projDir) {
|
|
249
|
+
let entries;
|
|
250
|
+
try {
|
|
251
|
+
entries = (await fsp.readdir(projDir)).filter((f) => f.endsWith('.jsonl'));
|
|
252
|
+
} catch { return null; }
|
|
253
|
+
|
|
254
|
+
let newestPath = null;
|
|
255
|
+
let newestMtimeMs = 0;
|
|
256
|
+
for (const tf of entries) {
|
|
257
|
+
const fp = path.join(projDir, tf);
|
|
258
|
+
try {
|
|
259
|
+
const st = await fsp.stat(fp);
|
|
260
|
+
if (st.mtimeMs > newestMtimeMs) {
|
|
261
|
+
newestMtimeMs = st.mtimeMs;
|
|
262
|
+
newestPath = fp;
|
|
263
|
+
}
|
|
264
|
+
} catch { continue; }
|
|
157
265
|
}
|
|
266
|
+
if (!newestPath) return null;
|
|
158
267
|
|
|
268
|
+
const tlines = await readTailLinesAsync(newestPath, TAIL_BYTES);
|
|
269
|
+
let rawCwd = null;
|
|
270
|
+
for (let i = tlines.length - 1; i >= 0; i--) {
|
|
271
|
+
let row;
|
|
272
|
+
try { row = JSON.parse(tlines[i]); } catch { continue; }
|
|
273
|
+
if (row.cwd) { rawCwd = row.cwd; break; }
|
|
274
|
+
}
|
|
275
|
+
return rawCwd ? { rawCwd, newestMtimeMs } : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* rescanProjectsDirAsync(projectsDir) → Promise<{dirMtimeMs, records}>
|
|
280
|
+
* The background counterpart of the first-scan branch in scanProjectsDir:
|
|
281
|
+
* same semantics, entirely fs.promises, with the per-project-dir reads
|
|
282
|
+
* bounded to RESCAN_CONCURRENCY in flight at once. A top-level failure
|
|
283
|
+
* (missing dir, unreadable) is left to throw/reject — startAsyncRefresh's
|
|
284
|
+
* caller catches it, logs once, and keeps the old cached record.
|
|
285
|
+
*/
|
|
286
|
+
async function rescanProjectsDirAsync(projectsDir) {
|
|
287
|
+
const dirMtimeMs = (await fsp.stat(projectsDir)).mtimeMs;
|
|
288
|
+
const slugs = await fsp.readdir(projectsDir);
|
|
289
|
+
|
|
290
|
+
const records = [];
|
|
291
|
+
let nextIdx = 0;
|
|
292
|
+
async function worker() {
|
|
293
|
+
for (;;) {
|
|
294
|
+
const i = nextIdx++;
|
|
295
|
+
if (i >= slugs.length) return;
|
|
296
|
+
const rec = await scanOneProjectDirAsync(path.join(projectsDir, slugs[i]));
|
|
297
|
+
if (rec) records.push(rec);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const workerCount = Math.min(RESCAN_CONCURRENCY, slugs.length);
|
|
301
|
+
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
302
|
+
|
|
303
|
+
Object.freeze(records);
|
|
304
|
+
return { dirMtimeMs, records };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* startAsyncRefresh(projectsDir) → Promise<void>
|
|
309
|
+
* Kicks off at most one in-flight background rescan per projectsDir — later
|
|
310
|
+
* calls while one is in flight get back the same promise instead of starting
|
|
311
|
+
* another. On success the cache entry is swapped atomically; on failure the
|
|
312
|
+
* old record is kept and the error is logged once. Never throws/rejects
|
|
313
|
+
* into a synchronous caller — scanProjectsDir never awaits this.
|
|
314
|
+
*/
|
|
315
|
+
function startAsyncRefresh(projectsDir) {
|
|
316
|
+
const entry = rawScanCache.get(projectsDir);
|
|
317
|
+
if (!entry) return Promise.resolve();
|
|
318
|
+
if (entry.refreshPromise) return entry.refreshPromise;
|
|
319
|
+
|
|
320
|
+
const promise = rescanProjectsDirAsync(projectsDir)
|
|
321
|
+
.then((result) => {
|
|
322
|
+
const current = rawScanCache.get(projectsDir);
|
|
323
|
+
if (!current) return; // busted mid-flight; nothing to swap into
|
|
324
|
+
rawScanCache.set(projectsDir, {
|
|
325
|
+
dirMtimeMs: result.dirMtimeMs, cachedAt: Date.now(), records: result.records, refreshPromise: null,
|
|
326
|
+
});
|
|
327
|
+
})
|
|
328
|
+
.catch((err) => {
|
|
329
|
+
console.error(`[activeSessions] async rescan of ${projectsDir} failed, keeping stale record: ${err.message}`);
|
|
330
|
+
const current = rawScanCache.get(projectsDir);
|
|
331
|
+
if (current) current.refreshPromise = null;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
entry.refreshPromise = promise;
|
|
335
|
+
return promise;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* __waitForPendingRefresh(projectsDir) → Promise<void>
|
|
340
|
+
* Test-only hook: awaits the in-flight async rescan for projectsDir, if any.
|
|
341
|
+
* Resolves immediately when no refresh is in flight.
|
|
342
|
+
*/
|
|
343
|
+
function __waitForPendingRefresh(projectsDir) {
|
|
344
|
+
const entry = rawScanCache.get(projectsDir);
|
|
345
|
+
return entry && entry.refreshPromise ? entry.refreshPromise : Promise.resolve();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* deriveCwds(records, maxAgeMin, maxCwds, tmpDropRoots) → string[]
|
|
350
|
+
*
|
|
351
|
+
* Filters/sorts a scanProjectsDir() record into the view activeProjectCwds/
|
|
352
|
+
* allProjectCwds promise: age cutoff, ops-root/worktree normalization,
|
|
353
|
+
* dedup, existence check, and the maxCwds cap — identical semantics to the
|
|
354
|
+
* old per-call scan, now performed in memory against the shared record.
|
|
355
|
+
*/
|
|
356
|
+
function deriveCwds(records, maxAgeMin, maxCwds, tmpDropRoots) {
|
|
159
357
|
// maxAgeMin === Infinity means "every project ever seen, no recency filter"
|
|
160
358
|
// (allProjectCwds below). Date.now() - Infinity is -Infinity, which every
|
|
161
359
|
// mtime clears — spelled out here because it reads like an accident.
|
|
@@ -199,46 +397,39 @@ function activeProjectCwds(maxAgeMin = 90, {
|
|
|
199
397
|
result.push(cwd);
|
|
200
398
|
}
|
|
201
399
|
|
|
202
|
-
|
|
203
|
-
let slugs;
|
|
204
|
-
try { slugs = fs.readdirSync(projectsDir); } catch { cwdScanCache.delete(cacheKey); return Object.freeze(result); }
|
|
205
|
-
|
|
206
|
-
for (const slug of slugs) {
|
|
400
|
+
for (const { rawCwd, newestMtimeMs } of records) {
|
|
207
401
|
if (result.length >= maxCwds) break;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
entries = fs.readdirSync(projDir).filter((f) => f.endsWith('.jsonl'));
|
|
212
|
-
} catch { continue; }
|
|
213
|
-
|
|
214
|
-
// Find the most recently modified transcript in this project dir.
|
|
215
|
-
let newestPath = null;
|
|
216
|
-
let newestMtimeMs = 0;
|
|
217
|
-
for (const tf of entries) {
|
|
218
|
-
const fp = path.join(projDir, tf);
|
|
219
|
-
try {
|
|
220
|
-
const st = fs.statSync(fp);
|
|
221
|
-
if (st.mtimeMs > newestMtimeMs) {
|
|
222
|
-
newestMtimeMs = st.mtimeMs;
|
|
223
|
-
newestPath = fp;
|
|
224
|
-
}
|
|
225
|
-
} catch { continue; }
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (!newestPath || newestMtimeMs < cutoffMs) continue;
|
|
229
|
-
|
|
230
|
-
// Read `cwd` from the last parseable line of the transcript.
|
|
231
|
-
const tlines = readTailLines(newestPath, TAIL_BYTES);
|
|
232
|
-
for (let i = tlines.length - 1; i >= 0; i--) {
|
|
233
|
-
let row;
|
|
234
|
-
try { row = JSON.parse(tlines[i]); } catch { continue; }
|
|
235
|
-
if (row.cwd) { addCwd(row.cwd); break; }
|
|
236
|
-
}
|
|
402
|
+
if (newestMtimeMs < cutoffMs) continue;
|
|
403
|
+
addCwd(rawCwd);
|
|
237
404
|
}
|
|
238
405
|
|
|
239
|
-
Object.freeze(result);
|
|
240
|
-
|
|
241
|
-
|
|
406
|
+
return Object.freeze(result);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* activeProjectCwds(maxAgeMin = 90, opts?) → string[]
|
|
411
|
+
*
|
|
412
|
+
* Returns distinct, on-disk project cwds that have an open/active session
|
|
413
|
+
* within the last maxAgeMin minutes.
|
|
414
|
+
*
|
|
415
|
+
* Sole detection path: scan ~/.claude/projects/*/ for transcript *.jsonl
|
|
416
|
+
* files modified within maxAgeMin, read `cwd` from the last parseable line
|
|
417
|
+
* of the newest transcript per project dir.
|
|
418
|
+
* Complexity: O(P × L) over P project dirs, each bounded-tail read (64 KB) —
|
|
419
|
+
* paid once per TTL window (see scanProjectsDir), shared across views.
|
|
420
|
+
*
|
|
421
|
+
* opts (for testing):
|
|
422
|
+
* projectsDir — override the default ~/.claude/projects path
|
|
423
|
+
* tmpDropRoots — override the tmp guard's drop roots (default TMP_DROP_ROOTS)
|
|
424
|
+
*/
|
|
425
|
+
function activeProjectCwds(maxAgeMin = 90, {
|
|
426
|
+
projectsDir = path.join(HOME, '.claude', 'projects'),
|
|
427
|
+
maxCwds = MAX_CWDS,
|
|
428
|
+
tmpDropRoots = TMP_DROP_ROOTS,
|
|
429
|
+
} = {}) {
|
|
430
|
+
const records = scanProjectsDir(projectsDir);
|
|
431
|
+
if (!records) return EMPTY_CWDS;
|
|
432
|
+
return deriveCwds(records, maxAgeMin, maxCwds, tmpDropRoots);
|
|
242
433
|
}
|
|
243
434
|
|
|
244
435
|
/**
|
|
@@ -266,4 +457,5 @@ module.exports = {
|
|
|
266
457
|
projectRootOf,
|
|
267
458
|
worktreeMainRootOf,
|
|
268
459
|
bustProjectCwdCache,
|
|
460
|
+
__waitForPendingRefresh,
|
|
269
461
|
};
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
'use strict';
|
|
24
24
|
|
|
25
|
+
const path = require('node:path');
|
|
25
26
|
const { z } = require('zod');
|
|
26
27
|
const { WorkTypeSchema, LEGACY_WORK_TYPES } = require('./workTypeLibrary.cjs');
|
|
27
28
|
const { PERSONA_NAME_RE, ALL_PROJECTS } = require('../agentLibrary.cjs');
|
|
@@ -35,8 +36,13 @@ const BoundedString = z.string().max(2000);
|
|
|
35
36
|
// `projects` entries are either the `'*'` sentinel (every project) or an
|
|
36
37
|
// absolute path — agentLibrary.cjs's openProjects always deals in absolute
|
|
37
38
|
// cwds, and a relative entry could never match one.
|
|
39
|
+
/** @param {string} v @param {NodeJS.Platform} [platform] */
|
|
40
|
+
function isAbsoluteForPlatform(v, platform = process.platform) {
|
|
41
|
+
return (platform === 'win32' ? path.win32 : path.posix).isAbsolute(v);
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
const ProjectEntrySchema = z.string().refine(
|
|
39
|
-
(v) => v === ALL_PROJECTS || v
|
|
45
|
+
(/** @type {string} */ v) => v === ALL_PROJECTS || isAbsoluteForPlatform(v),
|
|
40
46
|
`must be "${ALL_PROJECTS}" or an absolute path`,
|
|
41
47
|
);
|
|
42
48
|
|
|
@@ -95,4 +101,5 @@ function assertValidAgentPersonaSave(persona) {
|
|
|
95
101
|
module.exports = {
|
|
96
102
|
AgentPersonaSaveSchema,
|
|
97
103
|
assertValidAgentPersonaSave,
|
|
104
|
+
isAbsoluteForPlatform,
|
|
98
105
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* appRuntime.cjs — how to run a Node script from an EXTERNAL process.
|
|
3
|
+
*
|
|
4
|
+
* Packaged installer builds may have no system `node`, so scripts handed to
|
|
5
|
+
* Claude Code hooks / MCP registrations run through the app's own binary with
|
|
6
|
+
* ELECTRON_RUN_AS_NODE=1. npx/dev builds keep using `node`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{ packaged?: boolean }} [opts]
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
function isPackagedApp(opts) {
|
|
16
|
+
if (opts && typeof opts.packaged === 'boolean') return opts.packaged;
|
|
17
|
+
if (process.env.SM_FORCE_PACKAGED === '1') return true;
|
|
18
|
+
try {
|
|
19
|
+
return require('electron').app?.isPackaged === true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} scriptPath
|
|
27
|
+
* @param {{ packaged?: boolean, execPath?: string }} [opts]
|
|
28
|
+
* @returns {{ command: string, args: string[], env: Record<string, string> }}
|
|
29
|
+
*/
|
|
30
|
+
function nodeSpawnSpec(scriptPath, opts) {
|
|
31
|
+
if (!isPackagedApp(opts)) return { command: 'node', args: [scriptPath], env: {} };
|
|
32
|
+
return {
|
|
33
|
+
command: (opts && opts.execPath) || process.execPath,
|
|
34
|
+
args: [scriptPath],
|
|
35
|
+
env: { ELECTRON_RUN_AS_NODE: '1' },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {string} p */
|
|
40
|
+
function quote(p) {
|
|
41
|
+
if (p.includes('"')) throw new Error(`appRuntime: path contains a double quote: ${p}`);
|
|
42
|
+
return `"${p}"`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Shell command string (hook `command` fields). Paths are double-quoted.
|
|
47
|
+
* @param {string} scriptPath
|
|
48
|
+
* @param {{ packaged?: boolean, execPath?: string }} [opts]
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function nodeShellCommand(scriptPath, opts) {
|
|
52
|
+
if (!isPackagedApp(opts)) return `node ${quote(scriptPath)}`;
|
|
53
|
+
const execPath = (opts && opts.execPath) || process.execPath;
|
|
54
|
+
return `ELECTRON_RUN_AS_NODE=1 ${quote(execPath)} ${quote(scriptPath)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { isPackagedApp, nodeSpawnSpec, nodeShellCommand };
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
const fs = require('node:fs');
|
|
54
54
|
const { spawn, execFile } = require('node:child_process');
|
|
55
|
+
const { killTree } = require('./killTree.cjs');
|
|
55
56
|
|
|
56
57
|
// Injectable for tests (enumeration-failure coverage).
|
|
57
58
|
const deps = { execFile };
|
|
@@ -150,6 +151,7 @@ function openLog(logPath) {
|
|
|
150
151
|
// Async: resolves to the survivors list, rejects on ps failure/timeout. O(P) in
|
|
151
152
|
// the process table, off the main event loop.
|
|
152
153
|
function enumerateProcessGroupSurvivors(pgid) {
|
|
154
|
+
// ps -eo is Linux-only; on win32 (and macOS) report the empty/unknown result without spawning ps.
|
|
153
155
|
if (process.platform !== 'linux') return Promise.resolve([]);
|
|
154
156
|
if (!pgid || pgid <= 1) return Promise.resolve([]);
|
|
155
157
|
return new Promise((resolve, reject) => {
|
|
@@ -182,12 +184,14 @@ function sweepChildProcessGroup(child, detached, safeLog) {
|
|
|
182
184
|
if (!child || !child.pid || child.pid <= 1) return null;
|
|
183
185
|
// Defensive: never target the Session Manager process's own group.
|
|
184
186
|
if (child.pid === process.pid) return null;
|
|
187
|
+
// win32 has no process groups; taskkill on an already-exited pid risks hitting a reused pid.
|
|
188
|
+
if (process.platform === 'win32') return null;
|
|
185
189
|
|
|
186
190
|
const pid = child.pid;
|
|
187
191
|
const kill = () => {
|
|
188
|
-
|
|
192
|
+
killTree(pid, 'SIGTERM'); // ESRCH on an empty group is the normal case
|
|
189
193
|
const t = setTimeout(() => {
|
|
190
|
-
|
|
194
|
+
killTree(pid, 'SIGKILL');
|
|
191
195
|
}, POST_EXIT_GROUP_SWEEP_GRACE_MS);
|
|
192
196
|
if (t.unref) t.unref();
|
|
193
197
|
};
|
|
@@ -219,13 +223,9 @@ function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watc
|
|
|
219
223
|
killTree(signal) {
|
|
220
224
|
const c = ctx.child;
|
|
221
225
|
if (!c) return false;
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
catch {
|
|
226
|
-
try { process.kill(c.pid, signal); return true; }
|
|
227
|
-
catch { return false; /* already dead */ }
|
|
228
|
-
}
|
|
226
|
+
// Whole process group on POSIX / taskkill /T on win32 (requires detachedSpawnOpts at spawn);
|
|
227
|
+
// killTree falls back to the bare pid if the process is not a group leader.
|
|
228
|
+
return killTree(c.pid, signal, { log: safeLog });
|
|
229
229
|
},
|
|
230
230
|
addTimer(t) { extraTimers.push(t); },
|
|
231
231
|
};
|
|
@@ -315,7 +315,7 @@ function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watc
|
|
|
315
315
|
const cancel = () => {
|
|
316
316
|
clearAllTimers();
|
|
317
317
|
// Best-effort kill; ignore errors (process may already be gone).
|
|
318
|
-
|
|
318
|
+
if (!killTree(child.pid, 'SIGKILL', { log: safeLog })) {
|
|
319
319
|
try { child.kill('SIGKILL'); } catch { /* */ }
|
|
320
320
|
}
|
|
321
321
|
};
|