conductor-board 1.2.0 → 1.3.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/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { spawn } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import readline from "node:readline";
5
6
  import { startServer } from "../server/server.js";
6
7
 
7
8
  const argv = process.argv.slice(2);
@@ -117,6 +118,91 @@ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
117
118
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
118
119
  const iris = (s) => `\x1b[38;5;141m${s}\x1b[0m`;
119
120
  const mint = (s) => `\x1b[38;5;78m${s}\x1b[0m`;
121
+ const amber = (s) => `\x1b[38;5;214m${s}\x1b[0m`;
122
+
123
+ function ago(iso) {
124
+ const t = new Date(iso).getTime();
125
+ if (Number.isNaN(t)) return "";
126
+ const s = Math.max(0, Math.floor((Date.now() - t) / 1000));
127
+ if (s < 60) return `${s}s ago`;
128
+ const m = Math.floor(s / 60);
129
+ if (m < 60) return `${m}m ago`;
130
+ const h = Math.floor(m / 60);
131
+ if (h < 24) return `${h}h ago`;
132
+ return `${Math.floor(h / 24)}d ago`;
133
+ }
134
+
135
+ function ask(question) {
136
+ return new Promise((resolve) => {
137
+ if (!process.stdin.isTTY) return resolve(false); // non-interactive — never block
138
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
139
+ rl.question(question, (answer) => {
140
+ rl.close();
141
+ resolve(/^y(es)?$/i.test(answer.trim()));
142
+ });
143
+ });
144
+ }
145
+
146
+ // A pid is "alive" if signalling it doesn't throw ESRCH (EPERM = alive, not ours).
147
+ function pidAlive(pid) {
148
+ try {
149
+ process.kill(pid, 0);
150
+ return true;
151
+ } catch (e) {
152
+ return e.code === "EPERM";
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Before binding a port, look for a board that's already running here. A live
158
+ * one we offer to reclaim; a dead pid leaves a stale server.json we just clear.
159
+ */
160
+ async function preflightStaleBoard(serverJsonPath) {
161
+ let info;
162
+ try {
163
+ info = JSON.parse(fs.readFileSync(serverJsonPath, "utf8"));
164
+ } catch {
165
+ return; // no (readable) server.json — nothing to do
166
+ }
167
+ const pid = info && info.pid;
168
+ const clear = () => {
169
+ try {
170
+ fs.unlinkSync(serverJsonPath);
171
+ } catch {
172
+ /* already gone */
173
+ }
174
+ };
175
+
176
+ if (!pid || !pidAlive(pid)) {
177
+ if (pid) {
178
+ clear();
179
+ console.log(dim(`\n cleared a stale server.json (pid ${pid} is no longer running).`));
180
+ }
181
+ return;
182
+ }
183
+
184
+ const port = info.port ?? "?";
185
+ const when = info.started_at ? `, started ${ago(info.started_at)}` : "";
186
+ console.log("");
187
+ console.log(` ${amber("⚠")} A board is already running on port ${bold(port)} (pid ${pid}${when}).`);
188
+ const yes = await ask(` Kill it and start fresh? ${dim("[y/N]")} `);
189
+ if (yes) {
190
+ try {
191
+ process.kill(pid, "SIGTERM");
192
+ } catch {
193
+ /* it may have just exited */
194
+ }
195
+ await new Promise((r) => setTimeout(r, 600)); // let it release the port
196
+ clear();
197
+ console.log(dim(` stopped pid ${pid}.`));
198
+ } else {
199
+ console.log(dim(` leaving it running — starting on the next free port.`));
200
+ }
201
+ }
202
+
203
+ await preflightStaleBoard(
204
+ path.join(path.dirname(path.resolve(process.cwd(), statusPath)), "server.json"),
205
+ );
120
206
 
121
207
  const { conductorPath: resolvedConductor, absStatus, server, serverJsonPath } =
122
208
  await listenWithFallback(wantedPort);
package/cli/setup.js CHANGED
@@ -82,6 +82,9 @@ steps:
82
82
  At least once per minute, append a heartbeat {at, note} to the current
83
83
  step's heartbeat array (read prior entries first; orient against the gate
84
84
  AND the goal; use [text](url) links for any PRs or pages you produce).
85
+ Before marking each step done, append a finalBeat — {at, note, finalBeat:
86
+ true, handoff: {to, context, produced}} — summarizing the step and handing
87
+ off to the next; read the previous step's finalBeat before you start one.
85
88
  For loop steps, update "completed" and the "iterations" object as EACH
86
89
  iteration finishes — don't wait until the loop ends.
87
90
  Tag heartbeats with an "insight" object when you spot a way to improve the