conductor-board 1.0.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/README.md +1 -1
- package/bin/cli.js +88 -1
- package/cli/setup.js +18 -4
- package/dist/assets/index-Dauby4vm.js +34 -0
- package/dist/assets/index-nvl4ljRP.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/server/server.js +313 -61
- package/dist/assets/index--J1uxrSo.js +0 -34
- package/dist/assets/index-DMqA9hDY.css +0 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ npx conductor-board --help # all commands + options
|
|
|
42
42
|
| `--path`, `-p` | `.conductor/status.json` | Path to the status file |
|
|
43
43
|
| `--conductor`, `-c` | auto-discovered | Path to the conductor `.yaml` |
|
|
44
44
|
| `--port` | `3042` | Port to serve on (walks forward if taken) |
|
|
45
|
-
| `--no-open` | — | Don't open the browser |
|
|
45
|
+
| `--no-open` | — | Don't open the browser — CI / headless only (it opens by default) |
|
|
46
46
|
| `--help`, `-h` | — | Show help |
|
|
47
47
|
|
|
48
48
|
```bash
|
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);
|
|
@@ -35,7 +36,8 @@ const HELP = `
|
|
|
35
36
|
--path, -p <file> Path to status.json (default: .conductor/status.json)
|
|
36
37
|
--conductor, -c <file> Path to the conductor (default: auto-discovered)
|
|
37
38
|
--port <n> Port to serve on (default: 3042)
|
|
38
|
-
--no-open Don't open the browser
|
|
39
|
+
--no-open Don't open the browser (CI / headless only;
|
|
40
|
+
the board opens your browser by default)
|
|
39
41
|
|
|
40
42
|
init options
|
|
41
43
|
--name, -n <name> Workflow name (skips the prompts)
|
|
@@ -116,6 +118,91 @@ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
|
116
118
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
117
119
|
const iris = (s) => `\x1b[38;5;141m${s}\x1b[0m`;
|
|
118
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
|
+
);
|
|
119
206
|
|
|
120
207
|
const { conductorPath: resolvedConductor, absStatus, server, serverJsonPath } =
|
|
121
208
|
await listenWithFallback(wantedPort);
|
package/cli/setup.js
CHANGED
|
@@ -27,8 +27,9 @@ steps:
|
|
|
27
27
|
|
|
28
28
|
- id: start-board
|
|
29
29
|
instruction: |
|
|
30
|
-
Start the board server in the background
|
|
31
|
-
|
|
30
|
+
Start the board server in the background. It opens the browser
|
|
31
|
+
automatically — do NOT pass --no-open here, that defeats the live view.
|
|
32
|
+
npx conductor-board >/tmp/conductor-board.log 2>&1 &
|
|
32
33
|
Wait ~3 seconds for it to initialize. It auto-detects a free port if 3042
|
|
33
34
|
is taken and records the chosen port in .conductor/server.json.
|
|
34
35
|
requires: [preflight]
|
|
@@ -74,9 +75,22 @@ steps:
|
|
|
74
75
|
instruction: |
|
|
75
76
|
Execute the generated conductor workflow.
|
|
76
77
|
Create .conductor/status.json with all steps pending and a timestamp run_id.
|
|
78
|
+
Set the top-level "goal" from the conductor's description, and refresh
|
|
79
|
+
"current_step_goal" each time current_step changes.
|
|
77
80
|
Walk each step in order, updating status.json after every step and gate
|
|
78
|
-
change. Retry on gate failure — never skip.
|
|
79
|
-
|
|
81
|
+
change. Retry on gate failure — never skip.
|
|
82
|
+
At least once per minute, append a heartbeat {at, note} to the current
|
|
83
|
+
step's heartbeat array (read prior entries first; orient against the gate
|
|
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.
|
|
88
|
+
For loop steps, update "completed" and the "iterations" object as EACH
|
|
89
|
+
iteration finishes — don't wait until the loop ends.
|
|
90
|
+
Tag heartbeats with an "insight" object when you spot a way to improve the
|
|
91
|
+
workflow. Before setting status to "done", review your insight-tagged
|
|
92
|
+
heartbeats and write 3-5 optimization "suggestions" to status.json.
|
|
93
|
+
Set the top-level status to "done" when the last step completes.
|
|
80
94
|
requires: [convert-to-conductor]
|
|
81
95
|
gate:
|
|
82
96
|
- name: "Status file exists"
|