stim 1.14.0 → 1.16.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.
Files changed (152) hide show
  1. package/README.md +2 -0
  2. package/dist/activity-BxwdD81S.mjs +609 -0
  3. package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
  4. package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
  5. package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
  6. package/dist/android-cas-8U4DPqeR.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
  9. package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
  10. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  11. package/dist/budget-DjrURfQn.mjs +2700 -0
  12. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
  13. package/dist/build-slots-rUk77xqh.mjs +132 -0
  14. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  15. package/dist/cache-manifest.mjs +1 -1
  16. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  17. package/dist/cli-BkYZ_6hy.mjs +51 -0
  18. package/dist/cli.mjs +6 -42
  19. package/dist/client-BTY686R6.mjs +1368 -0
  20. package/dist/collector-run.mjs +9 -195
  21. package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
  22. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  23. package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
  24. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
  25. package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
  26. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  27. package/dist/dev-client-CroAIjjj.mjs +43 -0
  28. package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
  29. package/dist/device-capacity-BHwxNBlu.mjs +136 -0
  30. package/dist/device-host-worker.d.mts +1 -0
  31. package/dist/device-host-worker.mjs +964 -0
  32. package/dist/device-ios-BadmeL2V.mjs +381 -0
  33. package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
  34. package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
  35. package/dist/device-pool-CUaEWMMm.mjs +125 -0
  36. package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
  37. package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
  38. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
  39. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  40. package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
  41. package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
  42. package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
  43. package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
  44. package/dist/hosted-client-a-WmqE2L.mjs +197 -0
  45. package/dist/hosted-ios-CIWelIqf.mjs +220 -0
  46. package/dist/hosted-logs-D5g041Q3.mjs +187 -0
  47. package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
  48. package/dist/idle-DYVymG92.mjs +1099 -0
  49. package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
  50. package/dist/in-use-DvBw0Kxo.mjs +130 -0
  51. package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
  52. package/dist/ios-Cr1h5gnO.mjs +189 -0
  53. package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
  54. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  55. package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
  56. package/dist/ios-state-DS5UpYTH.mjs +48 -0
  57. package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
  58. package/dist/logs-CHEVK8zD.mjs +371 -0
  59. package/dist/machines-BIBUdhpj.mjs +265 -0
  60. package/dist/macos-CSYEAFmF.mjs +473 -0
  61. package/dist/macos-run.d.mts +5 -0
  62. package/dist/macos-run.mjs +2 -0
  63. package/dist/maintenance-run.d.mts +4 -0
  64. package/dist/maintenance-run.mjs +254 -0
  65. package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
  66. package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
  67. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
  68. package/dist/native-run-NaBrRFtQ.mjs +147 -0
  69. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
  70. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  71. package/dist/offload-worker.d.mts +122 -0
  72. package/dist/offload-worker.mjs +606 -0
  73. package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
  74. package/dist/ownership-hCkPzGmr.mjs +571 -0
  75. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  76. package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
  77. package/dist/prebuild-D2fsca_Z.mjs +213 -0
  78. package/dist/preview-DhVbvPAZ.mjs +318 -0
  79. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  80. package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
  81. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  82. package/dist/pull-requests.d.mts +120 -0
  83. package/dist/pull-requests.mjs +2 -0
  84. package/dist/recordings-BIXOzVnr.mjs +37 -0
  85. package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
  86. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
  87. package/dist/run-f4vI6EnK.mjs +119 -0
  88. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
  89. package/dist/server-expo-CVqO2c35.mjs +2 -0
  90. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
  91. package/dist/settings-BBpMcDbD.mjs +25 -0
  92. package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
  93. package/dist/settings-oTSkkNrM.mjs +1439 -0
  94. package/dist/settings.schema.json +845 -97
  95. package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
  96. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
  97. package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  98. package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
  99. package/dist/stage-CzaiwFsI.mjs +143 -0
  100. package/dist/start-CX8Ig-U2.mjs +2 -0
  101. package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
  102. package/dist/state-3DwSQIBK.mjs +41 -0
  103. package/dist/state-B4PQFXMW.d.mts +12 -0
  104. package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
  105. package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
  106. package/dist/stats-CEZ_i5je.mjs +77 -0
  107. package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
  108. package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
  109. package/dist/status-DlKvVJBE.mjs +49 -0
  110. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  111. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  112. package/dist/stim-footprint.swift +32 -0
  113. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  114. package/dist/stop-B0Yxtmiy.mjs +48 -0
  115. package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
  116. package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
  117. package/dist/stop-cause-CidxIqup.mjs +97 -0
  118. package/dist/supervisor-run.d.mts +43 -11
  119. package/dist/supervisor-run.mjs +363 -24
  120. package/dist/support-CAP8qDUF.mjs +316 -0
  121. package/dist/support-D4aYJniO.mjs +201 -0
  122. package/dist/teardown-CL6vDs2Y.mjs +727 -0
  123. package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
  124. package/dist/trigger-Bemu8ytw.mjs +65 -0
  125. package/dist/trigger-CXts6qt1.mjs +2 -0
  126. package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
  127. package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
  128. package/dist/web-run.mjs +310 -13
  129. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  130. package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
  131. package/dist/workspaces-BV4pdHFz.mjs +306 -0
  132. package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
  133. package/dist/worktree-jnG8Iph5.mjs +2 -0
  134. package/package.json +10 -5
  135. package/shim/bundle-response.cjs +28 -12
  136. package/dist/activity-CYVDcrt3.mjs +0 -356
  137. package/dist/budget-CGDkhvv_.mjs +0 -1745
  138. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  139. package/dist/idle-DXtx3RbX.mjs +0 -191
  140. package/dist/in-use-DlqBDKqY.mjs +0 -249
  141. package/dist/logs-Civ4gbBH.mjs +0 -486
  142. package/dist/merge-state-ytFQCeh4.mjs +0 -262
  143. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  144. package/dist/settings-D61iNOsB.mjs +0 -629
  145. package/dist/start-Cetfvy8l.mjs +0 -2
  146. package/dist/stats-CFpFGlJV.mjs +0 -62
  147. package/dist/stats-CX1Z8Tca.mjs +0 -368
  148. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  149. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  150. package/dist/worktree-BVrHsYZG.mjs +0 -752
  151. package/dist/worktree-CHWWvm5v.mjs +0 -2
  152. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -0,0 +1,371 @@
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
3
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { dirname, isAbsolute, join, relative } from "node:path";
5
+ import { join as join$1, resolve as resolve$1 } from "path";
6
+ import { buildCacheRoot as sharedBuildCache, withDirLock as withDirLock$1, workspaceId, workspaceName as workspaceName$1, workspaceStateDir as workspaceDir } from "@stim-cli/core";
7
+ import { WORKSPACE_AGENT_KEY, emulatorLogFile, readJsonObject, readWorkspaceState, readWorkspaceState as readWorkspaceState$1, sharedCcache as sharedCcache$1, sharedCompilationCache as sharedCompilationCache$1, supervisorLogFile, supervisorPidFile, workspaceAgentDeviceDir, workspaceDerivedData, workspaceLastUsed, workspaceLogErrorIndex, workspaceLogsDir as workspaceLogsDir$1, workspaceMetadataFile, workspaceStateFile, workspaceStateLock } from "@stim-cli/core/state";
8
+ import { homedir } from "node:os";
9
+ import { readFile, readdir } from "node:fs/promises";
10
+ //#region src/agent-sessions.ts
11
+ const TITLE_MAX_LENGTH = 120;
12
+ const SESSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
13
+ const CODEX_ACTIVE_MS = 18e5;
14
+ const CODEX_QUERY_TIMEOUT_MS = 5e3;
15
+ const COMMAND_ATTRIBUTION_MS = 18e5;
16
+ /**
17
+ * Claude desktop opens a Claude Code session with `claude://code/continue?session=<id>`, and accepts only its own
18
+ * `local_` session ids there, which a session file carries as `hostSessionId` when the desktop app hosts the session.
19
+ */
20
+ const CLAUDE_DESKTOP_SESSION = /^local_[A-Za-z0-9-]{1,64}$/;
21
+ /**
22
+ * Claude Code writes a Remote Control session's claude.ai id to its session file as `bridgeSessionId`, and prints
23
+ * `https://claude.ai/code/<id>` as the session URL, which the Claude mobile app also opens as a universal link.
24
+ */
25
+ const CLAUDE_REMOTE_SESSION = /^session_[A-Za-z0-9]{1,64}$/;
26
+ function shortTitle(value) {
27
+ if (typeof value !== "string") return void 0;
28
+ const title = value.replace(/\p{Cc}+/gu, " ").replace(/\s+/g, " ").trim();
29
+ if (!title) return void 0;
30
+ return title.length > TITLE_MAX_LENGTH ? `${title.slice(0, 117)}...` : title;
31
+ }
32
+ function isoFromMs(value) {
33
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? new Date(value).toISOString() : void 0;
34
+ }
35
+ function withOptional(session, fields) {
36
+ for (const [key, value] of Object.entries(fields)) if (value !== void 0) Object.assign(session, { [key]: value });
37
+ return session;
38
+ }
39
+ /** The agent session a shell's environment names, from `CLAUDE_CODE_SESSION_ID` or `CODEX_THREAD_ID`. */
40
+ function agentFromEnv(env) {
41
+ const claude = env.CLAUDE_CODE_SESSION_ID?.trim();
42
+ if (claude && SESSION_ID.test(claude)) return {
43
+ tool: "claude-code",
44
+ sessionId: claude
45
+ };
46
+ const codex = env.CODEX_THREAD_ID?.trim();
47
+ if (codex && SESSION_ID.test(codex)) return {
48
+ tool: "codex",
49
+ sessionId: codex
50
+ };
51
+ return null;
52
+ }
53
+ /**
54
+ * A Claude Code session from one `~/.claude/sessions/<pid>.json` file, or null when the entry lacks a pid, session id
55
+ * or absolute cwd. The format is Claude Code's own and undocumented, so every other field is optional.
56
+ */
57
+ function parseClaudeSession(entry, apps) {
58
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
59
+ const raw = entry;
60
+ if (!Number.isSafeInteger(raw.pid) || raw.pid <= 0) return null;
61
+ if (typeof raw.sessionId !== "string" || !SESSION_ID.test(raw.sessionId)) return null;
62
+ if (typeof raw.cwd !== "string" || !isAbsolute(raw.cwd)) return null;
63
+ const host = raw.hostSessionId;
64
+ const bridge = raw.bridgeSessionId;
65
+ return withOptional({
66
+ tool: "claude-code",
67
+ sessionId: raw.sessionId,
68
+ cwd: raw.cwd
69
+ }, {
70
+ title: shortTitle(raw.name),
71
+ startedAt: isoFromMs(raw.startedAt),
72
+ lastActiveAt: isoFromMs(raw.updatedAt),
73
+ pid: raw.pid,
74
+ openUrl: apps.claude && typeof host === "string" && CLAUDE_DESKTOP_SESSION.test(host) ? `claude://code/continue?session=${host}` : void 0,
75
+ webUrl: typeof bridge === "string" && CLAUDE_REMOTE_SESSION.test(bridge) ? `https://claude.ai/code/${bridge}` : void 0
76
+ });
77
+ }
78
+ function isSubagentSource(source) {
79
+ if (typeof source !== "string" || !source.startsWith("{")) return false;
80
+ try {
81
+ const parsed = JSON.parse(source);
82
+ return Boolean(parsed && typeof parsed === "object" && "subagent" in parsed);
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+ /**
88
+ * Codex threads from rows of its `threads` table. `name` is the thread's short name; `title` there is the first
89
+ * prompt, so it is never read. Subagent threads, which Codex spawns inside a thread, are left out.
90
+ */
91
+ function parseCodexThreads(rows, apps) {
92
+ if (!Array.isArray(rows)) return [];
93
+ return rows.flatMap((row) => {
94
+ if (!row || typeof row !== "object") return [];
95
+ const raw = row;
96
+ if (typeof raw.id !== "string" || !SESSION_ID.test(raw.id)) return [];
97
+ if (typeof raw.cwd !== "string" || !isAbsolute(raw.cwd) || isSubagentSource(raw.source)) return [];
98
+ return [withOptional({
99
+ tool: "codex",
100
+ sessionId: raw.id,
101
+ cwd: raw.cwd
102
+ }, {
103
+ title: shortTitle(raw.name),
104
+ startedAt: isoFromMs(raw.created_at_ms),
105
+ lastActiveAt: isoFromMs(raw.updated_at_ms),
106
+ openUrl: apps.codex ? `codex://threads/${raw.id}` : void 0
107
+ })];
108
+ });
109
+ }
110
+ function codexStateDatabase(names) {
111
+ let best = null;
112
+ for (const name of names) {
113
+ const version = Number(/^state_(\d+)\.sqlite$/.exec(name)?.[1]);
114
+ if (Number.isFinite(version) && (!best || version > best.version)) best = {
115
+ name,
116
+ version
117
+ };
118
+ }
119
+ return best?.name ?? null;
120
+ }
121
+ const APP_BUNDLES = {
122
+ claude: {
123
+ names: ["Claude"],
124
+ bundleId: "com.anthropic.claudefordesktop"
125
+ },
126
+ codex: {
127
+ names: ["ChatGPT", "Codex"],
128
+ bundleId: "com.openai.codex"
129
+ }
130
+ };
131
+ function bundleInstalled(home, { names, bundleId }) {
132
+ for (const folder of ["/Applications", join(home, "Applications")]) for (const name of names) try {
133
+ if (readFileSync(join(folder, `${name}.app`, "Contents", "Info.plist"), "latin1").includes(bundleId)) return true;
134
+ } catch {}
135
+ return false;
136
+ }
137
+ function installedAgentApps(home = homedir()) {
138
+ if (process.platform !== "darwin") return {
139
+ claude: false,
140
+ codex: false
141
+ };
142
+ return {
143
+ claude: bundleInstalled(home, APP_BUNDLES.claude),
144
+ codex: bundleInstalled(home, APP_BUNDLES.codex)
145
+ };
146
+ }
147
+ function canonical(session) {
148
+ try {
149
+ return {
150
+ ...session,
151
+ cwd: realpathSync(session.cwd)
152
+ };
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+ function sessionProcessRuns(session) {
158
+ const start = (0, process_identity_exports.inspectProcessStart)(session.pid);
159
+ if (start.status === "gone") return false;
160
+ if (start.status === "unknown" || !session.startedAt) return true;
161
+ return start.startedAtMs <= Date.parse(session.startedAt) + 2e3;
162
+ }
163
+ async function claudeSessions(home, apps) {
164
+ const directory = join(home, ".claude", "sessions");
165
+ let names;
166
+ try {
167
+ names = (await readdir(directory)).filter((name) => name.endsWith(".json"));
168
+ } catch {
169
+ return [];
170
+ }
171
+ return (await Promise.all(names.map(async (name) => {
172
+ try {
173
+ const session = parseClaudeSession(JSON.parse(await readFile(join(directory, name), "utf8")), apps);
174
+ return session && sessionProcessRuns(session) ? canonical(session) : null;
175
+ } catch {
176
+ return null;
177
+ }
178
+ }))).filter((session) => session !== null);
179
+ }
180
+ const CODEX_QUERY = `const { DatabaseSync } = require('node:sqlite');
181
+ const db = new DatabaseSync(process.argv[1], { readOnly: true });
182
+ const rows = db.prepare('SELECT id, cwd, name, source, created_at_ms, updated_at_ms FROM threads WHERE archived = 0 AND updated_at_ms >= ? AND source NOT LIKE ? ORDER BY updated_at_ms DESC LIMIT 50').all(Number(process.argv[2]), '%"subagent"%');
183
+ db.close();
184
+ process.stdout.write(JSON.stringify(rows));`;
185
+ async function codexSessions(codexHome, apps, now) {
186
+ let database;
187
+ try {
188
+ database = codexStateDatabase(await readdir(codexHome));
189
+ } catch {
190
+ return [];
191
+ }
192
+ if (!database) return [];
193
+ try {
194
+ const out = await getExecutor().runFileAsync(process.execPath, [
195
+ "--no-warnings",
196
+ "--experimental-sqlite",
197
+ "-e",
198
+ CODEX_QUERY,
199
+ join(codexHome, database),
200
+ String(now - CODEX_ACTIVE_MS)
201
+ ], { timeoutMs: CODEX_QUERY_TIMEOUT_MS });
202
+ return parseCodexThreads(JSON.parse(out), apps).map(canonical).filter((session) => session !== null);
203
+ } catch {
204
+ return [];
205
+ }
206
+ }
207
+ /**
208
+ * The Claude Code sessions whose process runs and the Codex threads updated in the last 30 minutes, with canonical
209
+ * working directories. Every source is optional: a missing, unreadable or changed format yields no sessions from it.
210
+ */
211
+ async function discoverAgentSessions({ home = homedir(), codexHome = process.env.CODEX_HOME || join(home, ".codex"), apps = installedAgentApps(home), now = Date.now() } = {}) {
212
+ const [claude, codex] = await Promise.all([claudeSessions(home, apps), codexSessions(codexHome, apps, now)]);
213
+ return [...claude, ...codex];
214
+ }
215
+ function within(directory, candidate) {
216
+ const path = relative(directory, candidate);
217
+ return path === "" || !path.startsWith("..") && !isAbsolute(path);
218
+ }
219
+ function agentKey(session) {
220
+ return `${session.tool}:${session.sessionId}`;
221
+ }
222
+ function activeAt(session) {
223
+ const at = Date.parse(session.lastActiveAt ?? "");
224
+ return Number.isFinite(at) ? at : -Infinity;
225
+ }
226
+ /**
227
+ * The sessions working in each workspace, most recently active first. A session belongs to the deepest workspace
228
+ * whose path holds its cwd, and to every workspace whose git worktree root is its cwd. A workspace's recorded session
229
+ * is added with the discovered session's details when it is running, or on its own for `COMMAND_ATTRIBUTION_MS`
230
+ * after the command.
231
+ */
232
+ function attributeAgentSessions(workspaces, sessions, now) {
233
+ const found = workspaces.map(() => /* @__PURE__ */ new Map());
234
+ for (const session of sessions) {
235
+ const holders = workspaces.flatMap((workspace, i) => within(workspace.path, session.cwd) ? [i] : []);
236
+ const deepest = Math.max(-1, ...holders.map((i) => workspaces[i].path.length));
237
+ workspaces.forEach((workspace, i) => {
238
+ if (holders.includes(i) && workspace.path.length === deepest || workspace.root === session.cwd) found[i].set(agentKey(session), session);
239
+ });
240
+ }
241
+ workspaces.forEach(({ recorded }, i) => {
242
+ if (!recorded) return;
243
+ const running = found[i].get(agentKey(recorded)) ?? sessions.find((session) => agentKey(session) === agentKey(recorded));
244
+ if (running) {
245
+ const newer = activeAt(recorded) > activeAt(running);
246
+ found[i].set(agentKey(recorded), newer ? {
247
+ ...running,
248
+ lastActiveAt: recorded.lastActiveAt
249
+ } : running);
250
+ } else if (now - activeAt(recorded) <= COMMAND_ATTRIBUTION_MS) found[i].set(agentKey(recorded), recorded);
251
+ });
252
+ return found.map((map) => [...map.values()].toSorted((a, b) => activeAt(b) - activeAt(a)));
253
+ }
254
+ //#endregion
255
+ //#region src/workspace/paths.ts
256
+ function ensureWorkspaceStorage(projectRoot) {
257
+ const canonicalRoot = resolve$1(projectRoot);
258
+ const dir = workspaceDir(canonicalRoot);
259
+ const file = workspaceMetadataFile(canonicalRoot);
260
+ mkdirSync(dir, { recursive: true });
261
+ return withDirLock$1(join$1(dir, "metadata.lock"), () => {
262
+ if (!existsSync(file)) {
263
+ const metadata = {
264
+ projectRoot: canonicalRoot,
265
+ workspace: workspaceName$1(canonicalRoot),
266
+ version: 1
267
+ };
268
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
269
+ writeFileSync(tmp, `${JSON.stringify(metadata, null, 2)}\n`);
270
+ try {
271
+ renameSync(tmp, file);
272
+ } catch (error) {
273
+ rmSync(tmp, { force: true });
274
+ throw error;
275
+ }
276
+ return dir;
277
+ }
278
+ if (readJsonObject(file)?.projectRoot === canonicalRoot) return dir;
279
+ const error = /* @__PURE__ */ new Error(`Stim workspace collision at ${dir}: its workspace.json does not belong to ${canonicalRoot}.`);
280
+ error.code = "STIM_WORKSPACE_COLLISION";
281
+ throw error;
282
+ });
283
+ }
284
+ //#endregion
285
+ //#region src/workspace/workspace-state.ts
286
+ function withWorkspaceStateLock(root, fn) {
287
+ const file = workspaceStateFile(root);
288
+ return withDirLock$1(workspaceStateLock(root), fn, { ensureParent: () => {
289
+ ensureWorkspaceStorage(root);
290
+ mkdirSync(dirname(file), { recursive: true });
291
+ } });
292
+ }
293
+ function writeWorkspaceState(root, patch) {
294
+ return updateWorkspaceState(root, (state) => ({
295
+ ...state,
296
+ ...patch
297
+ }));
298
+ }
299
+ function updateWorkspaceState(root, update) {
300
+ return withWorkspaceStateLock(root, () => replaceWorkspaceState(root, update(readWorkspaceState(root) ?? {})));
301
+ }
302
+ function replaceWorkspaceState(root, state) {
303
+ const file = workspaceStateFile(root);
304
+ mkdirSync(dirname(file), { recursive: true });
305
+ const tmp = `${file}.tmp-${process.pid}`;
306
+ writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { mode: 384 });
307
+ renameSync(tmp, file);
308
+ return state;
309
+ }
310
+ function clearWorkspaceStateKeys(root, keys) {
311
+ if (!existsSync(workspaceStateFile(root))) return;
312
+ withWorkspaceStateLock(root, () => {
313
+ const state = readWorkspaceState(root);
314
+ const file = workspaceStateFile(root);
315
+ if (!state) {
316
+ try {
317
+ rmSync(file, { force: true });
318
+ } catch {}
319
+ return;
320
+ }
321
+ let changed = false;
322
+ for (const key of keys) {
323
+ if (!(key in state)) continue;
324
+ delete state[key];
325
+ changed = true;
326
+ }
327
+ if (!changed) return;
328
+ if (Object.keys(state).length === 0) {
329
+ rmSync(file, { force: true });
330
+ return;
331
+ }
332
+ replaceWorkspaceState(root, state);
333
+ });
334
+ }
335
+ function clearWorkspaceStateKey(root, key, shouldClear) {
336
+ return withWorkspaceStateLock(root, () => {
337
+ const state = readWorkspaceState(root);
338
+ if (!state || !(key in state)) return true;
339
+ if (!shouldClear(state[key])) return false;
340
+ delete state[key];
341
+ const file = workspaceStateFile(root);
342
+ if (Object.keys(state).length === 0) {
343
+ try {
344
+ rmSync(file, { force: true });
345
+ } catch {}
346
+ return true;
347
+ }
348
+ replaceWorkspaceState(root, state);
349
+ return true;
350
+ });
351
+ }
352
+ /** Records a command's use of the workspace, and the agent session whose shell ran it when there is one. */
353
+ function recordWorkspaceUse(root, now = /* @__PURE__ */ new Date(), env = process.env) {
354
+ const agent = agentFromEnv(env);
355
+ let cwd = process.cwd();
356
+ try {
357
+ cwd = realpathSync(cwd);
358
+ } catch {}
359
+ try {
360
+ writeWorkspaceState(root, {
361
+ lastUsedAt: now.toISOString(),
362
+ ...agent ? { [WORKSPACE_AGENT_KEY]: {
363
+ ...agent,
364
+ cwd,
365
+ lastActiveAt: now.toISOString()
366
+ } } : {}
367
+ });
368
+ } catch {}
369
+ }
370
+ //#endregion
371
+ export { workspaceStateFile as C, discoverAgentSessions as D, attributeAgentSessions as E, workspaceName$1 as S, agentKey as T, workspaceDerivedData as _, updateWorkspaceState as a, workspaceLogErrorIndex as b, writeWorkspaceState as c, sharedBuildCache as d, sharedCcache$1 as f, workspaceAgentDeviceDir as g, supervisorPidFile as h, recordWorkspaceUse as i, emulatorLogFile as l, supervisorLogFile as m, clearWorkspaceStateKeys as n, withWorkspaceStateLock as o, sharedCompilationCache$1 as p, readWorkspaceState$1 as r, workspaceLastUsed as s, clearWorkspaceStateKey as t, ensureWorkspaceStorage as u, workspaceDir as v, withDirLock$1 as w, workspaceLogsDir$1 as x, workspaceId as y };
@@ -0,0 +1,306 @@
1
+ import { M as isOnMountedVolume, N as listMountedVolumes, P as measuredDirectorySize, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix, j as formatBytes } from "./config-D0DbpZtM.mjs";
2
+ import { S as workspaceName, s as workspaceLastUsed } from "./workspace-state-B8-aJ1_5.mjs";
3
+ import { i as recordGcResult, n as withIdleWorkspace, o as canonicalPath, r as workspaceInUse, t as emptyWorkspaceDir } from "./in-use-DvBw0Kxo.mjs";
4
+ import { a as teardownOwnedBrowser } from "./teardown-CL6vDs2Y.mjs";
5
+ import { existsSync, lstatSync, readdirSync, rmSync, statSync } from "fs";
6
+ import { basename, isAbsolute, join, relative } from "path";
7
+ import { isJsonObject, readJsonFile } from "@stim-cli/core/state";
8
+ import chalk from "chalk";
9
+ //#region src/commands/gc/workspaces.ts
10
+ function workspacesRoot() {
11
+ return join(getConfigDir(), "workspaces");
12
+ }
13
+ function isInsideWorkspaces(dir) {
14
+ const rel = relative(canonicalPath(workspacesRoot()), canonicalPath(dir));
15
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
16
+ }
17
+ function readRecordedRoot(dir, name) {
18
+ let parsed;
19
+ try {
20
+ parsed = readJsonFile(join(dir, "workspace.json"));
21
+ } catch (error) {
22
+ return {
23
+ projectRoot: null,
24
+ problem: error?.code === "ENOENT" ? "it has no workspace.json" : "its workspace.json does not parse"
25
+ };
26
+ }
27
+ const projectRoot = isJsonObject(parsed) ? parsed.projectRoot : void 0;
28
+ if (typeof projectRoot !== "string" || !isAbsolute(projectRoot)) return {
29
+ projectRoot: null,
30
+ problem: "its workspace.json records no absolute project root"
31
+ };
32
+ if (workspaceName(projectRoot) !== name) return {
33
+ projectRoot: null,
34
+ problem: `its workspace.json records ${projectRoot}, whose workspace directory has another name`
35
+ };
36
+ return {
37
+ projectRoot,
38
+ problem: null
39
+ };
40
+ }
41
+ function listWorkspaceDirs() {
42
+ const root = workspacesRoot();
43
+ let names;
44
+ try {
45
+ names = readdirSync(root).toSorted();
46
+ } catch {
47
+ return [];
48
+ }
49
+ const entries = [];
50
+ for (const name of names) {
51
+ const dir = join(root, name);
52
+ try {
53
+ if (!statSync(dir).isDirectory()) continue;
54
+ } catch {
55
+ continue;
56
+ }
57
+ entries.push({
58
+ dir,
59
+ ...readRecordedRoot(dir, name)
60
+ });
61
+ }
62
+ return entries;
63
+ }
64
+ function classifyWorkspaceDirs(entries, { registryKeys, exists, isMounted, inUse }) {
65
+ const orphaned = [];
66
+ const skipped = [];
67
+ for (const { dir, projectRoot, problem } of entries) {
68
+ if (projectRoot === null) {
69
+ skipped.push({
70
+ dir,
71
+ reason: `workspace directory not resolved: ${problem ?? "unknown project root"}`
72
+ });
73
+ continue;
74
+ }
75
+ const present = exists(projectRoot);
76
+ if (present === null) {
77
+ skipped.push({
78
+ dir,
79
+ reason: `cannot tell whether its project root ${projectRoot} still exists`
80
+ });
81
+ continue;
82
+ }
83
+ if (present) continue;
84
+ if (registryKeys.some((key) => isPathPrefix(projectRoot, key))) continue;
85
+ if (!isMounted(projectRoot)) {
86
+ skipped.push({
87
+ dir,
88
+ reason: `the volume of its project root ${projectRoot} is not mounted`
89
+ });
90
+ continue;
91
+ }
92
+ const reasons = inUse(projectRoot);
93
+ if (reasons.length) {
94
+ skipped.push({
95
+ dir,
96
+ reason: `its project root ${projectRoot} is gone, but ${reasons.join("; ")}`
97
+ });
98
+ continue;
99
+ }
100
+ orphaned.push({
101
+ dir,
102
+ projectRoot
103
+ });
104
+ }
105
+ return {
106
+ orphaned,
107
+ skipped
108
+ };
109
+ }
110
+ function rootPresence(path) {
111
+ try {
112
+ lstatSync(path);
113
+ return true;
114
+ } catch (error) {
115
+ const code = error?.code;
116
+ return code === "ENOENT" || code === "ENOTDIR" ? false : null;
117
+ }
118
+ }
119
+ function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure = true } = {}) {
120
+ const classified = classifyWorkspaceDirs(listWorkspaceDirs(), {
121
+ registryKeys,
122
+ exists: rootPresence,
123
+ isMounted: (path) => isOnMountedVolume(path, mountedVolumes),
124
+ inUse: (root) => workspaceInUse(root)
125
+ });
126
+ return {
127
+ orphaned: measure ? classified.orphaned.map((entry) => Object.assign({}, entry, { bytes: sizeOf(entry.dir) })) : classified.orphaned,
128
+ skipped: classified.skipped
129
+ };
130
+ }
131
+ const REBUILD_COST = "The next build of an unchanged app installs from the shared build cache; after a native change the compilation cache speeds the rebuild, but on React Native 0.86 Swift does not use it (explicit modules are off), so that build recompiles Swift. The next stim macos in a cleared workspace is a full Swift build.";
132
+ const WORKSPACE_OUTPUT_DIRS = [
133
+ "derived-data",
134
+ "gradle-build",
135
+ "android-cas",
136
+ "cache-provider"
137
+ ];
138
+ const DAY_MS = 864e5;
139
+ function planWorkspaceOutputs(entries, { olderThan, now }) {
140
+ return entries.map(({ dir, projectRoot, problem, bytes, lastUsed, inUse }) => {
141
+ const idleDays = Number.isFinite(lastUsed) ? Math.max(0, Math.floor((now - lastUsed) / DAY_MS)) : null;
142
+ const [keptCode, keptReason] = projectRoot === null ? ["unresolved", `workspace directory not resolved: ${problem ?? "unknown project root"}`] : inUse.length ? ["in-use", `in use: ${inUse.join("; ")}`] : olderThan === null ? [null, null] : idleDays === null ? ["last-use-unknown", "its last use is unknown"] : idleDays < olderThan ? ["recently-used", `used ${idleDays}d ago, within --older-than ${olderThan}`] : [null, null];
143
+ return {
144
+ dir,
145
+ projectRoot,
146
+ bytes,
147
+ idleDays,
148
+ willClear: keptCode === null,
149
+ keptCode,
150
+ keptReason
151
+ };
152
+ });
153
+ }
154
+ const SIZE_TIMEOUT_MS = 6e4;
155
+ function sizeOf(dir, timeoutMs = SIZE_TIMEOUT_MS) {
156
+ return measuredDirectorySize(dir, { timeoutMs });
157
+ }
158
+ function outputPaths(dir) {
159
+ const paths = WORKSPACE_OUTPUT_DIRS.map((name) => join(dir, name)).filter((path) => existsSync(path));
160
+ const macos = join(dir, "macos");
161
+ try {
162
+ if (!lstatSync(macos).isDirectory()) return paths;
163
+ for (const entry of readdirSync(macos, { withFileTypes: true })) if (entry.name === "build" || entry.name.endsWith(".app") || entry.name.startsWith("staging-") && entry.isDirectory()) paths.push(join(macos, entry.name));
164
+ } catch {}
165
+ return paths;
166
+ }
167
+ function outputBytes(paths, timeoutMs) {
168
+ let total = 0;
169
+ for (const path of paths) {
170
+ const size = sizeOf(path, timeoutMs);
171
+ if (size === null) return null;
172
+ total += size;
173
+ }
174
+ return total;
175
+ }
176
+ function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS }) {
177
+ const mountedVolumes = listMountedVolumes();
178
+ const entries = listWorkspaceDirs().filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { paths: outputPaths(entry.dir) })).filter((entry) => entry.paths.length > 0).map(({ paths, ...entry }) => Object.assign({}, entry, { bytes: measure ? outputBytes(paths, sizeTimeoutMs) : null })).map((entry) => Object.assign({}, entry, {
179
+ lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot),
180
+ inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
181
+ }));
182
+ return {
183
+ root: workspacesRoot(),
184
+ workspaces: planWorkspaceOutputs(entries, {
185
+ olderThan,
186
+ now
187
+ })
188
+ };
189
+ }
190
+ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
191
+ let failures = 0;
192
+ let cleared = 0;
193
+ const clearedRoots = [];
194
+ for (const entry of report.workspaces) {
195
+ const root = entry.projectRoot;
196
+ if (!entry.willClear || root === null) {
197
+ if (entry.keptReason) {
198
+ console.log(chalk.dim(`Kept the build outputs of ${root ?? entry.dir}: ${entry.keptReason}`));
199
+ recordGcResult("workspaceOutputs", "kept", root ?? entry.dir, {
200
+ bytes: entry.bytes,
201
+ detail: entry.keptReason
202
+ });
203
+ }
204
+ continue;
205
+ }
206
+ let run;
207
+ try {
208
+ run = await withIdleWorkspace(root, () => {
209
+ const [again] = planWorkspaceOutputs([{
210
+ ...entry,
211
+ problem: null,
212
+ lastUsed: workspaceLastUsed(root),
213
+ inUse: []
214
+ }], {
215
+ olderThan,
216
+ now
217
+ });
218
+ if (again?.keptReason) return again.keptReason;
219
+ for (const path of outputPaths(entry.dir)) rmSync(path, {
220
+ recursive: true,
221
+ force: true
222
+ });
223
+ return null;
224
+ }, { purpose: "gc" });
225
+ } catch (error) {
226
+ failures++;
227
+ console.log(chalk.red(`Could not clear the build outputs of ${root}: ${error?.message || String(error)}`));
228
+ recordGcResult("workspaceOutputs", "failed", root, {
229
+ bytes: entry.bytes,
230
+ detail: error?.message || String(error)
231
+ });
232
+ continue;
233
+ }
234
+ const kept = run.ran ? run.value : `in use: ${run.reasons.join("; ")}`;
235
+ if (kept) {
236
+ console.log(chalk.yellow(`Kept the build outputs of ${root}: ${kept}`));
237
+ recordGcResult("workspaceOutputs", "kept", root, {
238
+ bytes: entry.bytes,
239
+ detail: kept
240
+ });
241
+ continue;
242
+ }
243
+ cleared += entry.bytes ?? 0;
244
+ clearedRoots.push(root);
245
+ console.log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
246
+ recordGcResult("workspaceOutputs", "done", root, { bytes: entry.bytes });
247
+ }
248
+ if (cleared) console.log(chalk.dim(`Cleared ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
249
+ return {
250
+ failures,
251
+ cleared: clearedRoots
252
+ };
253
+ }
254
+ function stillOrphaned({ dir, projectRoot }) {
255
+ if (readRecordedRoot(dir, basename(dir)).projectRoot !== projectRoot || rootPresence(projectRoot) !== false || !isOnMountedVolume(projectRoot)) return false;
256
+ return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix(projectRoot, key));
257
+ }
258
+ async function deleteOrphanedWorkspaces(orphaned) {
259
+ let failures = 0;
260
+ for (const entry of orphaned) {
261
+ if (!existsSync(entry.dir)) continue;
262
+ let run;
263
+ try {
264
+ run = await withIdleWorkspace(entry.projectRoot, async () => {
265
+ if (!stillOrphaned(entry)) return false;
266
+ const browser = await teardownOwnedBrowser(entry.projectRoot, { deleteProfile: true });
267
+ if (browser.status === "failed" || browser.status === "skipped") throw new Error(`its owned Chrome profile was kept: ${browser.reason}`);
268
+ emptyWorkspaceDir(entry.dir);
269
+ return true;
270
+ }, { purpose: "gc" });
271
+ } catch (error) {
272
+ failures++;
273
+ console.log(chalk.red(`Could not remove ${entry.dir}: ${error?.message || error}`));
274
+ recordGcResult("workspaceDirectory", "failed", entry.dir, {
275
+ bytes: entry.bytes ?? null,
276
+ detail: error?.message || String(error)
277
+ });
278
+ continue;
279
+ }
280
+ if (!run.ran) {
281
+ console.log(chalk.yellow(`Kept ${entry.dir}: ${run.reasons.join("; ")}`));
282
+ recordGcResult("workspaceDirectory", "kept", entry.dir, {
283
+ bytes: entry.bytes ?? null,
284
+ detail: run.reasons.join("; ")
285
+ });
286
+ continue;
287
+ }
288
+ if (!run.value) {
289
+ console.log(chalk.yellow(`Kept ${entry.dir}: it can no longer be confirmed orphaned.`));
290
+ recordGcResult("workspaceDirectory", "kept", entry.dir, {
291
+ bytes: entry.bytes ?? null,
292
+ detail: "it can no longer be confirmed orphaned"
293
+ });
294
+ continue;
295
+ }
296
+ const size = entry.bytes === void 0 ? "" : ` (${sizeText(entry.bytes)})`;
297
+ console.log(chalk.green(`Removed the orphaned workspace directory ${entry.dir}${size}`));
298
+ recordGcResult("workspaceDirectory", "done", entry.dir, { bytes: entry.bytes ?? null });
299
+ }
300
+ return failures;
301
+ }
302
+ function sizeText(bytes) {
303
+ return bytes === null ? "size unknown" : formatBytes(bytes);
304
+ }
305
+ //#endregion
306
+ export { collectWorkspaceOutputs as a, listWorkspaceDirs as c, collectOrphanedWorkspaces as i, rootPresence as l, WORKSPACE_OUTPUT_DIRS as n, deleteOrphanedWorkspaces as o, clearWorkspaceOutputs as r, isInsideWorkspaces as s, REBUILD_COST as t, sizeText as u };