ccdeck 3.29.10 → 3.30.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 (176) hide show
  1. package/README.md +1 -1
  2. package/bin/agent-dag.js +67 -118
  3. package/bin/cli/help.js +67 -0
  4. package/bin/cli/login-item.js +182 -0
  5. package/bin/cli/one-shot.js +254 -0
  6. package/bin/cli/package.js +39 -0
  7. package/bin/cli/pulse.js +98 -0
  8. package/bin/cli/restart.js +179 -0
  9. package/bin/cli/screen.js +166 -0
  10. package/bin/cli/second-start.js +120 -0
  11. package/bin/cli/startup.js +459 -0
  12. package/bin/cli/uninstall.js +169 -0
  13. package/bin/deck.js +112 -1559
  14. package/dist/web/assets/BrowserWatchModal-DP3YMtEi.js +1 -0
  15. package/dist/web/assets/UsageHistoryModal-CcdPVXAw.js +1 -0
  16. package/dist/web/assets/index-3vS6A0vt.css +1 -0
  17. package/dist/web/assets/index-BVTWHDta.js +298 -0
  18. package/dist/web/index.html +2 -2
  19. package/hook/hook.js +15 -14
  20. package/package.json +1 -1
  21. package/release-notes.json +28 -0
  22. package/src/server/account-projects.mjs +25 -11
  23. package/src/server/account-routes.mjs +254 -0
  24. package/src/server/activity.mjs +3 -3
  25. package/src/server/append-budget.mjs +185 -0
  26. package/src/server/append-failures.mjs +147 -0
  27. package/src/server/args.mjs +37 -5
  28. package/src/server/atomic-write.mjs +300 -0
  29. package/src/server/block-notify.mjs +0 -5
  30. package/src/server/boot-lock.mjs +1 -1
  31. package/src/server/brand.mjs +7 -6
  32. package/src/server/browser-react.mjs +10 -2
  33. package/src/server/browser-watch-decks.mjs +146 -0
  34. package/src/server/browser-watch-feed.mjs +69 -0
  35. package/src/server/browser-watch-log.mjs +173 -0
  36. package/src/server/browser-watch-mtime.mjs +18 -0
  37. package/src/server/browser-watch-relay.mjs +112 -0
  38. package/src/server/browser-watch-routes.mjs +172 -0
  39. package/src/server/browser-watch-seen.mjs +127 -0
  40. package/src/server/browser-watch-store.mjs +14 -143
  41. package/src/server/browser-watch.mjs +278 -626
  42. package/src/server/canonical-path.mjs +150 -0
  43. package/src/server/ccusage-failure.mjs +64 -0
  44. package/src/server/ccusage-install.mjs +459 -0
  45. package/src/server/ccusage-runner.mjs +271 -0
  46. package/src/server/ccusage.mjs +21 -756
  47. package/src/server/claude-accounts.mjs +220 -464
  48. package/src/server/claude-collector.mjs +189 -0
  49. package/src/server/claude-dir.mjs +2 -2
  50. package/src/server/claude-identity.mjs +5 -4
  51. package/src/server/claude-reset-credits.mjs +130 -0
  52. package/src/server/claude-verdicts.mjs +174 -0
  53. package/src/server/clear-route.mjs +191 -0
  54. package/src/server/codex-auth.mjs +4 -4
  55. package/src/server/codex-dir.mjs +52 -8
  56. package/src/server/codex-enrichment.mjs +310 -0
  57. package/src/server/codex-quota.mjs +5 -3
  58. package/src/server/codex-translate.mjs +407 -0
  59. package/src/server/codex-usage.mjs +8 -5
  60. package/src/server/codex-watch.mjs +496 -0
  61. package/src/server/cswap-auto-loop.mjs +342 -0
  62. package/src/server/cswap-auto-readers.mjs +303 -0
  63. package/src/server/cswap-auto.mjs +51 -609
  64. package/src/server/cswap-install.mjs +69 -265
  65. package/src/server/cswap-layout.mjs +242 -0
  66. package/src/server/deck-home.mjs +1 -1
  67. package/src/server/deck-prefs.mjs +51 -184
  68. package/src/server/deck-scope.mjs +52 -0
  69. package/src/server/desktop-update-routes.mjs +75 -0
  70. package/src/server/detach.mjs +78 -0
  71. package/src/server/discovery.mjs +298 -0
  72. package/src/server/event-log.mjs +328 -0
  73. package/src/server/event-pipeline.mjs +235 -0
  74. package/src/server/event-ring.mjs +214 -0
  75. package/src/server/event-routes.mjs +352 -0
  76. package/src/server/event-sink.mjs +26 -0
  77. package/src/server/exec-children.mjs +128 -0
  78. package/src/server/exec-not-found.mjs +213 -0
  79. package/src/server/exec-output.mjs +23 -0
  80. package/src/server/exec-spec.mjs +341 -0
  81. package/src/server/exec.mjs +74 -661
  82. package/src/server/health-route.mjs +42 -0
  83. package/src/server/hook-challenge.mjs +37 -0
  84. package/src/server/http-io.mjs +104 -0
  85. package/src/server/index.mjs +172 -7620
  86. package/src/server/install-layout.mjs +506 -0
  87. package/src/server/installer.mjs +79 -608
  88. package/src/server/jsonl-chunks.mjs +139 -0
  89. package/src/server/lan-accounts.mjs +27 -0
  90. package/src/server/lan-beacon.mjs +457 -0
  91. package/src/server/lan-call.mjs +239 -0
  92. package/src/server/lan-copies.mjs +270 -0
  93. package/src/server/lan-deck.mjs +583 -0
  94. package/src/server/lan-dials.mjs +161 -0
  95. package/src/server/lan-engine.mjs +475 -1024
  96. package/src/server/lan-hearing.mjs +49 -0
  97. package/src/server/lan-inbound.mjs +100 -0
  98. package/src/server/lan-invite-offer.mjs +77 -0
  99. package/src/server/lan-invite.mjs +207 -0
  100. package/src/server/lan-lines.mjs +73 -0
  101. package/src/server/lan-manifest.mjs +74 -0
  102. package/src/server/lan-reach.mjs +82 -69
  103. package/src/server/lan-requests.mjs +249 -0
  104. package/src/server/lan-round-record.mjs +45 -0
  105. package/src/server/lan-round-timer.mjs +62 -0
  106. package/src/server/lan-routes.mjs +143 -0
  107. package/src/server/lan-serve.mjs +112 -0
  108. package/src/server/lan-socket.mjs +243 -925
  109. package/src/server/lan-sync.mjs +58 -1026
  110. package/src/server/lan-tailnet-poll.mjs +57 -0
  111. package/src/server/lan-turns.mjs +80 -0
  112. package/src/server/lan-wire.mjs +620 -0
  113. package/src/server/lifecycle.mjs +421 -0
  114. package/src/server/listen.mjs +110 -0
  115. package/src/server/live-decks.mjs +138 -0
  116. package/src/server/load-average.mjs +32 -0
  117. package/src/server/log-election.mjs +235 -0
  118. package/src/server/log-replay.mjs +374 -0
  119. package/src/server/log-writer.mjs +72 -557
  120. package/src/server/login-service.mjs +3 -2
  121. package/src/server/macmon.mjs +1 -1
  122. package/src/server/memory-files.mjs +163 -0
  123. package/src/server/memory-metrics.mjs +178 -0
  124. package/src/server/metrics-history.mjs +94 -0
  125. package/src/server/metrics-run.mjs +100 -0
  126. package/src/server/music-routes.mjs +134 -0
  127. package/src/server/network-metrics.mjs +5 -3
  128. package/src/server/network-sampler.mjs +256 -0
  129. package/src/server/npm-latest.mjs +385 -0
  130. package/src/server/npm-upgrade.mjs +352 -0
  131. package/src/server/npx.mjs +41 -7
  132. package/src/server/output-watch.mjs +5 -4
  133. package/src/server/pinned-build.mjs +108 -0
  134. package/src/server/prefs-path.mjs +14 -0
  135. package/src/server/prefs-refusal.mjs +77 -0
  136. package/src/server/prefs-routes.mjs +76 -0
  137. package/src/server/prefs-set-aside.mjs +86 -0
  138. package/src/server/prefs-state.mjs +51 -0
  139. package/src/server/process-command.mjs +104 -0
  140. package/src/server/process-list.mjs +547 -0
  141. package/src/server/pulse-line.mjs +132 -0
  142. package/src/server/purge-key.mjs +7 -7
  143. package/src/server/quota-cli.mjs +174 -0
  144. package/src/server/quota-oauth.mjs +190 -0
  145. package/src/server/quota-shape.mjs +229 -0
  146. package/src/server/quota-store-resets.mjs +206 -0
  147. package/src/server/quota.mjs +123 -509
  148. package/src/server/relay-guard.mjs +3 -3
  149. package/src/server/request-gates.mjs +441 -0
  150. package/src/server/restart-note.mjs +180 -0
  151. package/src/server/retire-sound-hook.mjs +3 -3
  152. package/src/server/ring-bounds.mjs +215 -0
  153. package/src/server/running-deck.mjs +1 -1
  154. package/src/server/running-version.mjs +24 -0
  155. package/src/server/self-update.mjs +39 -1371
  156. package/src/server/session-enrichment.mjs +622 -0
  157. package/src/server/session-read-gate.mjs +49 -0
  158. package/src/server/session-tracking.mjs +242 -0
  159. package/src/server/sse-clients.mjs +222 -0
  160. package/src/server/stale-discovery.mjs +30 -0
  161. package/src/server/static-serve.mjs +81 -0
  162. package/src/server/supervisor.mjs +73 -12
  163. package/src/server/system-metrics.mjs +169 -1935
  164. package/src/server/term.mjs +11 -249
  165. package/src/server/thermal-metrics.mjs +520 -0
  166. package/src/server/thermal-sampler.mjs +204 -0
  167. package/src/server/token-redact.mjs +123 -0
  168. package/src/server/transcript-gate.mjs +107 -0
  169. package/src/server/transcript-scan.mjs +563 -0
  170. package/src/server/usage-routes.mjs +93 -0
  171. package/src/server/uv-bootstrap.mjs +1 -1
  172. package/src/server/wordmark.mjs +127 -0
  173. package/dist/web/assets/BrowserWatchModal-CMn28FHb.js +0 -1
  174. package/dist/web/assets/UsageHistoryModal-C-sPhsKT.js +0 -1
  175. package/dist/web/assets/index-C1YFI9f4.js +0 -298
  176. package/dist/web/assets/index-Ck2RAla3.css +0 -1
@@ -0,0 +1,76 @@
1
+ // The deck's settings routes: GET /api/prefs, which answers what a page may see
2
+ // of them and what the machine allows, and POST /api/prefs, which writes a
3
+ // patch and hands the result to the LAN engine.
4
+ //
5
+ // These lived in src/server/index.mjs, beside the desktop notifier that reads
6
+ // the same settings, and moved once the settings had (prefs-state.mjs) and the
7
+ // engine had (lan-deck.mjs): what is left of them is a body read, a write
8
+ // through the one, a push to the other, and an answer. The notifier stays in
9
+ // index.mjs, beside the desktop-app connections it notifies through.
10
+ import { notificationsOn, notificationsVetoed, prefsRefusalDetail, prefsWriteRefusal, publicPrefs } from "./deck-prefs.mjs";
11
+ import { PRODUCT } from "./brand.mjs";
12
+ import { heldPrefs } from "./prefs-state.mjs";
13
+ import { applyLanPrefs, forgetReach } from "./lan-deck.mjs";
14
+ import { readBody, send } from "./http-io.mjs";
15
+
16
+ /**
17
+ * GET the deck's settings, and what the machine is allowing.
18
+ *
19
+ * THREE fields, not two, and the third is the one a first attempt got wrong.
20
+ * `notificationsAllowed` is the effective answer, after the environment
21
+ * variable has had its say — but "false" there means either "the user switched
22
+ * it off" or "the machine forbids it", and the menu has to say a different
23
+ * sentence for each. Deriving the second from the first made the switch read
24
+ * "off — set at launch" the moment anybody turned it off on a deck launched
25
+ * with no variable at all, which is the deck telling the user their own press
26
+ * was somebody else's doing. `notificationsVetoed` answers only the machine's
27
+ * half.
28
+ */
29
+ function prefsPayload() {
30
+ const prefs = heldPrefs.current();
31
+ return {
32
+ ok: true,
33
+ // `publicPrefs`, never the held prefs whole. This route answers anything
34
+ // that can reach the loopback port, and the LAN group passphrase is in the
35
+ // stored object — one field, and sending it here would put it in front of
36
+ // every page on the machine. What goes out is whether one is set.
37
+ prefs: publicPrefs(prefs),
38
+ notificationsAllowed: notificationsOn(prefs),
39
+ notificationsVetoed: notificationsVetoed(),
40
+ };
41
+ }
42
+
43
+ export function handlePrefsRead(req, res) {
44
+ return send(res, 200, prefsPayload());
45
+ }
46
+
47
+ /** POST a patch. Fields nobody sent keep their value — see writePrefs. */
48
+ export async function handlePrefsWrite(req, res) {
49
+ const raw = await readBody(req, res).catch(() => null);
50
+ let body = null;
51
+ try { body = JSON.parse(raw ?? ""); } catch { /* handled below */ }
52
+ if (!body || typeof body !== "object") return send(res, 400, { ok: false, reason: "bad_request" });
53
+ try {
54
+ await heldPrefs.write(body);
55
+ } catch (err) {
56
+ // A settings file or folder this user cannot reach is the machine's to fix,
57
+ // not the deck's, and `guard`'s bare 500 left the panel with nothing to say
58
+ // about it (#1335). Anything else is still a bug and still goes to `guard`.
59
+ const reason = prefsWriteRefusal(err);
60
+ if (!reason) throw err;
61
+ console.error(`${PRODUCT}: settings were not saved:`, err?.message ?? err);
62
+ // What blocked it, from closed sets and without the path, so a screenshot
63
+ // of the panel is enough to tell the cases apart (#1335).
64
+ const detail = prefsRefusalDetail(err);
65
+ return send(res, 500, detail ? { ok: false, reason, detail } : { ok: false, reason });
66
+ }
67
+ // The engine reads its settings from here rather than holding its own copy,
68
+ // so turning the switch off in the panel really does stop the sockets rather
69
+ // than only changing what the panel says.
70
+ await applyLanPrefs();
71
+ // Somebody has just switched this on and is watching the panel for the answer
72
+ // to one question: is anything going to turn up. Whatever the probe said
73
+ // before is about a deck whose sockets were down — see forgetReach.
74
+ if (body.lan?.enabled === true) forgetReach();
75
+ return send(res, 200, prefsPayload());
76
+ }
@@ -0,0 +1,86 @@
1
+ // Moving a prefs.json nobody can use out of the way, before anything can write
2
+ // over it: the bytes of a file that is not JSON (#1002), and a file another
3
+ // user owns (#1335). Moved out of deck-prefs.mjs unchanged; loadPrefs decides
4
+ // when, and this is how.
5
+ import { stat } from "node:fs/promises";
6
+ import { renameWithRetry } from "./atomic-write.mjs";
7
+ import { deckDataDir } from "./deck-home.mjs";
8
+ import { prefsDir, prefsPath } from "./prefs-path.mjs";
9
+
10
+ /** Where the bytes of a prefs.json nothing could parse are put.
11
+ *
12
+ * `Date.now()` rather than an ISO timestamp because a colon is not a legal
13
+ * filename character on Windows, and a quarantine that cannot be created on
14
+ * the platform it is protecting is not a quarantine. */
15
+ export const quarantinePath = (home = deckDataDir(), at = Date.now()) =>
16
+ `${prefsPath(home)}.corrupt-${at}`;
17
+
18
+ /** The read errors that can mean "not yours" rather than "broken". Only these
19
+ * send a read to setAsideForeign; EISDIR, EIO and EMFILE never do. */
20
+ export const DENIED = new Set(["EACCES", "EPERM"]);
21
+
22
+ /** Where a prefs.json another user owns is put. Beside the file, like
23
+ * quarantinePath, and stamped in milliseconds for the same Windows reason —
24
+ * but a name of its own, because nothing is wrong with these bytes. */
25
+ export const foreignPath = (home = deckDataDir(), at = Date.now()) =>
26
+ `${prefsPath(home)}.foreign-${at}`;
27
+
28
+ /**
29
+ * Move a prefs.json that ANOTHER USER OWNS out of the way, or say why not.
30
+ *
31
+ * #1335. A `sudo ccdeck` that kept the user's HOME writes a root-owned 0600
32
+ * prefs.json into the user's own folder. From then on every
33
+ * deck that user starts is refused the read, refuses every settings write on
34
+ * top of it — rightly — and so can never keep anything: a new LAN identity on
35
+ * every start, a share tick that says "Could not share that account", and no
36
+ * way out from inside the deck.
37
+ *
38
+ * WHY MOVING IT IS SAFE HERE WHEN IT IS NOT FOR "unreadable". The refusal
39
+ * exists so the deck never loses a key it could still have read. A file owned
40
+ * by somebody else is not one this user's deck will ever read — waiting does
41
+ * not help, and the deck is already running on defaults. A rename reads and
42
+ * changes nothing: owner and mode travel with the file, so the key in it stays
43
+ * private and whole, and `sudo chown` plus a move puts it back.
44
+ *
45
+ * Only when the owner is KNOWN to be somebody else. A file this user owns and
46
+ * still cannot read (a mode bit, a lock, a sandbox) is left exactly where it
47
+ * is, and so is anything on Windows, where there is no uid to compare and
48
+ * EPERM is usually another program holding the file.
49
+ *
50
+ * Answers `{ moved, uid }`, `{ gone }` when the file vanished under it, or,
51
+ * when it stayed, `{ why, owner, on }`: a sentence for the log, possibly empty,
52
+ * and who owns what blocked the read — "you", "other" or "unknown" — and
53
+ * whether that is the "file" or its "folder". The last two are what the panel
54
+ * is told (see prefsRefusalDetail); the sentence, which has the path, is not.
55
+ */
56
+ export async function setAsideForeign(path, home, deps) {
57
+ const uid = deps.getuid ? deps.getuid() : process.getuid?.();
58
+ if (uid === undefined) return { why: "", owner: "unknown", on: "file" };
59
+ const look = deps.stat ?? stat;
60
+ let st;
61
+ try {
62
+ st = await look(path);
63
+ } catch (err) {
64
+ if (err?.code === "ENOENT") return { gone: true };
65
+ // The folder itself is out of reach, so nothing in it can be looked at, let
66
+ // alone moved. Name the folder and its owner, which is what a person needs.
67
+ const folder = prefsDir(home);
68
+ const owner = await look(folder).then(s => s.uid, () => undefined);
69
+ if (owner === undefined) return { why: "", owner: "unknown", on: "folder" };
70
+ if (owner === uid) return { why: "", owner: "you", on: "folder" };
71
+ return {
72
+ why: ` The folder ${folder} belongs to another user (uid ${owner}), most likely from a run with sudo; sudo chown -R "$(id -un)" "${folder}" gives it back.`,
73
+ owner: "other",
74
+ on: "folder",
75
+ };
76
+ }
77
+ if (st.uid === uid) return { why: "", owner: "you", on: "file" };
78
+ const to = foreignPath(home);
79
+ try {
80
+ await (deps.rename ?? renameWithRetry)(path, to);
81
+ } catch (err) {
82
+ if (err?.code === "ENOENT") return { gone: true };
83
+ return { why: ` It belongs to another user (uid ${st.uid}) and could not be moved aside either: ${err?.message ?? err}.`, owner: "other", on: "file" };
84
+ }
85
+ return { moved: to, uid: st.uid };
86
+ }
@@ -0,0 +1,51 @@
1
+ // The deck's settings as this process holds them: the one in-memory copy every
2
+ // reader asks, and the one place a write's answer is kept.
3
+ //
4
+ // This was `let _prefs` in src/server/index.mjs, and it is the reason the LAN
5
+ // section could not leave that file. The notifier and the away-update read it;
6
+ // the settings route, the LAN peer route and six of the LAN engine's callbacks
7
+ // each assigned it back with `_prefs = await writePrefs(...)`; and a
8
+ // module-level `let` is one no other module can assign. So the variable moved
9
+ // first, behind the three things its callers actually do with it: read it,
10
+ // write a patch, and compute a patch from the file. Every write now goes
11
+ // through here, which is also what keeps the promise the comment below has
12
+ // always made — that the copy and the file cannot drift within one process —
13
+ // true by construction rather than by every caller remembering the assignment.
14
+ //
15
+ // The disk half is deck-prefs.mjs, unchanged: the queue, the merge and the
16
+ // refusal to write over a file it could not read all live there. This holds
17
+ // what they return and nothing else.
18
+ import { DEFAULTS, readPrefs, updatePrefs, writePrefs } from "./deck-prefs.mjs";
19
+
20
+ /**
21
+ * The deck's own settings, in memory, refreshed whenever they are written.
22
+ *
23
+ * Read once at boot and then kept here rather than read per event: `consider`
24
+ * is on the ingest path every hook event goes through, and a file read there
25
+ * would be a syscall per event to answer a question that changes when somebody
26
+ * presses a switch. The write path below updates this, so the in-memory copy
27
+ * and the file cannot drift within one process — and a second deck writing the
28
+ * file is picked up on ITS next write or this one's next boot, which is the
29
+ * same freshness every other cross-deck setting has.
30
+ */
31
+ let _prefs = { ...DEFAULTS };
32
+
33
+ // Read at import, so `consider` has its answer from the first event. LAN sync
34
+ // used to start from this same read, and it must not: bin/deck.js imports the
35
+ // server module to ASK whether a deck is already up, and a launcher that then
36
+ // attaches and exits had already bound the beacon and the sync port on the way
37
+ // in — which is the `lan sync (listen): EADDRINUSE` line a second `ccdeck`
38
+ // printed above `deck already running`. The engine starts from the listen that
39
+ // succeeds, in startServer, which is the only process that may hold a port.
40
+ export const prefsRead = readPrefs().then(p => { _prefs = p; }).catch(() => {});
41
+
42
+ export const heldPrefs = Object.freeze({
43
+ /** What the last read or write left here. Never a disk read — see above. */
44
+ current: () => _prefs,
45
+ /** `writePrefs(patch)`, and keep what it wrote. A write that throws keeps
46
+ * nothing, so the copy is never ahead of the file. */
47
+ write: async patch => (_prefs = await writePrefs(patch)),
48
+ /** `updatePrefs(mutate)`, and keep what it wrote: for a patch that has to be
49
+ * computed from the file rather than from `current()` — see updatePrefs. */
50
+ update: async mutate => (_prefs = await updatePrefs(mutate)),
51
+ });
@@ -0,0 +1,104 @@
1
+ // What of a process's command line the Machine panel may show: the arguments,
2
+ // with the executable that carries them taken off the front (commandTail), and
3
+ // then with the shapes a secret takes removed and the rest capped
4
+ // (redactCommand).
5
+ //
6
+ // Moved out of process-list.mjs unchanged. attachDetail there runs both on
7
+ // every candidate's argument vector before it leaves that module, because
8
+ // /api/system/processes answers anything that can reach the loopback port;
9
+ // process-list.mjs re-exports CMD_MAX, redactCommand and commandTail, which is
10
+ // where the suite reaches them.
11
+
12
+ /**
13
+ * An argument vector with the parts that must not be looked at taken out, and
14
+ * the parts nobody reads cut off.
15
+ *
16
+ * THIS IS A BLOCKLIST AND A BLOCKLIST LEAKS. It is written down here rather
17
+ * than implied, because the alternative — `comm`, which is what this replaced —
18
+ * leaked nothing and told the reader nothing either. What it removes is the
19
+ * shapes a secret actually takes on a command line; what it cannot remove is a
20
+ * secret that looks like an ordinary word, and no rule here pretends otherwise.
21
+ *
22
+ * The cap is the other half, and it is not a nicety. Measured on this machine:
23
+ * one Chrome helper's argv is 906 characters of `--field-trial-handle`,
24
+ * base64 `--gpu-preferences` and shared-memory handles. Forty of those is 36 KB
25
+ * on the wire every four seconds to render a column nobody can read. What
26
+ * identifies a process is its first few arguments — `ng serve admin-portal
27
+ * --port 44440` — and that is what fits.
28
+ */
29
+ export const CMD_MAX = 180;
30
+
31
+ const SECRET_FLAG =
32
+ /(-{1,2}[\w.-]*(?:token|password|passwd|secret|api[-_]?key|apikey|auth|credential|bearer|cookie|session[-_]?id|private[-_]?key)[\w.-]*)(=|\s+)(\S+)/gi;
33
+ // The shapes that are a secret on their own, with no flag in front of them.
34
+ const BARE_SECRET =
35
+ /\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{12,}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,})/g;
36
+ // user:password@host, in any URL. The password goes; the rest is a location.
37
+ const URL_CREDENTIALS = /\b([a-z][a-z0-9+.-]*:\/\/[^\s:@/]+):[^\s@/]+@/gi;
38
+
39
+ export function redactCommand(argv, home = "") {
40
+ let s = String(argv ?? "").trim();
41
+ if (!s) return "";
42
+ s = s.replace(SECRET_FLAG, (_, flag, sep, value) => `${flag}${sep === "=" ? "=" : " "}\u2022\u2022\u2022`);
43
+ s = s.replace(URL_CREDENTIALS, "$1:\u2022\u2022\u2022@");
44
+ s = s.replace(BARE_SECRET, "\u2022\u2022\u2022");
45
+ // `~` last, so a home path inside a redacted value is already gone. Only a
46
+ // whole path segment, so a user called `con` cannot rewrite the middle of an
47
+ // unrelated word.
48
+ if (home && home.length > 1) s = s.split(home + "/").join("~/").split(home + " ").join("~ ");
49
+ if (s.length > CMD_MAX) s = s.slice(0, CMD_MAX - 1).trimEnd() + "\u2026";
50
+ return s;
51
+ }
52
+
53
+ /**
54
+ * The ARGUMENTS, with the executable that carries them taken off the front.
55
+ *
56
+ * The name column already says `Google Chrome Helper`; repeating
57
+ * `/Applications/Google Chrome.app/Contents/Frameworks/Google Chrome
58
+ * Framework.framework/Versions/152.0.7977.76/Helpers/Google Chrome
59
+ * Helper.app/Contents/MacOS/Google Chrome Helper` beside it spends the entire
60
+ * 180-character budget on a path the reader can already see the end of, and
61
+ * cuts off `--type=renderer`, which is the part that says WHICH helper this is.
62
+ *
63
+ * argv[0] cannot be found by splitting on whitespace — on macOS the executable
64
+ * path routinely contains spaces, and `/Applications/Google Chrome.app/…` would
65
+ * be cut at "Google". The name from the first `ps` call is what resolves it:
66
+ * argv begins with a path whose last segment IS that name.
67
+ *
68
+ * "Last segment" is the whole of the rule, and the first version of this got it
69
+ * wrong in a way only the render showed. A plain `indexOf` cuts at the FIRST
70
+ * occurrence, and a macOS bundle contains the name twice — `…/Helpers/Google
71
+ * Chrome Helper.app/Contents/MacOS/Google Chrome Helper` — so every browser row
72
+ * read `.app/Contents/MacOS/Google Chrome Helper --type=gpu-process`, having
73
+ * spent the budget on the half of the path it was supposed to remove. A plain
74
+ * `lastIndexOf` is wrong the other way: `node /srv/node_modules/x` would cut
75
+ * inside `node_modules`.
76
+ *
77
+ * So: the first occurrence that a path segment actually ends at — preceded by a
78
+ * separator or nothing, followed by whitespace or nothing. `node /srv/app.js
79
+ * --port 3000` leaves `/srv/app.js --port 3000`; `/usr/local/bin/dotnet exec
80
+ * --runtimeconfig …` leaves `exec --runtimeconfig …`, which is what identifies
81
+ * it.
82
+ *
83
+ * When the name is not in argv at all — a process that rewrote argv[0], or a
84
+ * `comm` that Linux truncated at 15 characters — the first whitespace token
85
+ * goes only if it looks like a path, because dropping the first word of
86
+ * something already short would take the only word there was.
87
+ */
88
+ export function commandTail(argv, name) {
89
+ const s = String(argv ?? "").trim();
90
+ if (!s) return "";
91
+ const n = String(name ?? "").trim();
92
+ if (n) {
93
+ for (let at = s.indexOf(n); at >= 0; at = s.indexOf(n, at + 1)) {
94
+ const before = at === 0 ? "" : s[at - 1];
95
+ const after = s[at + n.length] ?? "";
96
+ if ((before === "" || before === "/" || before === "\\" || /\s/.test(before))
97
+ && (after === "" || /\s/.test(after))) {
98
+ return s.slice(at + n.length).trim();
99
+ }
100
+ }
101
+ }
102
+ const first = s.split(/\s+/)[0] ?? "";
103
+ return first.includes("/") ? s.slice(first.length).trim() : s;
104
+ }