ccdeck 3.29.11 → 3.31.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 +4 -3
- package/bin/agent-dag.js +67 -118
- package/bin/cli/help.js +67 -0
- package/bin/cli/login-item.js +182 -0
- package/bin/cli/one-shot.js +254 -0
- package/bin/cli/package.js +39 -0
- package/bin/cli/pulse.js +98 -0
- package/bin/cli/restart.js +179 -0
- package/bin/cli/screen.js +166 -0
- package/bin/cli/second-start.js +120 -0
- package/bin/cli/startup.js +459 -0
- package/bin/cli/uninstall.js +169 -0
- package/bin/deck.js +112 -1559
- package/dist/web/assets/BrowserWatchModal-n63GjLhv.js +1 -0
- package/dist/web/assets/UsageHistoryModal-DEwBX4Fk.js +1 -0
- package/dist/web/assets/index-CZJf1Cpp.js +299 -0
- package/dist/web/assets/index-D4q1ElXU.css +1 -0
- package/dist/web/index.html +2 -2
- package/hook/hook.js +15 -14
- package/package.json +1 -1
- package/release-notes.json +40 -0
- package/src/server/account-projects.mjs +25 -11
- package/src/server/account-routes.mjs +254 -0
- package/src/server/activity.mjs +3 -3
- package/src/server/append-budget.mjs +185 -0
- package/src/server/append-failures.mjs +147 -0
- package/src/server/args.mjs +37 -5
- package/src/server/atomic-write.mjs +300 -0
- package/src/server/block-notify.mjs +0 -5
- package/src/server/boot-lock.mjs +1 -1
- package/src/server/brand.mjs +7 -6
- package/src/server/browser-react.mjs +10 -2
- package/src/server/browser-watch-decks.mjs +146 -0
- package/src/server/browser-watch-feed.mjs +69 -0
- package/src/server/browser-watch-log.mjs +173 -0
- package/src/server/browser-watch-mtime.mjs +18 -0
- package/src/server/browser-watch-relay.mjs +112 -0
- package/src/server/browser-watch-routes.mjs +172 -0
- package/src/server/browser-watch-seen.mjs +127 -0
- package/src/server/browser-watch-store.mjs +14 -143
- package/src/server/browser-watch.mjs +278 -626
- package/src/server/canonical-path.mjs +150 -0
- package/src/server/ccusage-failure.mjs +64 -0
- package/src/server/ccusage-install.mjs +459 -0
- package/src/server/ccusage-runner.mjs +271 -0
- package/src/server/ccusage.mjs +21 -756
- package/src/server/claude-accounts.mjs +220 -464
- package/src/server/claude-collector.mjs +189 -0
- package/src/server/claude-dir.mjs +7 -3
- package/src/server/claude-identity.mjs +5 -4
- package/src/server/claude-reset-credits.mjs +130 -0
- package/src/server/claude-verdicts.mjs +174 -0
- package/src/server/clear-route.mjs +191 -0
- package/src/server/codex-auth.mjs +4 -4
- package/src/server/codex-dir.mjs +52 -8
- package/src/server/codex-enrichment.mjs +310 -0
- package/src/server/codex-quota.mjs +5 -3
- package/src/server/codex-translate.mjs +407 -0
- package/src/server/codex-usage.mjs +8 -5
- package/src/server/codex-watch.mjs +496 -0
- package/src/server/cswap-auto-loop.mjs +342 -0
- package/src/server/cswap-auto-readers.mjs +303 -0
- package/src/server/cswap-auto.mjs +51 -609
- package/src/server/cswap-install.mjs +69 -265
- package/src/server/cswap-layout.mjs +242 -0
- package/src/server/deck-home.mjs +2 -2
- package/src/server/deck-prefs.mjs +58 -184
- package/src/server/deck-scope.mjs +52 -0
- package/src/server/desktop-update-routes.mjs +75 -0
- package/src/server/detach.mjs +78 -0
- package/src/server/discovery.mjs +298 -0
- package/src/server/event-log.mjs +328 -0
- package/src/server/event-pipeline.mjs +235 -0
- package/src/server/event-ring.mjs +214 -0
- package/src/server/event-routes.mjs +352 -0
- package/src/server/event-sink.mjs +26 -0
- package/src/server/exec-children.mjs +128 -0
- package/src/server/exec-not-found.mjs +213 -0
- package/src/server/exec-output.mjs +23 -0
- package/src/server/exec-spec.mjs +341 -0
- package/src/server/exec.mjs +74 -661
- package/src/server/health-route.mjs +42 -0
- package/src/server/hook-challenge.mjs +37 -0
- package/src/server/http-io.mjs +104 -0
- package/src/server/index.mjs +174 -7620
- package/src/server/install-layout.mjs +506 -0
- package/src/server/installer.mjs +79 -608
- package/src/server/jsonl-chunks.mjs +139 -0
- package/src/server/lan-accounts.mjs +27 -0
- package/src/server/lan-beacon.mjs +457 -0
- package/src/server/lan-call.mjs +239 -0
- package/src/server/lan-copies.mjs +270 -0
- package/src/server/lan-deck.mjs +583 -0
- package/src/server/lan-dials.mjs +161 -0
- package/src/server/lan-engine.mjs +475 -1024
- package/src/server/lan-hearing.mjs +49 -0
- package/src/server/lan-inbound.mjs +100 -0
- package/src/server/lan-invite-offer.mjs +77 -0
- package/src/server/lan-invite.mjs +207 -0
- package/src/server/lan-lines.mjs +73 -0
- package/src/server/lan-manifest.mjs +74 -0
- package/src/server/lan-reach.mjs +82 -69
- package/src/server/lan-requests.mjs +249 -0
- package/src/server/lan-round-record.mjs +45 -0
- package/src/server/lan-round-timer.mjs +62 -0
- package/src/server/lan-routes.mjs +143 -0
- package/src/server/lan-serve.mjs +112 -0
- package/src/server/lan-socket.mjs +243 -925
- package/src/server/lan-sync.mjs +58 -1026
- package/src/server/lan-tailnet-poll.mjs +57 -0
- package/src/server/lan-turns.mjs +80 -0
- package/src/server/lan-wire.mjs +620 -0
- package/src/server/lifecycle.mjs +421 -0
- package/src/server/listen.mjs +110 -0
- package/src/server/live-decks.mjs +138 -0
- package/src/server/load-average.mjs +32 -0
- package/src/server/log-election.mjs +235 -0
- package/src/server/log-replay.mjs +374 -0
- package/src/server/log-writer.mjs +72 -557
- package/src/server/login-service.mjs +3 -2
- package/src/server/macmon.mjs +1 -1
- package/src/server/memory-files.mjs +163 -0
- package/src/server/memory-metrics.mjs +178 -0
- package/src/server/metrics-history.mjs +94 -0
- package/src/server/metrics-run.mjs +100 -0
- package/src/server/music-routes.mjs +134 -0
- package/src/server/network-metrics.mjs +5 -3
- package/src/server/network-sampler.mjs +256 -0
- package/src/server/npm-latest.mjs +385 -0
- package/src/server/npm-upgrade.mjs +352 -0
- package/src/server/npx.mjs +41 -7
- package/src/server/output-watch.mjs +5 -4
- package/src/server/pinned-build.mjs +110 -0
- package/src/server/prefs-give-back.mjs +206 -0
- package/src/server/prefs-path.mjs +14 -0
- package/src/server/prefs-refusal.mjs +86 -0
- package/src/server/prefs-routes.mjs +99 -0
- package/src/server/prefs-set-aside.mjs +86 -0
- package/src/server/prefs-state.mjs +70 -0
- package/src/server/process-command.mjs +104 -0
- package/src/server/process-list.mjs +547 -0
- package/src/server/provider-status.mjs +307 -0
- package/src/server/pulse-line.mjs +132 -0
- package/src/server/purge-key.mjs +7 -7
- package/src/server/quota-cli.mjs +174 -0
- package/src/server/quota-oauth.mjs +190 -0
- package/src/server/quota-shape.mjs +229 -0
- package/src/server/quota-store-resets.mjs +206 -0
- package/src/server/quota.mjs +123 -509
- package/src/server/relay-guard.mjs +3 -3
- package/src/server/request-gates.mjs +441 -0
- package/src/server/restart-note.mjs +180 -0
- package/src/server/retire-sound-hook.mjs +3 -3
- package/src/server/ring-bounds.mjs +215 -0
- package/src/server/running-deck.mjs +1 -1
- package/src/server/running-version.mjs +24 -0
- package/src/server/self-update.mjs +39 -1371
- package/src/server/session-enrichment.mjs +622 -0
- package/src/server/session-read-gate.mjs +49 -0
- package/src/server/session-tracking.mjs +242 -0
- package/src/server/sse-clients.mjs +222 -0
- package/src/server/stale-discovery.mjs +30 -0
- package/src/server/static-serve.mjs +81 -0
- package/src/server/supervisor.mjs +73 -12
- package/src/server/system-metrics.mjs +169 -1935
- package/src/server/term.mjs +11 -249
- package/src/server/thermal-metrics.mjs +520 -0
- package/src/server/thermal-sampler.mjs +204 -0
- package/src/server/token-redact.mjs +123 -0
- package/src/server/transcript-gate.mjs +107 -0
- package/src/server/transcript-scan.mjs +563 -0
- package/src/server/usage-routes.mjs +108 -0
- package/src/server/uv-bootstrap.mjs +1 -1
- package/src/server/wordmark.mjs +127 -0
- package/dist/web/assets/BrowserWatchModal-CyAFTvAY.js +0 -1
- package/dist/web/assets/UsageHistoryModal-KJfli0oa.js +0 -1
- package/dist/web/assets/index-Ck2RAla3.css +0 -1
- package/dist/web/assets/index-jmrcEsCj.js +0 -298
package/README.md
CHANGED
|
@@ -191,7 +191,7 @@ Quota is the one thing that is not just reading. It needs a live token, so when
|
|
|
191
191
|
|
|
192
192
|
It never steers an agent or edits your code, but it is not read-only either — besides the hook entry and its own event log, it manages the two tools it leans on, and it refreshes the Codex token it reads quota with, rewriting `~/.codex/auth.json` the way `codex` itself does. It also reads your browser's history, because Browser Watch is on unless you switch it off; that is its own section below, because it is the one thing here that is about you rather than about an agent.
|
|
193
193
|
|
|
194
|
-
What does go out is short and ordinary: a ~20-byte version check against the npm registry (plus one small request to confirm a version it has not seen before), installs and daily version checks for the two tools the deck manages (claude-swap from PyPI, ccusage from npm), on an Apple Silicon Mac whose sensors stay silent one release lookup and one binary download from GitHub for `macmon`, and, while the page is open, quota reads to Anthropic and OpenAI signed with your own credentials — that is where those numbers live. The [desktop app](#desktop-app) also asks this repository's GitHub releases for its own updates. With **Local network** on, which it is unless you switch it off, the deck also announces itself to other decks on your local network over UDP (port 45317) and pairs with the ones that answer — see [Local network](#local-network). `AGENTS_DECK_NO_INSTALL=1` turns off everything but the quota reads; `AGENTS_DECK_NO_DOWNLOAD=1` is the narrower version — no `uv` binary is fetched, the managed installs stay.
|
|
194
|
+
What does go out is short and ordinary: a ~20-byte version check against the npm registry (plus one small request to confirm a version it has not seen before), installs and daily version checks for the two tools the deck manages (claude-swap from PyPI, ccusage from npm), on an Apple Silicon Mac whose sensors stay silent one release lookup and one binary download from GitHub for `macmon`, and, while the page is open, quota reads to Anthropic and OpenAI signed with your own credentials — that is where those numbers live. While the page is open, and while the desktop app has a session running or waiting, it also reads Anthropic's and OpenAI's public status pages (`status.claude.com`, `status.openai.com`), unsigned, about every three minutes, and once a minute at most while a page is not answering, so an outage on their side shows as a chip in the topbar instead of looking like a fault on yours. The [desktop app](#desktop-app) also asks this repository's GitHub releases for its own updates. With **Local network** on, which it is unless you switch it off, the deck also announces itself to other decks on your local network over UDP (port 45317) and pairs with the ones that answer — see [Local network](#local-network). `AGENTS_DECK_NO_INSTALL=1` turns off everything but the quota reads; `AGENTS_DECK_NO_DOWNLOAD=1` is the narrower version — no `uv` binary is fetched, the managed installs stay.
|
|
195
195
|
|
|
196
196
|
## Browser Watch
|
|
197
197
|
|
|
@@ -376,11 +376,12 @@ Environment:
|
|
|
376
376
|
|---|---|
|
|
377
377
|
| `AGENT_DAG_PORT` | Default port, same as `-p` |
|
|
378
378
|
| `CODEX_HOME` | Override `~/.codex` |
|
|
379
|
-
| `AGENTS_DECK_NO_INSTALL=1` | Never install or update claude-swap / ccusage,
|
|
379
|
+
| `AGENTS_DECK_NO_INSTALL=1` | Never install or update claude-swap / ccusage, never ask npm about releases, and never read the status pages |
|
|
380
380
|
| `AGENTS_DECK_NO_DOWNLOAD=1` | Never download the `uv` binary, but keep the managed installs |
|
|
381
381
|
| `AGENTS_DECK_NO_UPDATE_CHECK=1` | Don't ask npm about releases, but keep everything else |
|
|
382
|
+
| `AGENTS_DECK_NO_STATUS=1` | Don't read Anthropic's and OpenAI's status pages, so no incident chip appears |
|
|
382
383
|
| `AGENTS_DECK_NO_FRESHEN=1` | Never nudge claude-swap to collect usage early |
|
|
383
|
-
| `AGENTS_DECK_NO_NOTIFY=1` | Never raise a desktop notification when a session blocks and no page is open |
|
|
384
|
+
| `AGENTS_DECK_NO_NOTIFY=1` | Never raise a desktop notification: not when a session blocks and no page is open, and not when Browser Watch finds something |
|
|
384
385
|
| `AGENTS_DECK_NO_LAN=1` | Keep **Local network** off, whatever its switch in the panel says |
|
|
385
386
|
| `AGENTS_DECK_CSWAP` | Full path to `cswap`, when it lives somewhere unusual |
|
|
386
387
|
| `AGENTS_DECK_CLAUDE` | Full path to the `claude` CLI |
|
package/bin/agent-dag.js
CHANGED
|
@@ -28,8 +28,10 @@
|
|
|
28
28
|
// happens beside it while it keeps serving, and only a fetch that worked is
|
|
29
29
|
// answered with the exit that gives up the port. See prefetchUpgrade.
|
|
30
30
|
//
|
|
31
|
-
// Everything else the deck does
|
|
32
|
-
//
|
|
31
|
+
// Everything else the deck does lives in bin/deck.js and the pieces lifted out
|
|
32
|
+
// of it into bin/cli/, and the rules this file follows are in
|
|
33
|
+
// src/server/supervisor.mjs. This file must stay boring: it is the one process
|
|
34
|
+
// that is never replaced.
|
|
33
35
|
import { spawn } from "node:child_process";
|
|
34
36
|
import { existsSync } from "node:fs";
|
|
35
37
|
import { connect } from "node:net";
|
|
@@ -37,19 +39,19 @@ import { dirname, join } from "node:path";
|
|
|
37
39
|
import { fileURLToPath } from "node:url";
|
|
38
40
|
import { killTree } from "../src/server/exec.mjs";
|
|
39
41
|
import { invokedAs } from "../src/server/invoked-as.mjs";
|
|
40
|
-
import {
|
|
41
|
-
import { npxFailureHint, npxFailureSummary, npxLaunch, npxPrefetch } from "../src/server/npx.mjs";
|
|
42
|
+
import { parseArgs } from "../src/server/args.mjs";
|
|
43
|
+
import { holdOutput, npxFailureHint, npxFailureSummary, npxLaunch, npxPrefetch } from "../src/server/npx.mjs";
|
|
42
44
|
import {
|
|
43
45
|
bareSpecName, claimRestartFailureKey, clearRestartFailure, currentName, installedName, installedVersion,
|
|
44
46
|
isNpxInstall, lastKnownLatest, npxRestartSpec, readRestartFailure, recordRestartFailure,
|
|
45
47
|
successorRoot,
|
|
46
48
|
} from "../src/server/self-update.mjs";
|
|
47
49
|
import {
|
|
48
|
-
|
|
49
|
-
upgradeAttempt, upgradeRefusalText, workerExitAction,
|
|
50
|
+
crashCeilingNote, crashPolicy, crashRestartNote, dieOfSignal, dieWithParent, isCrash, npxRelaunchArgs,
|
|
51
|
+
replacedNote, upgradeAttempt, upgradeRefusalText, workerArgs, workerExitAction,
|
|
50
52
|
} from "../src/server/supervisor.mjs";
|
|
51
53
|
import { colorProfile, glyphs, palette, termColumns, unicodeOK } from "../src/server/term.mjs";
|
|
52
|
-
import { DETACHED_ENV, detachAndWatch,
|
|
54
|
+
import { DETACHED_ENV, backgroundNote, detachAndWatch, noConsoleOptions, shouldDetach } from "../src/server/detach.mjs";
|
|
53
55
|
import { PRODUCT } from "../src/server/brand.mjs";
|
|
54
56
|
|
|
55
57
|
const BIN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
@@ -76,81 +78,31 @@ const INVOKED_AS = invokedAs({ pkgRoot: PKG_ROOT, argv1: process.argv[1], platfo
|
|
|
76
78
|
const P = palette(colorProfile({ isTTY: Boolean(process.stdout.isTTY) }));
|
|
77
79
|
const G = glyphs(unicodeOK());
|
|
78
80
|
|
|
79
|
-
// Who the restart-failure note below belongs to. Several decks of the same
|
|
80
|
-
// package run out of one home directory — two `npx ccdeck` runs even share the
|
|
81
|
-
// _npx directory and therefore the version — so a note named after the package
|
|
82
|
-
// alone was read by every one of them, and a deck that had never asked for an
|
|
83
|
-
// update reported someone else's failed npx as its own. Our pid is unique among
|
|
84
|
-
// the decks alive on the machine, and putting it in our own environment is what
|
|
85
|
-
// carries it to the worker: launch() spawns with a copy of it.
|
|
86
81
|
// ── the terminal stops being the deck's leash ────────────────────────────────
|
|
87
82
|
//
|
|
88
|
-
// Everything about why is in src/server/detach.mjs
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
// ALREADY DETACHED — we ARE the background copy. Carry on as this file always
|
|
92
|
-
// has: spawn the worker, supervise it, never come back here.
|
|
93
|
-
//
|
|
94
|
-
// A ONE-SHOT — `--version`, `--stop`, `--status`, `--help`, `--uninstall`.
|
|
95
|
-
// Those answer and leave, and a one-shot that detached would print its answer
|
|
96
|
-
// into a log file and hand the terminal back empty. They run in the
|
|
97
|
-
// foreground exactly as they always have.
|
|
98
|
-
//
|
|
99
|
-
// Anything else is a start, and a start goes to the background.
|
|
83
|
+
// Everything about why is in src/server/detach.mjs, and so is the decision —
|
|
84
|
+
// shouldDetach, which names every way of staying in the foreground. Here is
|
|
85
|
+
// only what it is asked, and what a yes does.
|
|
100
86
|
const DETACHED = process.env[DETACHED_ENV] === "1";
|
|
101
87
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
*
|
|
107
|
-
* ON WINDOWS, A CONSOLE PROGRAM STARTED BY A PROCESS WITH NO CONSOLE IS GIVEN A
|
|
108
|
-
* NEW ONE, and with Windows Terminal as the default terminal that new console
|
|
109
|
-
* is a window. Measured on a Windows 10 box: `npx ccdeck` put a second
|
|
110
|
-
* Windows Terminal window on screen, hosting the deck's own node.exe, after the
|
|
111
|
-
* one the user typed into. `windowsHide` alone does not stop it: with stdio
|
|
112
|
-
* inherited, libuv only asks for a hidden window, which Windows Terminal's
|
|
113
|
-
* default-terminal handoff does not honour. DETACHED_PROCESS gives the child
|
|
114
|
-
* no console at all, so there is nothing to show.
|
|
115
|
-
*
|
|
116
|
-
* Only when detached: a supervisor in the user's own terminal shares that
|
|
117
|
-
* console with its worker, and Ctrl+C has to reach both.
|
|
118
|
-
*/
|
|
119
|
-
const NO_CONSOLE = DETACHED && process.platform === "win32"
|
|
120
|
-
? { detached: true, windowsHide: true }
|
|
121
|
-
: {};
|
|
88
|
+
// How the worker (and an upgrade's replacement) is started when this
|
|
89
|
+
// supervisor has no console of its own — without one either, on Windows,
|
|
90
|
+
// where a console program given none opens a window. See noConsoleOptions.
|
|
91
|
+
const NO_CONSOLE = noConsoleOptions({ detached: DETACHED, platform: process.platform });
|
|
122
92
|
// A parent already holding our lifecycle. `process.send` exists only when
|
|
123
93
|
// somebody spawned us with an IPC channel, and that somebody has armed
|
|
124
|
-
// dieWithParent below and is waiting on our exit code
|
|
125
|
-
//
|
|
126
|
-
// The suite's spawnSupervised is the caller that does this today.
|
|
94
|
+
// dieWithParent below and is waiting on our exit code. The suite's
|
|
95
|
+
// spawnSupervised is the caller that does this today.
|
|
127
96
|
const LEASHED = typeof process.send === "function";
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// Every version before this one held the terminal, and something out there
|
|
131
|
-
// depends on that: a wrapper script, a CI step, a supervisor of somebody else's
|
|
132
|
-
// that starts `ccdeck` and waits on it, a `ccdeck && open …`. Handing all of
|
|
133
|
-
// those an immediate exit and no way to say otherwise would be a breaking change
|
|
134
|
-
// with no escape hatch — and the marker above is an internal one, not something
|
|
135
|
-
// to tell a user to export.
|
|
97
|
+
// `--foreground` and the one-shots are read off these.
|
|
136
98
|
const FLAGS = parseArgs(process.argv.slice(2));
|
|
137
|
-
if (
|
|
99
|
+
if (shouldDetach({ detached: DETACHED, leashed: LEASHED, flags: FLAGS })) {
|
|
138
100
|
const { deckLogDir } = await import("../src/server/deck-home.mjs");
|
|
139
101
|
const { registeredDecks } = await import("../src/server/running-deck.mjs");
|
|
140
102
|
const isTTY = Boolean(process.stdout.isTTY);
|
|
141
103
|
const profile = colorProfile({ isTTY });
|
|
142
104
|
const tone = palette(profile);
|
|
143
105
|
const npx = isNpxInstall(PKG_ROOT);
|
|
144
|
-
const stop = stopCommand({ npx, invokedAs: INVOKED_AS, product: PRODUCT });
|
|
145
|
-
// THE ONE THING AN NPX RUN CANNOT HAVE, said where it is missing. A login item
|
|
146
|
-
// must name a path that will still be there tomorrow, and npx runs out of a
|
|
147
|
-
// cache npm deletes whenever it likes — so an npx deck runs in the background
|
|
148
|
-
// and cannot come back after a reboot. One line, no disk written, offered
|
|
149
|
-
// rather than done: `npx` means "run without installing", and a tool that
|
|
150
|
-
// installs itself anyway is the one people uninstall.
|
|
151
|
-
const offer = npx
|
|
152
|
-
? ` ${tone.muted}\`${INVOKED_AS ?? PRODUCT} --install\` also starts it at login${tone.reset}\n`
|
|
153
|
-
: "";
|
|
154
106
|
const outcome = await detachAndWatch({
|
|
155
107
|
file: fileURLToPath(import.meta.url),
|
|
156
108
|
argv: process.argv.slice(2),
|
|
@@ -161,7 +113,7 @@ if (!DETACHED && !LEASHED && FLAGS.foreground !== true && !isOneShot(FLAGS)) {
|
|
|
161
113
|
isTTY,
|
|
162
114
|
profile,
|
|
163
115
|
columns: termColumns(process.stdout),
|
|
164
|
-
backgroundLine:
|
|
116
|
+
backgroundLine: backgroundNote({ npx, invokedAs: INVOKED_AS, product: PRODUCT, tone, g: G }),
|
|
165
117
|
});
|
|
166
118
|
// detachAndWatch never returns on the paths that worked. Reaching this line
|
|
167
119
|
// means the log could not be opened at all — a read-only home, a full disk —
|
|
@@ -170,6 +122,13 @@ if (!DETACHED && !LEASHED && FLAGS.foreground !== true && !isOneShot(FLAGS)) {
|
|
|
170
122
|
console.error(`${PRODUCT}: could not open ${PRODUCT}'s log (${outcome.reason}) ${G.dash} staying in the foreground.`);
|
|
171
123
|
}
|
|
172
124
|
|
|
125
|
+
// Who the restart-failure note below belongs to. Several decks of the same
|
|
126
|
+
// package run out of one home directory — two `npx ccdeck` runs even share the
|
|
127
|
+
// _npx directory and therefore the version — so a note named after the package
|
|
128
|
+
// alone was read by every one of them, and a deck that had never asked for an
|
|
129
|
+
// update reported someone else's failed npx as its own. Our pid is unique among
|
|
130
|
+
// the decks alive on the machine, and putting it in our own environment is what
|
|
131
|
+
// carries it to the worker: launch() spawns with a copy of it.
|
|
173
132
|
claimRestartFailureKey();
|
|
174
133
|
|
|
175
134
|
// The port the worker actually bound, which is not necessarily the one it was
|
|
@@ -199,6 +158,23 @@ let stopping = false;
|
|
|
199
158
|
// answer to broken is to stop and say so rather than to spin.
|
|
200
159
|
let crashes = [];
|
|
201
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Stop the npx fetch in flight, if there is one, and say whether there was.
|
|
163
|
+
*
|
|
164
|
+
* A fetch ends early three ways — the worker it was for exits, a signal
|
|
165
|
+
* reaches this process, the parent holding our leash goes away — and all three
|
|
166
|
+
* stop it with killTree, which on Windows is the only thing that reaches npm
|
|
167
|
+
* under cmd.exe. The answer is for the first of them: a worker that leaves
|
|
168
|
+
* mid-fetch writes its attempt off, while one that leaves after a fetch that
|
|
169
|
+
* worked is handing the attempt to launchNpx.
|
|
170
|
+
*/
|
|
171
|
+
function stopFetch(signal) {
|
|
172
|
+
if (!fetching) return false;
|
|
173
|
+
killTree(fetching, signal);
|
|
174
|
+
fetching = null;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
202
178
|
function launch(respawn) {
|
|
203
179
|
// Asked on every spawn rather than once at boot: the deletion this catches
|
|
204
180
|
// happens while the deck is running, so a constant read at import time would
|
|
@@ -213,9 +189,9 @@ function launch(respawn) {
|
|
|
213
189
|
console.error(replaced);
|
|
214
190
|
process.exit(1);
|
|
215
191
|
}
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
|
|
192
|
+
// On a respawn, the port the last worker bound goes last so it wins — see
|
|
193
|
+
// workerArgs.
|
|
194
|
+
const args = workerArgs(WORKER, process.argv.slice(2), { respawn, boundPort });
|
|
219
195
|
|
|
220
196
|
const worker = spawn(process.execPath, args, {
|
|
221
197
|
...NO_CONSOLE,
|
|
@@ -272,7 +248,7 @@ function launch(respawn) {
|
|
|
272
248
|
// download is spent effort and the process holding it has to be stopped:
|
|
273
249
|
// left running it would keep writing into the npx cache directory the next
|
|
274
250
|
// attempt reads, minutes after the deck stopped waiting for it.
|
|
275
|
-
if (
|
|
251
|
+
if (stopFetch()) attempting = null;
|
|
276
252
|
// `stopping` outranks the exit code — see supervisor.mjs. A restart and a
|
|
277
253
|
// Ctrl+C can land together, and honouring the code first is how the deck
|
|
278
254
|
// came back to life after the user stopped it.
|
|
@@ -292,14 +268,16 @@ function launch(respawn) {
|
|
|
292
268
|
// Before this ran in the background a crash was self-reporting: the terminal
|
|
293
269
|
// came back with the stack on it. Detached, the first sign is noticing hours
|
|
294
270
|
// later that a day of work was never recorded — so it goes back up. The
|
|
295
|
-
// whole of which crashes qualify is in isCrash,
|
|
296
|
-
//
|
|
271
|
+
// whole of which crashes qualify is in isCrash, the ceiling that stops this
|
|
272
|
+
// becoming a spin loop is in crashPolicy, and what each answer says is in
|
|
273
|
+
// crashRestartNote and crashCeilingNote.
|
|
297
274
|
if (isCrash({ code, signal, served: boundPort != null, stopping })) {
|
|
298
275
|
const verdict = crashPolicy(crashes);
|
|
299
276
|
if (verdict.restart) {
|
|
300
277
|
crashes = verdict.history;
|
|
301
|
-
|
|
302
|
-
|
|
278
|
+
console.error(crashRestartNote({
|
|
279
|
+
code, signal, delayMs: verdict.delayMs, recent: verdict.recent, product: PRODUCT, dash: G.dash,
|
|
280
|
+
}));
|
|
303
281
|
restarts++;
|
|
304
282
|
// Unref'd would be wrong here: this timer IS the supervisor's reason to
|
|
305
283
|
// stay alive, and without it the event loop empties and the process
|
|
@@ -310,7 +288,7 @@ function launch(respawn) {
|
|
|
310
288
|
// Said once, with the two numbers that make it actionable, and then this
|
|
311
289
|
// process really does end — a supervisor that keeps trying forever is the
|
|
312
290
|
// failure the ceiling exists to prevent.
|
|
313
|
-
console.error(
|
|
291
|
+
console.error(crashCeilingNote({ product: PRODUCT, command: INVOKED_AS ?? PRODUCT, dash: G.dash }));
|
|
314
292
|
}
|
|
315
293
|
|
|
316
294
|
// Anything else is the worker's own verdict and belongs to whoever started
|
|
@@ -328,26 +306,12 @@ function launch(respawn) {
|
|
|
328
306
|
});
|
|
329
307
|
}
|
|
330
308
|
|
|
331
|
-
/** Drop the two flags launchNpx sets itself. `--port` takes a value, and both
|
|
332
|
-
* spellings npm's parser accepts (`--port 4317`, `--port=4317`) have to go. */
|
|
333
|
-
function withoutPortAndOpen(args) {
|
|
334
|
-
const out = [];
|
|
335
|
-
for (let i = 0; i < args.length; i++) {
|
|
336
|
-
const a = args[i];
|
|
337
|
-
if (a === "--no-open") continue;
|
|
338
|
-
if (a === "--port") { i++; continue; } // and its value
|
|
339
|
-
if (a.startsWith("--port=")) continue;
|
|
340
|
-
out.push(a);
|
|
341
|
-
}
|
|
342
|
-
return out;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
309
|
/**
|
|
346
310
|
* Which package an npx upgrade here names, and the spec that installs it — or
|
|
347
311
|
* null when this deck was not started by npx and there is nothing to re-run.
|
|
348
312
|
*
|
|
349
313
|
* One function because there is one answer, and the worker above us reaches it
|
|
350
|
-
* independently: `upgradeName` in
|
|
314
|
+
* independently: `upgradeName` in install-layout.mjs resolves the npx case as
|
|
351
315
|
* `bareSpecName(npxRestartSpec(pkgRoot, installedName(pkgRoot)))`, and
|
|
352
316
|
* everything registry-shaped on that side — the dist-tag it fetched, the marker
|
|
353
317
|
* it cached the answer in, the failure note the browser reads — is keyed by it.
|
|
@@ -487,14 +451,9 @@ function launchNpx() {
|
|
|
487
451
|
// one answer, so the spec that is run, the note that records it and the
|
|
488
452
|
// marker its target came from all name one package. See npxUpgrade.
|
|
489
453
|
const { spec, pkgName } = upgrade;
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
|
|
493
|
-
const args = ["-y", spec, ...withoutPortAndOpen(process.argv.slice(2))];
|
|
494
|
-
if (boundPort != null) args.push("--port", String(boundPort));
|
|
495
|
-
// The tab that asked for this is open and reconnecting; a second one would be
|
|
496
|
-
// the deck talking over itself.
|
|
497
|
-
args.push("--no-open");
|
|
454
|
+
// On the port the deck is bound to, and without opening a tab — see
|
|
455
|
+
// npxRelaunchArgs.
|
|
456
|
+
const args = npxRelaunchArgs(spec, process.argv.slice(2), boundPort);
|
|
498
457
|
|
|
499
458
|
// A retry answers for itself: whatever the last attempt left on disk is about
|
|
500
459
|
// to be replaced by this attempt's outcome, and leaving it there would keep
|
|
@@ -526,18 +485,8 @@ function launchNpx() {
|
|
|
526
485
|
// Held, not discarded: the moment the replacement is serving, everything it
|
|
527
486
|
// wrote goes to the terminal and every later byte passes straight through.
|
|
528
487
|
// Until then it is only evidence for a failure that may not happen.
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const tee = () => {
|
|
532
|
-
if (teeing) return;
|
|
533
|
-
teeing = true;
|
|
534
|
-
if (tail) process.stderr.write(tail);
|
|
535
|
-
};
|
|
536
|
-
started.stderr?.on("data", (d) => {
|
|
537
|
-
const s = String(d);
|
|
538
|
-
if (teeing) { process.stderr.write(s); return; }
|
|
539
|
-
tail = (tail + s).slice(-8000);
|
|
540
|
-
});
|
|
488
|
+
const held = holdOutput({ write: (s) => process.stderr.write(s) });
|
|
489
|
+
started.stderr?.on("data", held.take);
|
|
541
490
|
|
|
542
491
|
// Whether the replacement ever got as far as serving. An npx that cannot
|
|
543
492
|
// resolve exits in seconds having bound nothing; a deck the user stops with
|
|
@@ -547,7 +496,7 @@ function launchNpx() {
|
|
|
547
496
|
if (boundPort == null || served) return;
|
|
548
497
|
const sock = connect({ port: boundPort, host: "127.0.0.1" });
|
|
549
498
|
sock.setTimeout(1000);
|
|
550
|
-
sock.on("connect", () => { served = true;
|
|
499
|
+
sock.on("connect", () => { served = true; held.release(); sock.destroy(); });
|
|
551
500
|
sock.on("timeout", () => sock.destroy());
|
|
552
501
|
sock.on("error", () => { /* not up yet */ });
|
|
553
502
|
}, 1000);
|
|
@@ -557,8 +506,8 @@ function launchNpx() {
|
|
|
557
506
|
clearInterval(probe);
|
|
558
507
|
child = null;
|
|
559
508
|
if (stopping || served) return; // the user stopped it, or it ran and ended
|
|
560
|
-
const summary = npxFailureSummary(tail);
|
|
561
|
-
const hint = npxFailureHint(tail);
|
|
509
|
+
const summary = npxFailureSummary(held.tail);
|
|
510
|
+
const hint = npxFailureHint(held.tail);
|
|
562
511
|
console.error(`${PRODUCT}: ${why} ${G.dash} staying on v${VERSION}`);
|
|
563
512
|
if (summary) console.error(` ${summary}`);
|
|
564
513
|
if (hint) console.error(` ${hint}`);
|
|
@@ -610,7 +559,7 @@ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
|
610
559
|
// Windows the shim path leaves npm as a grandchild of cmd.exe that only
|
|
611
560
|
// killTree can reach. Stopped before the worker, since the worker's own
|
|
612
561
|
// exit path is what ends this process.
|
|
613
|
-
|
|
562
|
+
stopFetch(second ? "SIGKILL" : sig);
|
|
614
563
|
if (!child) { process.exit(0); return; }
|
|
615
564
|
try { child.kill(second ? "SIGKILL" : sig); } catch { /* already gone */ }
|
|
616
565
|
// The npx step is a shell that exec's the new deck, and a signal can land in
|
|
@@ -643,7 +592,7 @@ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
|
643
592
|
// the deck kill itself a second after every successful start.
|
|
644
593
|
if (!DETACHED) dieWithParent(() => {
|
|
645
594
|
stopping = true;
|
|
646
|
-
|
|
595
|
+
stopFetch("SIGTERM");
|
|
647
596
|
if (child) killTree(child, "SIGTERM");
|
|
648
597
|
// Long enough for the worker to unregister its discovery file and let the
|
|
649
598
|
// port go, short enough that nothing is waiting on us. Unref'd: if the worker
|
package/bin/cli/help.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// The `--help` text, and nothing else.
|
|
2
|
+
//
|
|
3
|
+
// Lifted out of bin/deck.js, which answers `--help` before anything else in the
|
|
4
|
+
// boot has run and was carrying sixty lines of prose it prints once and never
|
|
5
|
+
// reads. A leaf, like the modules that file imports at the top: brand.mjs and
|
|
6
|
+
// nothing more, so asking for help still starts nothing.
|
|
7
|
+
import { PRODUCT } from "../../src/server/brand.mjs";
|
|
8
|
+
|
|
9
|
+
export function printHelp() {
|
|
10
|
+
process.stdout.write(`${PRODUCT} — live deck of Claude Code + Codex agents
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
${PRODUCT} [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
-p, --port <number> Preferred port (default: 4317; falls back to random 4318–4400)
|
|
17
|
+
--no-open Don't open the browser automatically
|
|
18
|
+
--foreground Hold the terminal, the way every version before 3.20
|
|
19
|
+
did. Ctrl+C stops the deck again
|
|
20
|
+
--new Replace the running deck with a fresh one.
|
|
21
|
+
Without it, a bare \`${PRODUCT}\` beside a deck that is
|
|
22
|
+
already up opens that deck's tab instead of building
|
|
23
|
+
a rival on another port
|
|
24
|
+
--stop Stop the running deck.
|
|
25
|
+
With --port <n>, stop that one; with --all, stop every
|
|
26
|
+
deck on this machine
|
|
27
|
+
--status What is running on this machine, and on which ports
|
|
28
|
+
--logs What the deck wrote where a terminal would have shown
|
|
29
|
+
it, and where that file is
|
|
30
|
+
--install Put the deck on your PATH and start it at login.
|
|
31
|
+
What an \`npx\` run needs to survive a reboot
|
|
32
|
+
--install-service Start the deck when you log in. Set up on first run;
|
|
33
|
+
this is only for putting it back
|
|
34
|
+
--uninstall-service Stop starting at login. \`--uninstall\` does this too
|
|
35
|
+
--workspace <path> Only capture sessions whose cwd is inside <path>
|
|
36
|
+
--scope Restrict to current working directory
|
|
37
|
+
--all Capture every session (default). Accepted and
|
|
38
|
+
ignored — it is what a bare run already does, and
|
|
39
|
+
\`--stop --port <n>\` is the only way to narrow a stop
|
|
40
|
+
--history <path> Override events log file (default: this platform's log directory)
|
|
41
|
+
--no-persist Don't write or replay events log (RAM-only)
|
|
42
|
+
--codex Force-enable Codex capture even if ~/.codex/ missing
|
|
43
|
+
--no-codex Skip Codex capture (Claude only)
|
|
44
|
+
--claude Force-enable Claude capture even if Claude Code wasn't found
|
|
45
|
+
--no-claude Skip Claude entirely: no hooks, no claude-swap, no accounts panel
|
|
46
|
+
--uninstall Remove ${PRODUCT}'s hooks from ~/.claude/settings.json and
|
|
47
|
+
~/.codex/hooks.json, and restore any sound hooks of yours it parked.
|
|
48
|
+
Hook entries only: the forwarder script under
|
|
49
|
+
~/.claude/agent-dag/, the deck's own state and log
|
|
50
|
+
directories (\`--status\` names them), ~/.agents-deck/
|
|
51
|
+
and claude-swap all stay. It NAMES the prefs.json
|
|
52
|
+
files that still hold this deck's LAN private key
|
|
53
|
+
--purge With --uninstall: delete those prefs.json files too, so the
|
|
54
|
+
private key every deck you paired with has pinned does not
|
|
55
|
+
outlive the uninstall. Your ${PRODUCT} settings go with them
|
|
56
|
+
-h, --help Show this help
|
|
57
|
+
-v, --version Print the version and exit
|
|
58
|
+
|
|
59
|
+
Anything else on the command line is reported as an unknown option and then
|
|
60
|
+
ignored: the deck still starts.
|
|
61
|
+
|
|
62
|
+
A flag that takes a value never swallows the next flag. If the value is missing,
|
|
63
|
+
empty, or itself looks like a flag — \`${PRODUCT} --workspace \$UNSET --no-persist\`
|
|
64
|
+
after the shell has dropped an unset variable — the flag is reported, left on its
|
|
65
|
+
default, and the token it would have eaten is parsed as the flag it is.
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// The login item, from the command line: `--install`, `--install-service` and
|
|
2
|
+
// `--uninstall-service`.
|
|
3
|
+
//
|
|
4
|
+
// Lifted out of the one-shot block in bin/deck.js — oneShot in
|
|
5
|
+
// bin/cli/one-shot.js now — which still answers all three before the registry
|
|
6
|
+
// is read and before anything else in the boot has run. Each resolves to the
|
|
7
|
+
// exit code, and deck.js exits with it.
|
|
8
|
+
//
|
|
9
|
+
// The one-shots' voice comes in with every call — the screen's glyphs and
|
|
10
|
+
// palette by the names those lines use, and `say`.
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { PRODUCT } from "../../src/server/brand.mjs";
|
|
14
|
+
import { COMMAND, PKG_ROOT, PKG_VERSION } from "./package.js";
|
|
15
|
+
import { G, P, write } from "./screen.js";
|
|
16
|
+
|
|
17
|
+
// The copy of the deck this file belongs to: what `--install-service` and a
|
|
18
|
+
// first start put at login. `--install` names the global copy instead.
|
|
19
|
+
const OWN_SCRIPT = join(PKG_ROOT, "bin", "agent-dag.js");
|
|
20
|
+
|
|
21
|
+
/** The login item's job, for whichever copy of the deck it should start. */
|
|
22
|
+
const loginJob = (script, deckLogDir) => ({ script, logPath: join(deckLogDir(), "deck.log"), product: PRODUCT });
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What the service record says once a login item is written, and once one is
|
|
26
|
+
* taken away. The record is what shouldOfferService reads, so every writer of
|
|
27
|
+
* it goes through these two rather than spelling the shape again.
|
|
28
|
+
*/
|
|
29
|
+
const installedRecord = (path) => ({ installed: PKG_VERSION, at: new Date().toISOString(), path });
|
|
30
|
+
export const removedRecord = () => ({ removed: new Date().toISOString(), version: PKG_VERSION });
|
|
31
|
+
|
|
32
|
+
/** The one place the three platforms genuinely differ in what a login item buys you. */
|
|
33
|
+
function warnWhenLingerOff(svc, { say, tone, gWarn }) {
|
|
34
|
+
if (svc.lingerState() === "off") {
|
|
35
|
+
say(` ${tone.warn}${gWarn} systemd tears your session down at logout, so the deck goes with it.${tone.reset}`);
|
|
36
|
+
say(` ${tone.muted}\`sudo loginctl enable-linger $USER\` keeps it running when you are logged out.${tone.reset}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── --install ─────────────────────────────────────────────────────────────
|
|
41
|
+
//
|
|
42
|
+
// The one command that turns an npx run into a deck that comes back after a
|
|
43
|
+
// reboot. `npx ccdeck` cannot start at login — a login item must name a path
|
|
44
|
+
// that will still be there tomorrow, and npx runs out of a cache npm deletes
|
|
45
|
+
// whenever it likes — so this puts the package on PATH and points the service
|
|
46
|
+
// at THAT.
|
|
47
|
+
//
|
|
48
|
+
// BEHIND A FLAG, because `npx` means "run without installing" and a tool that
|
|
49
|
+
// installs itself anyway is the tool people uninstall. Somebody typed this.
|
|
50
|
+
export async function installGlobally({ say, tone, dash, gOk, gWarn, bullet, gEllipsis, deckDataDir, deckLogDir }) {
|
|
51
|
+
const gi = await import(pathToFileURL(join(PKG_ROOT, "src/server/global-install.mjs")).href);
|
|
52
|
+
const svc = await import(pathToFileURL(join(PKG_ROOT, "src/server/login-service.mjs")).href);
|
|
53
|
+
const { installedName } = await import(pathToFileURL(join(PKG_ROOT, "src/server/self-update.mjs")).href);
|
|
54
|
+
const { run } = await import(pathToFileURL(join(PKG_ROOT, "src/server/exec.mjs")).href);
|
|
55
|
+
// The name they typed, not ours. Three packages publish this deck, and
|
|
56
|
+
// installing `ccdeck` for somebody who ran `npx agent-dag` hands them a
|
|
57
|
+
// command they did not ask for.
|
|
58
|
+
const pkg = installedName(PKG_ROOT, PRODUCT);
|
|
59
|
+
|
|
60
|
+
say(`\n ${tone.muted}${dash} installing ${pkg} globally${gEllipsis}${tone.reset}`);
|
|
61
|
+
const got = await run("npm", ["i", "-g", pkg], { timeout: gi.INSTALL_TIMEOUT_MS });
|
|
62
|
+
if (!got?.ok) {
|
|
63
|
+
say(` ${tone.err}${gWarn} ${gi.installFailure(got, { pkg })}${tone.reset}\n`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
say(` ${tone.ok}${gOk}${tone.reset} ${pkg} is on your PATH${tone.muted} ${bullet} type \`${pkg}\` to start it${tone.reset}`);
|
|
67
|
+
|
|
68
|
+
// WHERE npm PUT IT, asked rather than assumed: a prefix the user set
|
|
69
|
+
// themselves is common and nothing here can guess it.
|
|
70
|
+
const root = gi.readGlobalRoot(await run("npm", ["root", "-g"], { timeout: 30_000 }));
|
|
71
|
+
const script = gi.globalScript(root, pkg);
|
|
72
|
+
if (!script) {
|
|
73
|
+
say(` ${tone.warn}${gWarn} installed, but npm did not say where ${dash} run \`${pkg} --install-service\` to start it at login${tone.reset}\n`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
const out = svc.installService(loginJob(script, deckLogDir));
|
|
77
|
+
if (!out.ok) {
|
|
78
|
+
say(` ${tone.warn}${gWarn} installed, but it will not start at login ${dash} ${out.reason}${tone.reset}\n`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
svc.writeServiceRecord(deckDataDir(), installedRecord(out.path));
|
|
82
|
+
say(` ${tone.ok}${gOk}${tone.reset} and starts when you log in${tone.muted} ${bullet} ${out.path}${tone.reset}`);
|
|
83
|
+
warnWhenLingerOff(svc, { say, tone, gWarn });
|
|
84
|
+
say(` ${tone.muted}\`${pkg} --uninstall-service\` undoes the login part${tone.reset}\n`);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── --install-service / --uninstall-service ───────────────────────────────
|
|
89
|
+
// Answered before the registry is read: neither one is about a deck that is
|
|
90
|
+
// running, and both are as meaningful on a machine with no deck up as on one
|
|
91
|
+
// with three.
|
|
92
|
+
export async function loginItemCommand(flags, { say, tone, dash, gOk, gWarn, bullet, deckDataDir, deckLogDir }) {
|
|
93
|
+
const svc = await import(pathToFileURL(join(PKG_ROOT, "src/server/login-service.mjs")).href);
|
|
94
|
+
const { isGitCheckout, isNpxInstall } = await import(pathToFileURL(join(PKG_ROOT, "src/server/self-update.mjs")).href);
|
|
95
|
+
if (flags.uninstallService) {
|
|
96
|
+
const out = svc.uninstallService();
|
|
97
|
+
// Recorded either way. The record is what stops the next ordinary start
|
|
98
|
+
// putting back what was just taken away, and a tool that argues with its
|
|
99
|
+
// user about a login item is a tool that gets uninstalled entirely.
|
|
100
|
+
svc.writeServiceRecord(deckDataDir(), removedRecord());
|
|
101
|
+
// `existed` rather than the record: the record says what THIS tool last
|
|
102
|
+
// did, and the machine is what actually has a login item on it. Somebody
|
|
103
|
+
// who removed the plist by hand should be told the truth about the
|
|
104
|
+
// machine, not about our bookkeeping.
|
|
105
|
+
say(!out.ok
|
|
106
|
+
? `\n ${tone.err}${gWarn} could not remove it ${dash} ${out.reason}${tone.reset}\n`
|
|
107
|
+
: out.existed
|
|
108
|
+
? `\n ${tone.ok}${gOk}${tone.reset} no longer starts at login${tone.muted} ${bullet} ${out.path}${tone.reset}\n`
|
|
109
|
+
: `\n ${tone.muted}${dash} it was not starting at login${tone.reset}\n`);
|
|
110
|
+
return out.ok ? 0 : 1;
|
|
111
|
+
}
|
|
112
|
+
if (isGitCheckout(PKG_ROOT)) {
|
|
113
|
+
// Not refused outright — somebody running from a checkout may genuinely
|
|
114
|
+
// want this — but not done silently either: the item would name a working
|
|
115
|
+
// tree, and that is worth knowing before it is written.
|
|
116
|
+
say(`\n ${tone.warn}${gWarn} this is a checkout, so the login item would name ${PKG_ROOT}${tone.reset}`);
|
|
117
|
+
say(` ${tone.muted}a renamed, moved or deleted working tree leaves a login item pointing at nothing${tone.reset}\n`);
|
|
118
|
+
}
|
|
119
|
+
if (isNpxInstall(PKG_ROOT)) {
|
|
120
|
+
// The item would name a path inside ~/.npm/_npx/<hash>/, which npm deletes
|
|
121
|
+
// whenever it feels like it — a login item pointing at nothing, forever,
|
|
122
|
+
// on a machine where nothing was ever installed.
|
|
123
|
+
say(`\n ${tone.warn}${gWarn} an npx run cannot start at login ${dash} its files live in npm's cache and are deleted without warning.${tone.reset}`);
|
|
124
|
+
say(` ${tone.muted}install it first: \`npm i -g ${COMMAND}\`${tone.reset}\n`);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
const out = svc.installService(loginJob(OWN_SCRIPT, deckLogDir));
|
|
128
|
+
if (!out.ok) {
|
|
129
|
+
say(`\n ${tone.err}${gWarn} could not set it up ${dash} ${out.reason}${tone.reset}\n`);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
svc.writeServiceRecord(deckDataDir(), installedRecord(out.path));
|
|
133
|
+
say(`\n ${tone.ok}${gOk}${tone.reset} starts when you log in${tone.muted} ${bullet} ${out.path}${tone.reset}`);
|
|
134
|
+
if (out.how === "file-only") {
|
|
135
|
+
// The file is on disk and both launchd and systemd read their directories
|
|
136
|
+
// at the next login, so this works from then on. Said rather than hidden:
|
|
137
|
+
// "it will work tomorrow" is a different promise from "it works now".
|
|
138
|
+
say(` ${tone.warn}${gWarn} not started now ${dash} ${out.reason}. It will come up at your next login.${tone.reset}`);
|
|
139
|
+
}
|
|
140
|
+
warnWhenLingerOff(svc, { say, tone, gWarn });
|
|
141
|
+
say(` ${tone.muted}\`${COMMAND} --uninstall-service\` undoes it${tone.reset}\n`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── starting at login ─────────────────────────────────────────────────────────
|
|
146
|
+
//
|
|
147
|
+
// ONCE PER MACHINE, EVER. The record in the deck's own data directory is what
|
|
148
|
+
// makes that true: without it, `--uninstall-service` would be undone by the next
|
|
149
|
+
// start, which is not an uninstall — it is a tool arguing with its user.
|
|
150
|
+
//
|
|
151
|
+
// npx is excluded and AGENTS_DECK_NO_INSTALL is honoured — see
|
|
152
|
+
// shouldOfferService, which owns both rules and says why. A failure is one line
|
|
153
|
+
// and nothing else: the deck is already running, and the worst case is the
|
|
154
|
+
// behaviour every version before this one had.
|
|
155
|
+
//
|
|
156
|
+
// Called by the boot, not by a one-shot, so it writes the boot's way: `P`, `G`
|
|
157
|
+
// and `write`, straight from bin/cli/screen.js.
|
|
158
|
+
export async function offerLoginItem({ deckDataDir, deckLogDir }) {
|
|
159
|
+
try {
|
|
160
|
+
const svc = await import(pathToFileURL(join(PKG_ROOT, "src/server/login-service.mjs")).href);
|
|
161
|
+
const { isGitCheckout, isNpxInstall } = await import(pathToFileURL(join(PKG_ROOT, "src/server/self-update.mjs")).href);
|
|
162
|
+
if (svc.shouldOfferService({
|
|
163
|
+
record: svc.readServiceRecord(deckDataDir()),
|
|
164
|
+
npx: isNpxInstall(PKG_ROOT),
|
|
165
|
+
checkout: isGitCheckout(PKG_ROOT),
|
|
166
|
+
})) {
|
|
167
|
+
const out = svc.installService(loginJob(OWN_SCRIPT, deckLogDir));
|
|
168
|
+
svc.writeServiceRecord(deckDataDir(), out.ok
|
|
169
|
+
? installedRecord(out.path)
|
|
170
|
+
: { failed: out.reason ?? "unknown", at: new Date().toISOString(), version: PKG_VERSION });
|
|
171
|
+
// Said once, on the one run that does it, and never again. A tool that
|
|
172
|
+
// adds itself to your login items and does not mention it is a tool you
|
|
173
|
+
// find later, in a settings pane, and stop trusting.
|
|
174
|
+
write(out.ok
|
|
175
|
+
? ` ${P.muted}${G.dash} ${PRODUCT} will now start when you log in ${G.dash} \`${COMMAND} --uninstall-service\` undoes it${P.reset}\n\n`
|
|
176
|
+
: ` ${P.muted}${G.dash} could not set ${PRODUCT} to start at login (${out.reason}) ${G.dash} it still starts when you type it${P.reset}\n\n`);
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
// Never fatal. The deck is up; this is a convenience that did not happen.
|
|
180
|
+
console.error(`${PRODUCT}: could not check the login item:`, err?.message ?? err);
|
|
181
|
+
}
|
|
182
|
+
}
|