agent-dag 3.19.0 → 3.21.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 +176 -6
- package/bin/agent-dag.js +125 -4
- package/bin/deck.js +532 -4
- package/dist/web/assets/index-BYMVIG6f.css +1 -0
- package/dist/web/assets/index-G0ntHhBn.js +266 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/server/activity.mjs +52 -0
- package/src/server/args.mjs +27 -0
- package/src/server/auto-update.mjs +79 -0
- package/src/server/claude-accounts.mjs +205 -6
- package/src/server/cswap-admin.mjs +104 -11
- package/src/server/deck-home.mjs +243 -0
- package/src/server/deck-prefs.mjs +88 -8
- package/src/server/deck-probe.mjs +111 -0
- package/src/server/detach.mjs +244 -0
- package/src/server/global-install.mjs +67 -0
- package/src/server/index.mjs +275 -88
- package/src/server/installer.mjs +33 -8
- package/src/server/lan-about.mjs +119 -0
- package/src/server/lan-engine.mjs +142 -9
- package/src/server/lan-socket.mjs +94 -11
- package/src/server/lan-sync.mjs +5 -1
- package/src/server/login-service.mjs +473 -0
- package/src/server/presence.mjs +40 -0
- package/src/server/running-deck.mjs +234 -0
- package/src/server/self-update.mjs +5 -2
- package/src/server/stop-deck.mjs +171 -0
- package/src/server/supervisor.mjs +90 -0
- package/src/server/term.mjs +35 -2
- package/dist/web/assets/index-CE-8EKi6.css +0 -1
- package/dist/web/assets/index-DTbVC1d9.js +0 -266
package/bin/deck.js
CHANGED
|
@@ -12,11 +12,16 @@ import { isPortValue, parseArgs } from "../src/server/args.mjs";
|
|
|
12
12
|
import {
|
|
13
13
|
CURSOR_HIDE, CURSOR_SHOW, colorProfile, fit, glyphs, labelColumn, link, motionOK, oneLine,
|
|
14
14
|
palette, pulseDot, pulseText, spinnerFrames, statusLine, supportsHyperlinks, termColumns,
|
|
15
|
-
elapsedSuffix, unicodeOK, unregisteredDetail, visibleWidth, wordmark,
|
|
15
|
+
elapsedSuffix, sinceLabel, unicodeOK, unregisteredDetail, visibleWidth, wordmark,
|
|
16
16
|
} from "../src/server/term.mjs";
|
|
17
17
|
import { PRODUCT } from "../src/server/brand.mjs";
|
|
18
18
|
import { invokedName, renameNotice } from "../src/server/invoked-as.mjs";
|
|
19
19
|
import { budget, bootDeadlineMs } from "../src/server/boot-deadline.mjs";
|
|
20
|
+
// A leaf — fs, path and claude-dir.mjs, nothing else — so it is imported here
|
|
21
|
+
// with the rest rather than fetched later. Deliberately NOT the other way
|
|
22
|
+
// round: it takes the handshake as a parameter precisely so that it never has
|
|
23
|
+
// to import the server. See the note at the top of that file.
|
|
24
|
+
import { asksForOwnDeck, runningDeck, versionNote } from "../src/server/running-deck.mjs";
|
|
20
25
|
|
|
21
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
27
|
const PKG_ROOT = resolve(__dirname, "..");
|
|
@@ -101,6 +106,32 @@ if (flags.uninstall) {
|
|
|
101
106
|
// command's job. So is the other half: turning the sound on parked the user's
|
|
102
107
|
// own afplay/PowerShell Stop hooks, and once the deck is uninstalled nothing
|
|
103
108
|
// else on the machine knows where they went.
|
|
109
|
+
// The login item goes too, and this is the one place the uninstall's
|
|
110
|
+
// documented narrowness has to bend. "Hook entries only" is right for the
|
|
111
|
+
// event log and the port registry, which are data somebody may still want —
|
|
112
|
+
// but a login item left behind after an uninstall is not data, it is a
|
|
113
|
+
// machine that keeps starting a deck whose hooks were just removed.
|
|
114
|
+
{
|
|
115
|
+
const svc = await import(pathToFileURL(join(PKG_ROOT, "src/server/login-service.mjs")).href);
|
|
116
|
+
const { deckDataDir: dataDir } = await import(pathToFileURL(join(PKG_ROOT, "src/server/deck-home.mjs")).href);
|
|
117
|
+
if (svc.readServiceRecord(dataDir()) !== null) {
|
|
118
|
+
const gone = svc.uninstallService();
|
|
119
|
+
svc.writeServiceRecord(dataDir(), { removed: new Date().toISOString(), version: PKG_VERSION });
|
|
120
|
+
// Silent when there was nothing on the machine: an uninstall that reports
|
|
121
|
+
// removing a login item this deck never had is the same lie the explicit
|
|
122
|
+
// command used to tell, in the place a reader is least able to check it.
|
|
123
|
+
if (!gone.ok) {
|
|
124
|
+
// `gDash`, not an em dash: this block runs at module top level, on a
|
|
125
|
+
// console that may be the legacy Windows one, and #797 is the rule that
|
|
126
|
+
// a printed string never carries punctuation the terminal may not have.
|
|
127
|
+
console.error(`${PRODUCT}: could NOT remove the login item ${gDash} ${gone.reason} (${gone.path})`);
|
|
128
|
+
} else if (gone.existed) {
|
|
129
|
+
console.log(`${PRODUCT}: no longer starts at login`);
|
|
130
|
+
}
|
|
131
|
+
if (!gone.ok) refused = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
104
135
|
const { retireSoundHook } = await import(pathToFileURL(join(PKG_ROOT, "src/server/retire-sound-hook.mjs")).href);
|
|
105
136
|
const sound = await retireSoundHook();
|
|
106
137
|
if (sound.removed) console.log(`${PRODUCT}: sound hook removed`);
|
|
@@ -135,6 +166,284 @@ if (flags.uninstall) {
|
|
|
135
166
|
process.exit(refused ? 1 : 0);
|
|
136
167
|
}
|
|
137
168
|
|
|
169
|
+
// ── the two questions that are about another deck, not this one ──────────────
|
|
170
|
+
//
|
|
171
|
+
// One-shot commands in the shape `--uninstall` already established: do the
|
|
172
|
+
// thing, print, exit, and never start a server. They sit ABOVE the migration
|
|
173
|
+
// and well above the heavy imports, because a command that ends a deck has no
|
|
174
|
+
// business moving that deck's files on the way past, and because asking a
|
|
175
|
+
// server to stop should not require starting one.
|
|
176
|
+
//
|
|
177
|
+
// Their own glyphs and palette rather than `G`/`P`, for the reason the bad-port
|
|
178
|
+
// block below gives (#797): those are declared two hundred lines further down
|
|
179
|
+
// and this runs at module top level.
|
|
180
|
+
//
|
|
181
|
+
// WHICH DECK. The shape a bare `ccdeck` would have built — same `sameShape` the
|
|
182
|
+
// attach uses, so `ccdeck` and `ccdeck --stop` name the same deck and there is
|
|
183
|
+
// one model to hold. `--port <n>` names one directly; `--all` takes every deck
|
|
184
|
+
// on the machine. (`--all` is the legacy capture flag, a no-op since it became
|
|
185
|
+
// the default; beside `--stop` it can only mean this, and it is the word a
|
|
186
|
+
// person reaches for.)
|
|
187
|
+
if (flags.stop || flags.status || flags.logs || flags.install || flags.installService || flags.uninstallService) {
|
|
188
|
+
const { dash, ok: gOk, warn: gWarn, bullet, arrow, ellipsis: gEllipsis } = glyphs(unicodeOK());
|
|
189
|
+
const tone = palette(colorProfile({ isTTY: Boolean(process.stdout.isTTY) }));
|
|
190
|
+
const say = (line) => process.stdout.write(`${line}\n`);
|
|
191
|
+
|
|
192
|
+
const { deckDataDir, deckLogDir } = await import(pathToFileURL(join(PKG_ROOT, "src/server/deck-home.mjs")).href);
|
|
193
|
+
|
|
194
|
+
// ── --install ─────────────────────────────────────────────────────────────
|
|
195
|
+
//
|
|
196
|
+
// The one command that turns an npx run into a deck that comes back after a
|
|
197
|
+
// reboot. `npx ccdeck` cannot start at login — a login item must name a path
|
|
198
|
+
// that will still be there tomorrow, and npx runs out of a cache npm deletes
|
|
199
|
+
// whenever it likes — so this puts the package on PATH and points the service
|
|
200
|
+
// at THAT.
|
|
201
|
+
//
|
|
202
|
+
// BEHIND A FLAG, because `npx` means "run without installing" and a tool that
|
|
203
|
+
// installs itself anyway is the tool people uninstall. Somebody typed this.
|
|
204
|
+
if (flags.install) {
|
|
205
|
+
const gi = await import(pathToFileURL(join(PKG_ROOT, "src/server/global-install.mjs")).href);
|
|
206
|
+
const svc = await import(pathToFileURL(join(PKG_ROOT, "src/server/login-service.mjs")).href);
|
|
207
|
+
const { installedName } = await import(pathToFileURL(join(PKG_ROOT, "src/server/self-update.mjs")).href);
|
|
208
|
+
const { run } = await import(pathToFileURL(join(PKG_ROOT, "src/server/exec.mjs")).href);
|
|
209
|
+
// The name they typed, not ours. Three packages publish this deck, and
|
|
210
|
+
// installing `ccdeck` for somebody who ran `npx agent-dag` hands them a
|
|
211
|
+
// command they did not ask for.
|
|
212
|
+
const pkg = installedName(PKG_ROOT, PRODUCT);
|
|
213
|
+
|
|
214
|
+
say(`\n ${tone.muted}${dash} installing ${pkg} globally${gEllipsis}${tone.reset}`);
|
|
215
|
+
const got = await run("npm", ["i", "-g", pkg], { timeout: gi.INSTALL_TIMEOUT_MS });
|
|
216
|
+
if (!got?.ok) {
|
|
217
|
+
say(` ${tone.err}${gWarn} ${gi.installFailure(got, { pkg })}${tone.reset}\n`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
say(` ${tone.ok}${gOk}${tone.reset} ${pkg} is on your PATH${tone.muted} ${bullet} type \`${pkg}\` to start it${tone.reset}`);
|
|
221
|
+
|
|
222
|
+
// WHERE npm PUT IT, asked rather than assumed: a prefix the user set
|
|
223
|
+
// themselves is common and nothing here can guess it.
|
|
224
|
+
const root = gi.readGlobalRoot(await run("npm", ["root", "-g"], { timeout: 30_000 }));
|
|
225
|
+
const script = gi.globalScript(root, pkg);
|
|
226
|
+
if (!script) {
|
|
227
|
+
say(` ${tone.warn}${gWarn} installed, but npm did not say where ${dash} run \`${pkg} --install-service\` to start it at login${tone.reset}\n`);
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
const out = svc.installService({ script, logPath: join(deckLogDir(), "deck.log"), product: PRODUCT });
|
|
231
|
+
if (!out.ok) {
|
|
232
|
+
say(` ${tone.warn}${gWarn} installed, but it will not start at login ${dash} ${out.reason}${tone.reset}\n`);
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
svc.writeServiceRecord(deckDataDir(), { installed: PKG_VERSION, at: new Date().toISOString(), path: out.path });
|
|
236
|
+
say(` ${tone.ok}${gOk}${tone.reset} and starts when you log in${tone.muted} ${bullet} ${out.path}${tone.reset}`);
|
|
237
|
+
if (svc.lingerState() === "off") {
|
|
238
|
+
say(` ${tone.warn}${gWarn} systemd tears your session down at logout, so the deck goes with it.${tone.reset}`);
|
|
239
|
+
say(` ${tone.muted}\`sudo loginctl enable-linger $USER\` keeps it running when you are logged out.${tone.reset}`);
|
|
240
|
+
}
|
|
241
|
+
say(` ${tone.muted}\`${pkg} --uninstall-service\` undoes the login part${tone.reset}\n`);
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── --install-service / --uninstall-service ───────────────────────────────
|
|
246
|
+
// Answered before the registry is read: neither one is about a deck that is
|
|
247
|
+
// running, and both are as meaningful on a machine with no deck up as on one
|
|
248
|
+
// with three.
|
|
249
|
+
if (flags.installService || flags.uninstallService) {
|
|
250
|
+
const svc = await import(pathToFileURL(join(PKG_ROOT, "src/server/login-service.mjs")).href);
|
|
251
|
+
const { isGitCheckout, isNpxInstall } = await import(pathToFileURL(join(PKG_ROOT, "src/server/self-update.mjs")).href);
|
|
252
|
+
if (flags.uninstallService) {
|
|
253
|
+
const out = svc.uninstallService();
|
|
254
|
+
// Recorded either way. The record is what stops the next ordinary start
|
|
255
|
+
// putting back what was just taken away, and a tool that argues with its
|
|
256
|
+
// user about a login item is a tool that gets uninstalled entirely.
|
|
257
|
+
svc.writeServiceRecord(deckDataDir(), { removed: new Date().toISOString(), version: PKG_VERSION });
|
|
258
|
+
// `existed` rather than the record: the record says what THIS tool last
|
|
259
|
+
// did, and the machine is what actually has a login item on it. Somebody
|
|
260
|
+
// who removed the plist by hand should be told the truth about the
|
|
261
|
+
// machine, not about our bookkeeping.
|
|
262
|
+
say(!out.ok
|
|
263
|
+
? `\n ${tone.err}${gWarn} could not remove it ${dash} ${out.reason}${tone.reset}\n`
|
|
264
|
+
: out.existed
|
|
265
|
+
? `\n ${tone.ok}${gOk}${tone.reset} no longer starts at login${tone.muted} ${bullet} ${out.path}${tone.reset}\n`
|
|
266
|
+
: `\n ${tone.muted}${dash} it was not starting at login${tone.reset}\n`);
|
|
267
|
+
process.exit(out.ok ? 0 : 1);
|
|
268
|
+
}
|
|
269
|
+
if (isGitCheckout(PKG_ROOT)) {
|
|
270
|
+
// Not refused outright — somebody running from a checkout may genuinely
|
|
271
|
+
// want this — but not done silently either: the item would name a working
|
|
272
|
+
// tree, and that is worth knowing before it is written.
|
|
273
|
+
say(`\n ${tone.warn}${gWarn} this is a checkout, so the login item would name ${PKG_ROOT}${tone.reset}`);
|
|
274
|
+
say(` ${tone.muted}a renamed, moved or deleted working tree leaves a login item pointing at nothing${tone.reset}\n`);
|
|
275
|
+
}
|
|
276
|
+
if (isNpxInstall(PKG_ROOT)) {
|
|
277
|
+
// The item would name a path inside ~/.npm/_npx/<hash>/, which npm deletes
|
|
278
|
+
// whenever it feels like it — a login item pointing at nothing, forever,
|
|
279
|
+
// on a machine where nothing was ever installed.
|
|
280
|
+
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}`);
|
|
281
|
+
say(` ${tone.muted}install it first: \`npm i -g ${INVOKED_AS ?? PRODUCT}\`${tone.reset}\n`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
const out = svc.installService({
|
|
285
|
+
script: join(PKG_ROOT, "bin", "agent-dag.js"),
|
|
286
|
+
logPath: join(deckLogDir(), "deck.log"),
|
|
287
|
+
product: PRODUCT,
|
|
288
|
+
});
|
|
289
|
+
if (!out.ok) {
|
|
290
|
+
say(`\n ${tone.err}${gWarn} could not set it up ${dash} ${out.reason}${tone.reset}\n`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
svc.writeServiceRecord(deckDataDir(), { installed: PKG_VERSION, at: new Date().toISOString(), path: out.path });
|
|
294
|
+
say(`\n ${tone.ok}${gOk}${tone.reset} starts when you log in${tone.muted} ${bullet} ${out.path}${tone.reset}`);
|
|
295
|
+
if (out.how === "file-only") {
|
|
296
|
+
// The file is on disk and both launchd and systemd read their directories
|
|
297
|
+
// at the next login, so this works from then on. Said rather than hidden:
|
|
298
|
+
// "it will work tomorrow" is a different promise from "it works now".
|
|
299
|
+
say(` ${tone.warn}${gWarn} not started now ${dash} ${out.reason}. It will come up at your next login.${tone.reset}`);
|
|
300
|
+
}
|
|
301
|
+
// The one place the three platforms genuinely differ in what this buys you.
|
|
302
|
+
if (svc.lingerState() === "off") {
|
|
303
|
+
say(` ${tone.warn}${gWarn} systemd tears your session down at logout, so the deck goes with it.${tone.reset}`);
|
|
304
|
+
say(` ${tone.muted}\`sudo loginctl enable-linger $USER\` keeps it running when you are logged out.${tone.reset}`);
|
|
305
|
+
}
|
|
306
|
+
say(` ${tone.muted}\`${INVOKED_AS ?? PRODUCT} --uninstall-service\` undoes it${tone.reset}\n`);
|
|
307
|
+
process.exit(0);
|
|
308
|
+
}
|
|
309
|
+
const { canonicalLogPath } = await import(pathToFileURL(join(PKG_ROOT, "src/server/log-writer.mjs")).href);
|
|
310
|
+
const { hasCodexInstalled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
|
|
311
|
+
const { hasClaudeInstalled } = await import(pathToFileURL(join(PKG_ROOT, "src/server/claude-dir.mjs")).href);
|
|
312
|
+
const { liveDecks, sameShape } = await import(pathToFileURL(join(PKG_ROOT, "src/server/running-deck.mjs")).href);
|
|
313
|
+
|
|
314
|
+
// The default shape, spelled the same way the boot below spells it. NOT the
|
|
315
|
+
// shape of the flags on THIS command line: `--stop --no-codex` is not a
|
|
316
|
+
// request to stop a Codex-less deck, it is a flag that means nothing here,
|
|
317
|
+
// and reading it as a selector would make `--stop` miss the deck it was
|
|
318
|
+
// pointed at and say "nothing is running".
|
|
319
|
+
const mine = {
|
|
320
|
+
workspace: "",
|
|
321
|
+
persist: canonicalLogPath(join(deckLogDir(), "events.jsonl")),
|
|
322
|
+
codex: hasCodexInstalled(),
|
|
323
|
+
claude: hasClaudeInstalled(),
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// ── --logs ────────────────────────────────────────────────────────────────
|
|
327
|
+
// What the deck wrote where a terminal would have shown it. Printed RAW,
|
|
328
|
+
// escapes and all: the launcher passes its own colour tier down to a detached
|
|
329
|
+
// deck, so this file is a mirror of the terminal the deck was started from
|
|
330
|
+
// and re-rendering it is the whole point. The path goes last, because that is
|
|
331
|
+
// the line somebody copies into `tail -f`.
|
|
332
|
+
if (flags.logs) {
|
|
333
|
+
const { readFileSync: readLog, statSync: statLog } = await import("node:fs");
|
|
334
|
+
const logPath = join(deckLogDir(), "deck.log");
|
|
335
|
+
let text = null;
|
|
336
|
+
try { text = readLog(logPath, "utf8"); } catch { /* never started, or swept */ }
|
|
337
|
+
if (text === null) {
|
|
338
|
+
say(`\n ${tone.muted}${dash} nothing logged yet ${dash} ${logPath}${tone.reset}\n`);
|
|
339
|
+
process.exit(0);
|
|
340
|
+
}
|
|
341
|
+
// The tail, not the file. It is truncated at the first start on an idle
|
|
342
|
+
// machine, so it is normally small — but an attach appends to a running
|
|
343
|
+
// deck's log every time, and a long-lived deck's log is somebody's week.
|
|
344
|
+
const lines = text.split("\n");
|
|
345
|
+
const shown = lines.slice(Math.max(0, lines.length - 200));
|
|
346
|
+
if (shown.length < lines.length) {
|
|
347
|
+
say(` ${tone.muted}${dash} showing the last ${shown.length} of ${lines.length} lines${tone.reset}`);
|
|
348
|
+
}
|
|
349
|
+
process.stdout.write(shown.join("\n"));
|
|
350
|
+
if (!text.endsWith("\n")) say("");
|
|
351
|
+
const size = (() => { try { return statLog(logPath).size; } catch { return 0; } })();
|
|
352
|
+
say(` ${tone.muted}${dash} ${logPath} ${bullet} ${size} bytes${tone.reset}\n`);
|
|
353
|
+
process.exit(0);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const decks = await liveDecks().catch(() => []);
|
|
357
|
+
const age = (d) => sinceLabel(Date.now() - Date.parse(d.startedAt ?? ""));
|
|
358
|
+
const where = (d) => (d.workspace ? d.workspace : "(all)");
|
|
359
|
+
const url = (d) => `http://127.0.0.1:${d.port}`;
|
|
360
|
+
|
|
361
|
+
if (flags.status) {
|
|
362
|
+
if (!decks.length) {
|
|
363
|
+
say(`\n ${tone.muted}${dash} no deck is running ${dash} \`${INVOKED_AS ?? PRODUCT}\` starts one${tone.reset}\n`);
|
|
364
|
+
process.exit(0);
|
|
365
|
+
}
|
|
366
|
+
// The one a bare `ccdeck` would open is marked, because with two decks up
|
|
367
|
+
// that is the only question this command is really being asked.
|
|
368
|
+
const opens = decks.find(d => sameShape(d, mine)) ?? null;
|
|
369
|
+
say("");
|
|
370
|
+
for (const d of decks) {
|
|
371
|
+
// The version chunk is dropped rather than printed as "v?" for a deck too
|
|
372
|
+
// old to publish one — same rule as the attach line, and for the same
|
|
373
|
+
// reason: a question mark beside two real facts reads as a fault.
|
|
374
|
+
const head = [d.version ? `v${d.version}` : "", `pid ${d.pid}`, `up ${age(d)}`]
|
|
375
|
+
.filter(Boolean).join(` ${bullet} `);
|
|
376
|
+
const mark = d === opens ? `${tone.ok}${gOk}${tone.reset}` : `${tone.muted}${bullet}${tone.reset}`;
|
|
377
|
+
const tail = d === opens ? `${tone.muted} ${arrow} \`${INVOKED_AS ?? PRODUCT}\` opens this one${tone.reset}` : "";
|
|
378
|
+
say(` ${mark} ${tone.muted}${head}${tone.reset}${tail}`);
|
|
379
|
+
say(` ${tone.accent}${tone.bold}${url(d)}${tone.reset}`);
|
|
380
|
+
say(` ${tone.muted}${where(d)} ${bullet} ${d.persist ?? "no log (--no-persist)"}${tone.reset}`);
|
|
381
|
+
}
|
|
382
|
+
say("");
|
|
383
|
+
process.exit(0);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── --stop ────────────────────────────────────────────────────────────────
|
|
387
|
+
const { stopDeck } = await import(pathToFileURL(join(PKG_ROOT, "src/server/stop-deck.mjs")).href);
|
|
388
|
+
// `flags.port`, not the `rawPort` computed below: that folds in
|
|
389
|
+
// AGENT_DAG_PORT, which is how somebody RUNS a deck rather than which deck
|
|
390
|
+
// they mean to stop — and a deck started on a custom port still has the
|
|
391
|
+
// default SHAPE, so the matcher finds it without help. It is also declared
|
|
392
|
+
// below this block, which would make reading it here a ReferenceError (#797).
|
|
393
|
+
const named = flags.port != null && isPortValue(flags.port) ? Number(flags.port) : null;
|
|
394
|
+
const wanted = flags.all
|
|
395
|
+
? decks
|
|
396
|
+
: named !== null
|
|
397
|
+
? decks.filter(d => d.port === named)
|
|
398
|
+
: decks.filter(d => sameShape(d, mine)).slice(0, 1);
|
|
399
|
+
|
|
400
|
+
if (!wanted.length) {
|
|
401
|
+
// Three different silences, and saying the wrong one sends the reader
|
|
402
|
+
// looking in the wrong place. A machine with no deck at all is not a
|
|
403
|
+
// machine whose deck is scoped differently.
|
|
404
|
+
const why = !decks.length
|
|
405
|
+
? `no deck is running`
|
|
406
|
+
: named !== null
|
|
407
|
+
? `no deck is listening on ${named} ${dash} \`${INVOKED_AS ?? PRODUCT} --status\` lists them`
|
|
408
|
+
: `no deck of this shape is running ${dash} \`${INVOKED_AS ?? PRODUCT} --status\` lists the ${decks.length} that ${decks.length === 1 ? "is" : "are"}`;
|
|
409
|
+
say(`\n ${tone.muted}${dash} ${why}${tone.reset}\n`);
|
|
410
|
+
process.exit(0);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
say("");
|
|
414
|
+
let refused = false;
|
|
415
|
+
for (const d of wanted) {
|
|
416
|
+
const was = age(d);
|
|
417
|
+
const out = await stopDeck(d);
|
|
418
|
+
if (!out.ok) {
|
|
419
|
+
refused = true;
|
|
420
|
+
say(` ${tone.err}${gWarn} could not stop pid ${d.pid} on ${d.port} ${dash} ${out.reason}${tone.reset}`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
// HOW it went out, not just that it did. "asked" means the deck closed its
|
|
424
|
+
// listener, unlinked its registration and left the LAN cleanly; anything
|
|
425
|
+
// else means none of that happened and the next boot has litter to sweep.
|
|
426
|
+
const how = out.how === "asked"
|
|
427
|
+
? ""
|
|
428
|
+
: out.old
|
|
429
|
+
? ` ${tone.muted}(${out.how} ${dash} that deck predates \`--stop\`)${tone.reset}`
|
|
430
|
+
: ` ${tone.muted}(${out.how} ${dash} it did not answer)${tone.reset}`;
|
|
431
|
+
say(` ${tone.ok}${gOk}${tone.reset} stopped${tone.muted} ${bullet} pid ${d.pid} ${bullet} port ${d.port} ${bullet} was up ${was}${tone.reset}${how}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// What is still up, named. A command that ends one of three decks and says
|
|
435
|
+
// only "stopped" leaves the reader believing the machine is clear.
|
|
436
|
+
const left = decks.filter(d => !wanted.includes(d));
|
|
437
|
+
if (left.length) {
|
|
438
|
+
say("");
|
|
439
|
+
say(` ${tone.muted}${dash} ${left.length} other deck${left.length === 1 ? "" : "s"} still running:${tone.reset}`);
|
|
440
|
+
for (const d of left) say(` ${tone.muted}pid ${d.pid} ${bullet} ${d.port} ${bullet} ${where(d)}${tone.reset}`);
|
|
441
|
+
say(` ${tone.muted}\`${INVOKED_AS ?? PRODUCT} --stop --port <n>\` ends one ${bullet} \`--stop --all\` ends every deck${tone.reset}`);
|
|
442
|
+
}
|
|
443
|
+
say("");
|
|
444
|
+
process.exit(refused ? 1 : 0);
|
|
445
|
+
}
|
|
446
|
+
|
|
138
447
|
// The port, and the one piece of argv the deck really does refuse to boot over.
|
|
139
448
|
//
|
|
140
449
|
// It refused before too — `--port banana` and `--port --no-open` both became
|
|
@@ -173,6 +482,36 @@ const openBrowser = flags.noOpen !== true;
|
|
|
173
482
|
// config dir rather than assuming ~/.claude — see src/server/claude-dir.mjs.
|
|
174
483
|
const { claudeConfigDir, hasClaudeInstalled } =
|
|
175
484
|
await import(pathToFileURL(join(PKG_ROOT, "src/server/claude-dir.mjs")).href);
|
|
485
|
+
|
|
486
|
+
// ── the deck's own files, brought to their own directory ────────────────────
|
|
487
|
+
//
|
|
488
|
+
// BEFORE ANYTHING READS THEM, which is why it is here and not in the server:
|
|
489
|
+
// index.mjs reads the preferences at module load, so a migration inside it
|
|
490
|
+
// would run after the file it is meant to find had already been looked for in
|
|
491
|
+
// the wrong place. deck-home.mjs owns the reasoning; the shape of it is that
|
|
492
|
+
// nothing is deleted and a destination that exists is never touched, so this is
|
|
493
|
+
// safe to run on every start and does nothing at all on all but the first.
|
|
494
|
+
//
|
|
495
|
+
// A failure here is reported and then ignored. A read-only home or a missing
|
|
496
|
+
// permission is not a reason for the deck not to start — the old paths are
|
|
497
|
+
// still there, and the worst case is a deck that keeps using them.
|
|
498
|
+
const { deckDataDir, deckLogDir, legacyDeckDir, migrateDeckFiles, sweepTempFiles } =
|
|
499
|
+
await import(pathToFileURL(join(PKG_ROOT, "src/server/deck-home.mjs")).href);
|
|
500
|
+
{
|
|
501
|
+
const fsp = await import("node:fs/promises");
|
|
502
|
+
const legacy = legacyDeckDir();
|
|
503
|
+
const data = deckDataDir();
|
|
504
|
+
const log = deckLogDir();
|
|
505
|
+
const { moved } = await migrateDeckFiles({
|
|
506
|
+
from: legacy, data, log, fs: fsp,
|
|
507
|
+
onError: (name, err) => console.error(`${PRODUCT}: could not move ${name}:`, err?.message ?? err),
|
|
508
|
+
});
|
|
509
|
+
if (moved.length) console.error(`${PRODUCT}: moved ${moved.join(", ")} to ${data}`);
|
|
510
|
+
// The litter an atomic write leaves when its process is killed between the
|
|
511
|
+
// write and the rename. Nothing has ever swept it, because the code that
|
|
512
|
+
// makes it is not running any more when it is made.
|
|
513
|
+
await sweepTempFiles({ dirs: [legacy, data, log], fs: fsp }).catch(() => 0);
|
|
514
|
+
}
|
|
176
515
|
// CANONICALISED here rather than left as typed: the discovery file publishes
|
|
177
516
|
// this path so the hook can tell which decks share one log and elect a single
|
|
178
517
|
// writer for it, and two spellings of one file would read as two files.
|
|
@@ -184,7 +523,12 @@ const { claudeConfigDir, hasClaudeInstalled } =
|
|
|
184
523
|
const { canonicalLogPath } = await import(pathToFileURL(join(PKG_ROOT, "src/server/log-writer.mjs")).href);
|
|
185
524
|
const persist = flags.noPersist
|
|
186
525
|
? null
|
|
187
|
-
|
|
526
|
+
// The log follows the deck rather than Claude Code now — a hundred megabytes
|
|
527
|
+
// of rotating events was never configuration, and it was living in another
|
|
528
|
+
// application's configuration directory. `--history` still wins, and the
|
|
529
|
+
// canonical form is still what the discovery file publishes, so decks that
|
|
530
|
+
// share a path still elect one writer for it.
|
|
531
|
+
: canonicalLogPath(flags.history ?? join(deckLogDir(), "events.jsonl"));
|
|
188
532
|
|
|
189
533
|
const { installHooks, keepDiscovery, removeDiscovery, hasCodexInstalled } =
|
|
190
534
|
await import(pathToFileURL(join(PKG_ROOT, "src/server/installer.mjs")).href);
|
|
@@ -888,11 +1232,90 @@ dieWithParent(() => shutdown(0));
|
|
|
888
1232
|
// whole report, and a bind that fails in there with nothing attached to it is an
|
|
889
1233
|
// unhandledRejection — which Node answers by killing the process over a port it
|
|
890
1234
|
// could have named.
|
|
1235
|
+
// ── is one of ours already up? ────────────────────────────────────────────────
|
|
1236
|
+
// A bare `ccdeck` typed beside a deck that is already running used to build a
|
|
1237
|
+
// second everything on a random port, and neither half mentioned the other.
|
|
1238
|
+
// src/server/running-deck.mjs carries the whole argument, the registry read and
|
|
1239
|
+
// the token handshake that makes the answer trustworthy; this is only where it
|
|
1240
|
+
// is asked.
|
|
1241
|
+
//
|
|
1242
|
+
// ASKED EXACTLY HERE, and the position is the point. Everything the answer
|
|
1243
|
+
// depends on is resolved above — the workspace, the canonical log path, which
|
|
1244
|
+
// of the two CLIs this deck would serve — and nothing below has run yet: no
|
|
1245
|
+
// port bound, no hooks installed, no tool probed, no banner painted, no
|
|
1246
|
+
// discovery file written. An attach therefore leaves the machine precisely as
|
|
1247
|
+
// it found it, which is what makes it safe to do without asking.
|
|
1248
|
+
//
|
|
1249
|
+
// A respawn is excluded on its own line rather than left to the flag check.
|
|
1250
|
+
// The supervisor relaunches us with `--port <bound>`, which is a shaping flag
|
|
1251
|
+
// and would be excluded anyway — but a restart is THIS deck coming back, and
|
|
1252
|
+
// having that read as "somebody typed ccdeck twice" is an accident waiting for
|
|
1253
|
+
// the day the supervisor stops passing the port.
|
|
1254
|
+
if (!RESPAWN && !asksForOwnDeck(flags)) {
|
|
1255
|
+
const live = await runningDeck({
|
|
1256
|
+
want: { workspace, persist, codex: wantCodex, claude: wantClaude },
|
|
1257
|
+
}).catch(() => null);
|
|
1258
|
+
if (live) {
|
|
1259
|
+
const liveUrl = `http://127.0.0.1:${live.port}`;
|
|
1260
|
+
const note = versionNote(live.version, PKG_VERSION);
|
|
1261
|
+
// Not the startup report's rows. That report has a label column because it
|
|
1262
|
+
// has twelve lines to align; this has two, and borrowing the column would
|
|
1263
|
+
// indent a three-line message behind a gutter sized for "Codex sessions".
|
|
1264
|
+
//
|
|
1265
|
+
// The version chunk is dropped rather than printed as "v?" when the running
|
|
1266
|
+
// deck is too old to report one — see versionNote, which says nothing in the
|
|
1267
|
+
// same case for the same reason.
|
|
1268
|
+
const ident = [live.version ? `v${live.version}` : "", `pid ${live.pid}`]
|
|
1269
|
+
.filter(Boolean).join(` ${G.bullet} `);
|
|
1270
|
+
write(`\n ${P.ok}${G.ok}${P.reset} deck already running${P.muted} ${G.bullet} ${ident}${P.reset}\n`);
|
|
1271
|
+
// Its own line, always. The URL is the one detail an ellipsis would destroy
|
|
1272
|
+
// — half an address is not a shorter address — and this message has no
|
|
1273
|
+
// report to hand it to. Same rule as statusLine's `keep`.
|
|
1274
|
+
write(` ${P.accent}${P.bold}${link(liveUrl, liveUrl, LINKS)}${P.reset}\n`);
|
|
1275
|
+
if (note) write(` ${P.warn}${G.warn} ${note}${P.reset}\n`);
|
|
1276
|
+
// THE TYPO'S WARNING, on the path that has no startup report to carry it.
|
|
1277
|
+
//
|
|
1278
|
+
// asksForOwnDeck used to answer `true` for these two so the report would
|
|
1279
|
+
// run and print them, and that is how `ccdeck --stpo` — a misspelling of
|
|
1280
|
+
// the flag that STOPS a deck — came to build a second one. The warning was
|
|
1281
|
+
// the requirement; the extra process never was. Printed here, in the same
|
|
1282
|
+
// rows the report uses, so nothing is lost and nothing is started.
|
|
1283
|
+
reportUnknownFlags(flags.unknown);
|
|
1284
|
+
reportIncompleteFlags(flags.incomplete);
|
|
1285
|
+
// The line that says a second deck was NOT started. Without it the command
|
|
1286
|
+
// looks like it did nothing at all, which is the other way to be confusing
|
|
1287
|
+
// about this — and it names the flag for the person who really did want two.
|
|
1288
|
+
write(`\n ${P.muted}${G.dash} no second deck was started ${G.dash} \`${INVOKED_AS ?? PRODUCT} --new\` starts one${P.reset}\n`);
|
|
1289
|
+
if (openBrowser) {
|
|
1290
|
+
write(`\n ${P.ok}${P.bold}${G.play} opening browser${G.ellipsis}${P.reset}\n\n`);
|
|
1291
|
+
try {
|
|
1292
|
+
const { openUrl, LAUNCH_GRACE_MS } = await import(pathToFileURL(join(PKG_ROOT, "src/server/open-url.mjs")).href);
|
|
1293
|
+
openUrl(liveUrl);
|
|
1294
|
+
// Held for exactly as long as openUrl needs to fall through to its next
|
|
1295
|
+
// launcher. Every child it spawns is unref'd, so an immediate exit ends
|
|
1296
|
+
// this process before a missing xdg-open has been answered by gio — and
|
|
1297
|
+
// then no browser opens and nothing says why. The boot path never had
|
|
1298
|
+
// to think about this because it stays alive forever.
|
|
1299
|
+
await sleep(LAUNCH_GRACE_MS);
|
|
1300
|
+
} catch { /* the URL is on screen; it can be clicked or pasted */ }
|
|
1301
|
+
} else {
|
|
1302
|
+
write("\n");
|
|
1303
|
+
}
|
|
1304
|
+
process.exit(0);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
891
1308
|
const starting = startServer({
|
|
892
1309
|
port, persist, workspace, codex: wantCodex, claude: wantClaude,
|
|
893
1310
|
// Withheld when nothing is supervising us: without a parent, exiting is just
|
|
894
1311
|
// exiting, and /api/restart answers 501 so the UI hides the control.
|
|
895
1312
|
onRestart: SUPERVISED ? requestRestart : null,
|
|
1313
|
+
// NOT withheld, unlike the restart above. A restart needs a supervisor to
|
|
1314
|
+
// bring the replacement up on the same port; ending is something any deck can
|
|
1315
|
+
// do on its own, supervised or not, and `ccdeck --stop` must work on both.
|
|
1316
|
+
// shutdown() is a hoisted declaration precisely so it is callable from the
|
|
1317
|
+
// first instruction of this module — see the long note beside it (#448).
|
|
1318
|
+
onStop: () => shutdown(0),
|
|
896
1319
|
}).then(s => ({ ok: true, s }), err => ({ ok: false, err }));
|
|
897
1320
|
|
|
898
1321
|
// Once-per-session setup — hook install, tool probes, registry lookups, and the
|
|
@@ -975,6 +1398,17 @@ discovery = keepDiscovery({
|
|
|
975
1398
|
token: hookToken(),
|
|
976
1399
|
persist,
|
|
977
1400
|
codex: wantCodex,
|
|
1401
|
+
// Both read by the next `ccdeck` on this machine, not by us: `claude` is half
|
|
1402
|
+
// of the shape it compares before it will attach to us, and `version` is what
|
|
1403
|
+
// it prints when the deck it found is not the one the user just launched. See
|
|
1404
|
+
// src/server/running-deck.mjs.
|
|
1405
|
+
claude: wantClaude,
|
|
1406
|
+
version: PKG_VERSION,
|
|
1407
|
+
// The supervisor above us, for `ccdeck --stop`'s fallback ladder: a worker
|
|
1408
|
+
// killed under a live supervisor is a worker the supervisor puts back, so the
|
|
1409
|
+
// parent has to go first and cannot be found without being told. Null when
|
|
1410
|
+
// nothing is supervising, which is the same question `onRestart` asks.
|
|
1411
|
+
parent: SUPERVISED ? process.ppid : null,
|
|
978
1412
|
onState: (state) => {
|
|
979
1413
|
const first = registered === null;
|
|
980
1414
|
registered = state.ok;
|
|
@@ -1001,6 +1435,75 @@ if (openBrowser && !RESPAWN) {
|
|
|
1001
1435
|
} catch {}
|
|
1002
1436
|
}
|
|
1003
1437
|
|
|
1438
|
+
// ── the deck from before this version that is still running ──────────────────
|
|
1439
|
+
//
|
|
1440
|
+
// UPGRADE DAY, and without this it is a mystery. A deck older than the attach
|
|
1441
|
+
// publishes no `claude` and no `version` in its discovery record, so sameShape
|
|
1442
|
+
// cannot match it — deliberately, because a record that cannot be compared is
|
|
1443
|
+
// not one to attach to. The consequence is that the first `ccdeck` after an
|
|
1444
|
+
// upgrade starts a SECOND deck beside the one already running, opens a tab on
|
|
1445
|
+
// it, and says nothing; and `ccdeck --stop` will not find the old one either,
|
|
1446
|
+
// because it has the same shape problem.
|
|
1447
|
+
//
|
|
1448
|
+
// So it is named, once, with the command that actually clears it. Only for
|
|
1449
|
+
// decks with no version: a deck started with `--new`, or one scoped to another
|
|
1450
|
+
// workspace, is somebody's deliberate second deck and needs no explaining.
|
|
1451
|
+
if (!RESPAWN) {
|
|
1452
|
+
try {
|
|
1453
|
+
const { liveDecks } = await import(pathToFileURL(join(PKG_ROOT, "src/server/running-deck.mjs")).href);
|
|
1454
|
+
const older = (await liveDecks()).filter(d => !d.version);
|
|
1455
|
+
if (older.length) {
|
|
1456
|
+
const where = older.map(d => d.port).join(", ");
|
|
1457
|
+
write(` ${P.warn}${G.warn}${P.reset} ${P.muted}${older.length === 1 ? "a deck" : `${older.length} decks`} from an older ${PRODUCT} ${older.length === 1 ? "is" : "are"} still running on ${where}${P.reset}\n`);
|
|
1458
|
+
write(` ${P.muted}too old to be recognised, so this one started beside ${older.length === 1 ? "it" : "them"} ${G.dash} \`${INVOKED_AS ?? PRODUCT} --stop --all\` clears the lot${P.reset}\n\n`);
|
|
1459
|
+
}
|
|
1460
|
+
} catch { /* a question about other decks is never a reason to fail a boot */ }
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// ── starting at login ─────────────────────────────────────────────────────────
|
|
1464
|
+
//
|
|
1465
|
+
// ONCE PER MACHINE, EVER. The record in the deck's own data directory is what
|
|
1466
|
+
// makes that true: without it, `--uninstall-service` would be undone by the next
|
|
1467
|
+
// start, which is not an uninstall — it is a tool arguing with its user.
|
|
1468
|
+
//
|
|
1469
|
+
// After the boot rather than during it, and deliberately: a deck that could not
|
|
1470
|
+
// come up has no business teaching the machine to start it at every login. By
|
|
1471
|
+
// here the port is bound, the hooks are registered and the browser is open.
|
|
1472
|
+
//
|
|
1473
|
+
// npx is excluded and AGENTS_DECK_NO_INSTALL is honoured — see
|
|
1474
|
+
// shouldOfferService, which owns both rules and says why. A failure is one line
|
|
1475
|
+
// and nothing else: the deck is already running, and the worst case is the
|
|
1476
|
+
// behaviour every version before this one had.
|
|
1477
|
+
if (!RESPAWN) {
|
|
1478
|
+
try {
|
|
1479
|
+
const svc = await import(pathToFileURL(join(PKG_ROOT, "src/server/login-service.mjs")).href);
|
|
1480
|
+
const { isGitCheckout, isNpxInstall } = await import(pathToFileURL(join(PKG_ROOT, "src/server/self-update.mjs")).href);
|
|
1481
|
+
if (svc.shouldOfferService({
|
|
1482
|
+
record: svc.readServiceRecord(deckDataDir()),
|
|
1483
|
+
npx: isNpxInstall(PKG_ROOT),
|
|
1484
|
+
checkout: isGitCheckout(PKG_ROOT),
|
|
1485
|
+
})) {
|
|
1486
|
+
const out = svc.installService({
|
|
1487
|
+
script: join(PKG_ROOT, "bin", "agent-dag.js"),
|
|
1488
|
+
logPath: join(deckLogDir(), "deck.log"),
|
|
1489
|
+
product: PRODUCT,
|
|
1490
|
+
});
|
|
1491
|
+
svc.writeServiceRecord(deckDataDir(), out.ok
|
|
1492
|
+
? { installed: PKG_VERSION, at: new Date().toISOString(), path: out.path }
|
|
1493
|
+
: { failed: out.reason ?? "unknown", at: new Date().toISOString(), version: PKG_VERSION });
|
|
1494
|
+
// Said once, on the one run that does it, and never again. A tool that
|
|
1495
|
+
// adds itself to your login items and does not mention it is a tool you
|
|
1496
|
+
// find later, in a settings pane, and stop trusting.
|
|
1497
|
+
write(out.ok
|
|
1498
|
+
? ` ${P.muted}${G.dash} ${PRODUCT} will now start when you log in ${G.dash} \`${INVOKED_AS ?? PRODUCT} --uninstall-service\` undoes it${P.reset}\n\n`
|
|
1499
|
+
: ` ${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`);
|
|
1500
|
+
}
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
// Never fatal. The deck is up; this is a convenience that did not happen.
|
|
1503
|
+
console.error(`${PRODUCT}: could not check the login item:`, err?.message ?? err);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1004
1507
|
// ── Pulse indicator ───────────────────────────────────────────────────────────
|
|
1005
1508
|
// The whole line is rewritten each beat rather than just the dot: anything else
|
|
1006
1509
|
// on this deck that has something to say writes a newline first, and after that
|
|
@@ -1090,6 +1593,13 @@ if (MOTION) {
|
|
|
1090
1593
|
// the window opened at the top of this file closes; see requestRestart.
|
|
1091
1594
|
booted = true;
|
|
1092
1595
|
markDeckReady();
|
|
1596
|
+
// And said out loud, one link up. A launcher that put this deck in the
|
|
1597
|
+
// background has been tailing its log into the user's terminal since the spawn
|
|
1598
|
+
// and is waiting for exactly this to stop and hand the prompt back — NOT for
|
|
1599
|
+
// `listening`, which is sent before the server-ready row, the log row and the
|
|
1600
|
+
// browser line are written, and would cut the last three lines off every boot.
|
|
1601
|
+
// Inert when nothing is supervising us.
|
|
1602
|
+
try { process.send?.({ type: "booted" }); } catch { /* no channel; nothing waiting */ }
|
|
1093
1603
|
runHeldRestart();
|
|
1094
1604
|
|
|
1095
1605
|
/**
|
|
@@ -1250,10 +1760,28 @@ Usage:
|
|
|
1250
1760
|
Options:
|
|
1251
1761
|
-p, --port <number> Preferred port (default: 4317; falls back to random 4318–4400)
|
|
1252
1762
|
--no-open Don't open the browser automatically
|
|
1763
|
+
--foreground Hold the terminal, the way every version before 3.20
|
|
1764
|
+
did. Ctrl+C stops the deck again
|
|
1765
|
+
--new Start a second deck even if one is already running.
|
|
1766
|
+
Without it, a bare \`${PRODUCT}\` beside a deck that is
|
|
1767
|
+
already up opens that deck's tab instead of building
|
|
1768
|
+
a rival on another port
|
|
1769
|
+
--stop Stop the deck a bare \`${PRODUCT}\` would open.
|
|
1770
|
+
With --port <n>, stop that one; with --all, stop every
|
|
1771
|
+
deck on this machine
|
|
1772
|
+
--status What is running on this machine, and on which ports
|
|
1773
|
+
--logs What the deck wrote where a terminal would have shown
|
|
1774
|
+
it, and where that file is
|
|
1775
|
+
--install Put the deck on your PATH and start it at login.
|
|
1776
|
+
What an \`npx\` run needs to survive a reboot
|
|
1777
|
+
--install-service Start the deck when you log in. Set up on first run;
|
|
1778
|
+
this is only for putting it back
|
|
1779
|
+
--uninstall-service Stop starting at login. \`--uninstall\` does this too
|
|
1253
1780
|
--workspace <path> Only capture sessions whose cwd is inside <path>
|
|
1254
1781
|
--scope Restrict to current working directory
|
|
1255
|
-
--all Capture every session (default)
|
|
1256
|
-
|
|
1782
|
+
--all Capture every session (default). Beside --stop it
|
|
1783
|
+
means every deck rather than every session
|
|
1784
|
+
--history <path> Override events log file (default: this platform's log directory)
|
|
1257
1785
|
--no-persist Don't write or replay events log (RAM-only)
|
|
1258
1786
|
--codex Force-enable Codex capture even if ~/.codex/ missing
|
|
1259
1787
|
--no-codex Skip Codex capture (Claude only)
|