ccdeck 1.33.155 → 1.34.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/bin/ccdeck.js +152 -5
- package/package.json +2 -2
package/bin/ccdeck.js
CHANGED
|
@@ -31,10 +31,56 @@ const __dir = dirname(fileURLToPath(import.meta.url));
|
|
|
31
31
|
// keeps the guarantee in the paragraph above wherever a path does answer.
|
|
32
32
|
const nested = resolve(__dir, '../node_modules/agents-deck');
|
|
33
33
|
const sibling = resolve(__dir, '../../agents-deck');
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
|
|
35
|
+
// The three codes that mean "there is no deck here", as opposed to "the deck is
|
|
36
|
+
// broken". Both of the steps below can fail either way and they deserve opposite
|
|
37
|
+
// answers, so the distinction is made once, by code, and nothing else is caught.
|
|
38
|
+
//
|
|
39
|
+
// MODULE_NOT_FOUND is what the last resort above threw in the shape people
|
|
40
|
+
// actually hit it: a half-removed install, a linked workspace, Yarn PnP without
|
|
41
|
+
// agents-deck in scope. Unguarded it reached the user as an eight-frame stack
|
|
42
|
+
// out of node's CJS loader before a single line of the deck — the same failure
|
|
43
|
+
// shape 0075d57 was written to eliminate, arriving through a different API. A
|
|
44
|
+
// launcher that cannot find its own deck knows exactly what is wrong and can say
|
|
45
|
+
// it in a sentence.
|
|
46
|
+
//
|
|
47
|
+
// ERR_PACKAGE_PATH_NOT_EXPORTED is the case the header above worries about:
|
|
48
|
+
// `./package.json` is not exported by default, so an `exports` map added to
|
|
49
|
+
// agents-deck later would make that resolve throw rather than answer. Resolving
|
|
50
|
+
// `agents-deck/bin/agent-dag.js` instead, as #359 suggests, does not avoid it —
|
|
51
|
+
// every subpath is gated by an exports map, and that one resolves to the bin
|
|
52
|
+
// directory rather than to the package root. The path probes are what actually
|
|
53
|
+
// keep the header's promise; listing the code here only makes the failure legible
|
|
54
|
+
// if it ever arrives.
|
|
55
|
+
//
|
|
56
|
+
// ERR_MODULE_NOT_FOUND is the import below: a pkg that was found but has no
|
|
57
|
+
// supervisor.mjs under it — a truncated or half-written install — is just as
|
|
58
|
+
// much "there is no deck here", and it used to throw its own stack too.
|
|
59
|
+
const NO_DECK = new Set(['MODULE_NOT_FOUND', 'ERR_MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED']);
|
|
60
|
+
|
|
61
|
+
let pkg, dieOfSignal;
|
|
62
|
+
try {
|
|
63
|
+
pkg = existsSync(resolve(nested, 'package.json')) ? nested
|
|
64
|
+
: existsSync(resolve(sibling, 'package.json')) ? sibling
|
|
65
|
+
: dirname(createRequire(import.meta.url).resolve('agents-deck/package.json'));
|
|
66
|
+
({ dieOfSignal } = await import(pathToFileURL(resolve(pkg, 'src/server/supervisor.mjs')).href));
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// A supervisor.mjs that throws while it evaluates is a bug in the deck, not a
|
|
69
|
+
// missing install, and it keeps its stack — that stack is the only thing that
|
|
70
|
+
// would explain it.
|
|
71
|
+
if (!NO_DECK.has(err?.code)) throw err;
|
|
72
|
+
console.error([
|
|
73
|
+
'ccdeck: could not find the agents-deck install it runs.',
|
|
74
|
+
'',
|
|
75
|
+
` looked in ${nested}`,
|
|
76
|
+
` and ${sibling}`,
|
|
77
|
+
'',
|
|
78
|
+
' ccdeck ships only this launcher — the deck itself is the agents-deck package',
|
|
79
|
+
' npm installs alongside it. Reinstall with `npm i -g ccdeck`, or skip the',
|
|
80
|
+
' launcher and run the deck directly with `npx agents-deck`.',
|
|
81
|
+
].join('\n'));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
38
84
|
|
|
39
85
|
const bin = resolve(pkg, 'bin/agent-dag.js');
|
|
40
86
|
const child = spawn(process.execPath, [bin, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
@@ -43,5 +89,106 @@ const child = spawn(process.execPath, [bin, ...process.argv.slice(2)], { stdio:
|
|
|
43
89
|
// `code ?? 0` turned that into 0: a `kill` or an OOM kill of a deck started as
|
|
44
90
|
// `npx ccdeck` was reported to systemd — or to whatever script started it — as
|
|
45
91
|
// a clean stop, while the same build under `npx agents-deck` reported 143.
|
|
46
|
-
// Ctrl+C
|
|
92
|
+
// Ctrl+C hid it: that signals the whole group, so the stub died of SIGINT
|
|
93
|
+
// before this line could run — and a deck that shut itself down cleanly comes
|
|
94
|
+
// back through here as a plain 0 anyway. The handlers below are what stop the
|
|
95
|
+
// stub from dying first, which is what makes this line reachable on that path
|
|
96
|
+
// too; they change nothing about the verdict it reports.
|
|
47
97
|
child.on('exit', (code, signal) => (signal ? dieOfSignal(signal) : process.exit(code ?? 0)));
|
|
98
|
+
|
|
99
|
+
// ── a signal aimed at this process has to reach the deck, and must not reach it
|
|
100
|
+
// twice ─────────────────────────────────────────────────────────────────────
|
|
101
|
+
//
|
|
102
|
+
// Nothing here trapped a signal, so `kill <pid>` against the stub — a systemd
|
|
103
|
+
// unit without KillMode=control-group, any supervisor that signals only the main
|
|
104
|
+
// pid, a hand-typed kill — ended the launcher in about six milliseconds and left
|
|
105
|
+
// agent-dag.js and deck.js running: still serving on 4317, still advertising a
|
|
106
|
+
// live pid in the discovery file. Hooks keep posting to a deck the user believes
|
|
107
|
+
// is stopped, and the next `npx ccdeck` finds the port taken and quietly binds
|
|
108
|
+
// another one.
|
|
109
|
+
//
|
|
110
|
+
// Ctrl+C is why that survived: the terminal signals the whole foreground process
|
|
111
|
+
// group, and the child is in it — nothing here spawns detached — so the deck
|
|
112
|
+
// gets its own copy and tears itself down without us. Measured rather than
|
|
113
|
+
// assumed, by group-signalling a real stub over a real child: under a group-wide
|
|
114
|
+
// SIGINT the child receives exactly one signal and shuts down; under a targeted
|
|
115
|
+
// one it receives none and is still alive seconds later.
|
|
116
|
+
//
|
|
117
|
+
// Which is exactly why forwarding cannot be unconditional. Under Ctrl+C the
|
|
118
|
+
// child has already had the signal, and a forwarded copy lands about a
|
|
119
|
+
// millisecond behind it as a SECOND one — measured the same way, three runs out
|
|
120
|
+
// of three. agent-dag.js reads a second signal as an impatient user pressing
|
|
121
|
+
// Ctrl+C again and answers it by SIGKILLing deck.js, and a SIGKILLed deck is a
|
|
122
|
+
// deck denied its own shutdown: the discovery file it was partway through
|
|
123
|
+
// removing stays behind, naming a pid that is gone. That is this very bug
|
|
124
|
+
// arriving from the other direction, so the obvious one-liner trades an orphaned
|
|
125
|
+
// deck for a stale one.
|
|
126
|
+
//
|
|
127
|
+
// So the forward is delayed, and the child's own exit cancels it. If the child
|
|
128
|
+
// got the signal too it is gone long before the timer and nothing is sent; if it
|
|
129
|
+
// did not, the timer fires and the deck comes down. A second signal means
|
|
130
|
+
// somebody is waiting, so it goes straight through. Deciding this by clock
|
|
131
|
+
// rather than by asking agent-dag to tolerate a duplicate keeps the answer true
|
|
132
|
+
// against a deck this stub did not ship with — a workspace, a pnpm store, an
|
|
133
|
+
// older global agents-deck found by the resolution above — which is the whole
|
|
134
|
+
// reason those layouts are supported at all.
|
|
135
|
+
//
|
|
136
|
+
// Staying alive is the other half of the fix, and it is what the delay is built
|
|
137
|
+
// on. The default action for all three signals is to die, so the stub used to
|
|
138
|
+
// vanish while the deck was still shutting down, handing the shell a prompt and
|
|
139
|
+
// a status of its own before the deck had one. Now the child's status is what
|
|
140
|
+
// the caller gets, the way `npx agents-deck` has always answered — including the
|
|
141
|
+
// 0 deck.js chooses for a Ctrl+C, where the stub used to report 130. That is the
|
|
142
|
+
// parity the exit handler above exists for, extended to the signals that arrive
|
|
143
|
+
// here rather than there. dieOfSignal drops our handler before it re-raises, so
|
|
144
|
+
// a deck that really did die of a signal still comes back as 128+n instead of
|
|
145
|
+
// being swallowed by the handlers below.
|
|
146
|
+
//
|
|
147
|
+
// WINDOWS GETS THE OPPOSITE ANSWER, because it has no signals to forward. The
|
|
148
|
+
// only thing that runs these handlers there is a console event — Ctrl+C,
|
|
149
|
+
// Ctrl+Break, the window closing — and the console delivers those to every
|
|
150
|
+
// process attached to it, so the child always has its own copy already. A
|
|
151
|
+
// targeted kill there is TerminateProcess: no handler runs at all, and there is
|
|
152
|
+
// nothing this file can do about that. Meanwhile `child.kill()` on Windows IS
|
|
153
|
+
// TerminateProcess, so forwarding would take agent-dag.js out mid-shutdown and
|
|
154
|
+
// orphan deck.js underneath it — the failure we are here to fix, caused by the
|
|
155
|
+
// fix. So on Windows the handlers only keep us alive for the child's exit
|
|
156
|
+
// status, and never forward.
|
|
157
|
+
//
|
|
158
|
+
// The delay itself is chosen against what it has to outlast and what it makes
|
|
159
|
+
// wait: longer than a healthy teardown, since deck.js caps its entire shutdown
|
|
160
|
+
// at 1500ms, and shorter than every stop timeout that would be sitting on the
|
|
161
|
+
// other end of a `kill` — docker's 10s is the tightest, systemd's 90s the most
|
|
162
|
+
// common. Ctrl+C never waits it out, because the deck is long gone by then.
|
|
163
|
+
const FORWARD_AFTER_MS = 2000;
|
|
164
|
+
|
|
165
|
+
let pending = null; // a forward waiting to see whether the child needs one
|
|
166
|
+
let forwarded = false; // whether one has already gone out
|
|
167
|
+
|
|
168
|
+
const forward = (sig) => {
|
|
169
|
+
pending = null;
|
|
170
|
+
forwarded = true;
|
|
171
|
+
try { child.kill(sig); } catch { /* already gone */ }
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
175
|
+
process.on(sig, () => {
|
|
176
|
+
// Node sets both of these in the same breath as it emits 'exit', so this is
|
|
177
|
+
// the child's real state and not a guess: there is nothing left to signal,
|
|
178
|
+
// and the exit handler above is already on its way to ending us.
|
|
179
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
180
|
+
if (process.platform === 'win32') return;
|
|
181
|
+
if (!pending && !forwarded) {
|
|
182
|
+
// unref'd because the child's own handle is what keeps this process alive;
|
|
183
|
+
// a forward that is still waiting should never be the reason the stub
|
|
184
|
+
// outlives the deck.
|
|
185
|
+
pending = setTimeout(() => forward(sig), FORWARD_AFTER_MS).unref();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Anything after the first goes straight out, and `forwarded` keeps it that
|
|
189
|
+
// way: once somebody has asked twice, a later ask must not be slower than
|
|
190
|
+
// the one before it. The clear is a no-op when the delay has already run.
|
|
191
|
+
clearTimeout(pending);
|
|
192
|
+
forward(sig);
|
|
193
|
+
});
|
|
194
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccdeck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0",
|
|
4
4
|
"description": "Alias for agents-deck — live deck of Claude Code and Codex agents. Run with: npx ccdeck",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"LICENSE"
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"agents-deck": "1.
|
|
15
|
+
"agents-deck": "1.34.0"
|
|
16
16
|
},
|
|
17
17
|
"engines": {
|
|
18
18
|
"node": ">=18"
|