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.
- package/README.md +2 -0
- package/dist/activity-BxwdD81S.mjs +609 -0
- package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
- package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
- package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
- package/dist/android-cas-8U4DPqeR.mjs +167 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
- package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
- package/dist/attempt-Bk2ouQVL.mjs +39 -0
- package/dist/budget-DjrURfQn.mjs +2700 -0
- package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
- package/dist/build-slots-rUk77xqh.mjs +132 -0
- package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
- package/dist/cli-BkYZ_6hy.mjs +51 -0
- package/dist/cli.mjs +6 -42
- package/dist/client-BTY686R6.mjs +1368 -0
- package/dist/collector-run.mjs +9 -195
- package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
- package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
- package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
- package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
- package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
- package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
- package/dist/dev-client-CroAIjjj.mjs +43 -0
- package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
- package/dist/device-capacity-BHwxNBlu.mjs +136 -0
- package/dist/device-host-worker.d.mts +1 -0
- package/dist/device-host-worker.mjs +964 -0
- package/dist/device-ios-BadmeL2V.mjs +381 -0
- package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
- package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
- package/dist/device-pool-CUaEWMMm.mjs +125 -0
- package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
- package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
- package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
- package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
- package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
- package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
- package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
- package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
- package/dist/hosted-client-a-WmqE2L.mjs +197 -0
- package/dist/hosted-ios-CIWelIqf.mjs +220 -0
- package/dist/hosted-logs-D5g041Q3.mjs +187 -0
- package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
- package/dist/idle-DYVymG92.mjs +1099 -0
- package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
- package/dist/in-use-DvBw0Kxo.mjs +130 -0
- package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
- package/dist/ios-Cr1h5gnO.mjs +189 -0
- package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
- package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
- package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
- package/dist/ios-state-DS5UpYTH.mjs +48 -0
- package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
- package/dist/logs-CHEVK8zD.mjs +371 -0
- package/dist/machines-BIBUdhpj.mjs +265 -0
- package/dist/macos-CSYEAFmF.mjs +473 -0
- package/dist/macos-run.d.mts +5 -0
- package/dist/macos-run.mjs +2 -0
- package/dist/maintenance-run.d.mts +4 -0
- package/dist/maintenance-run.mjs +254 -0
- package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
- package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
- package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
- package/dist/native-run-NaBrRFtQ.mjs +147 -0
- package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
- package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
- package/dist/offload-worker.d.mts +122 -0
- package/dist/offload-worker.mjs +606 -0
- package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
- package/dist/ownership-hCkPzGmr.mjs +571 -0
- package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
- package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
- package/dist/prebuild-D2fsca_Z.mjs +213 -0
- package/dist/preview-DhVbvPAZ.mjs +318 -0
- package/dist/process-identity-BmRm1qV0.mjs +12 -0
- package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
- package/dist/pull-request-pxnDCQzn.mjs +322 -0
- package/dist/pull-requests.d.mts +120 -0
- package/dist/pull-requests.mjs +2 -0
- package/dist/recordings-BIXOzVnr.mjs +37 -0
- package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
- package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
- package/dist/run-f4vI6EnK.mjs +119 -0
- package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
- package/dist/server-expo-CVqO2c35.mjs +2 -0
- package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
- package/dist/settings-BBpMcDbD.mjs +25 -0
- package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
- package/dist/settings-oTSkkNrM.mjs +1439 -0
- package/dist/settings.schema.json +845 -97
- package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
- package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
- package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
- package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
- package/dist/stage-CzaiwFsI.mjs +143 -0
- package/dist/start-CX8Ig-U2.mjs +2 -0
- package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
- package/dist/state-3DwSQIBK.mjs +41 -0
- package/dist/state-B4PQFXMW.d.mts +12 -0
- package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
- package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
- package/dist/stats-CEZ_i5je.mjs +77 -0
- package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
- package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
- package/dist/status-DlKvVJBE.mjs +49 -0
- package/dist/stdout-reader-C8yjre1x.mjs +23 -0
- package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
- package/dist/stim-footprint.swift +32 -0
- package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
- package/dist/stop-B0Yxtmiy.mjs +48 -0
- package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
- package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
- package/dist/stop-cause-CidxIqup.mjs +97 -0
- package/dist/supervisor-run.d.mts +43 -11
- package/dist/supervisor-run.mjs +363 -24
- package/dist/support-CAP8qDUF.mjs +316 -0
- package/dist/support-D4aYJniO.mjs +201 -0
- package/dist/teardown-CL6vDs2Y.mjs +727 -0
- package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
- package/dist/trigger-Bemu8ytw.mjs +65 -0
- package/dist/trigger-CXts6qt1.mjs +2 -0
- package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
- package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
- package/dist/web-run.mjs +310 -13
- package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
- package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
- package/dist/workspaces-BV4pdHFz.mjs +306 -0
- package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
- package/dist/worktree-jnG8Iph5.mjs +2 -0
- package/package.json +10 -5
- package/shim/bundle-response.cjs +28 -12
- package/dist/activity-CYVDcrt3.mjs +0 -356
- package/dist/budget-CGDkhvv_.mjs +0 -1745
- package/dist/build-progress-DxKpwnaS.mjs +0 -247
- package/dist/idle-DXtx3RbX.mjs +0 -191
- package/dist/in-use-DlqBDKqY.mjs +0 -249
- package/dist/logs-Civ4gbBH.mjs +0 -486
- package/dist/merge-state-ytFQCeh4.mjs +0 -262
- package/dist/server-expo-DIHDhvGg.mjs +0 -2
- package/dist/settings-D61iNOsB.mjs +0 -629
- package/dist/start-Cetfvy8l.mjs +0 -2
- package/dist/stats-CFpFGlJV.mjs +0 -62
- package/dist/stats-CX1Z8Tca.mjs +0 -368
- package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
- package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
- package/dist/worktree-BVrHsYZG.mjs +0 -752
- package/dist/worktree-CHWWvm5v.mjs +0 -2
- 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 };
|