@evo-dev/evodev 0.0.1-alpha.10 → 0.0.1-alpha.11
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.
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
"name": "EvoDev"
|
|
5
5
|
},
|
|
6
6
|
"description": "EvoDev Claude Code hook plugins for AI-assisted R&D workflows.",
|
|
7
|
-
"version": "0.0.1-alpha.
|
|
7
|
+
"version": "0.0.1-alpha.11",
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
10
10
|
"name": "evodev",
|
|
11
11
|
"source": "./packages/plugin",
|
|
12
12
|
"description": "EvoDev hook integration for disciplined AI coding workflows.",
|
|
13
|
-
"version": "0.0.1-alpha.
|
|
13
|
+
"version": "0.0.1-alpha.11",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "EvoDev"
|
|
16
16
|
},
|
package/dist/index.js
CHANGED
|
@@ -29106,6 +29106,131 @@ async function fileExists2(path2) {
|
|
|
29106
29106
|
}
|
|
29107
29107
|
}
|
|
29108
29108
|
|
|
29109
|
+
// packages/cli/src/ui/runtime.ts
|
|
29110
|
+
import { mkdir as mkdir24, readFile as readFile35, rm as rm11, writeFile as writeFile24 } from "node:fs/promises";
|
|
29111
|
+
import { join as join35 } from "node:path";
|
|
29112
|
+
function resolveUiRuntimePaths(homeDir) {
|
|
29113
|
+
const rootDir = join35(resolveEvoDevPaths(homeDir).stateDir, "ui");
|
|
29114
|
+
return {
|
|
29115
|
+
rootDir,
|
|
29116
|
+
runtimePath: join35(rootDir, "runtime.json"),
|
|
29117
|
+
tokenPath: join35(rootDir, "token")
|
|
29118
|
+
};
|
|
29119
|
+
}
|
|
29120
|
+
async function readUiRuntimeBundle(homeDir) {
|
|
29121
|
+
const paths3 = resolveUiRuntimePaths(homeDir);
|
|
29122
|
+
const [runtimeText, tokenText] = await Promise.all([
|
|
29123
|
+
readOptionalFile(paths3.runtimePath),
|
|
29124
|
+
readOptionalFile(paths3.tokenPath)
|
|
29125
|
+
]);
|
|
29126
|
+
return {
|
|
29127
|
+
state: runtimeText === null ? null : parseUiRuntimeState(runtimeText),
|
|
29128
|
+
token: tokenText === null ? null : parseUiRuntimeToken(tokenText)
|
|
29129
|
+
};
|
|
29130
|
+
}
|
|
29131
|
+
async function writeUiRuntimeState(input) {
|
|
29132
|
+
const state = parseUiRuntimeState(JSON.stringify(input.state));
|
|
29133
|
+
const token = parseUiRuntimeToken(input.token);
|
|
29134
|
+
const paths3 = resolveUiRuntimePaths(input.homeDir);
|
|
29135
|
+
let tokenCreated = false;
|
|
29136
|
+
let runtimeCreated = false;
|
|
29137
|
+
await mkdir24(paths3.rootDir, { recursive: true });
|
|
29138
|
+
try {
|
|
29139
|
+
await writeFile24(paths3.tokenPath, `${token}
|
|
29140
|
+
`, {
|
|
29141
|
+
encoding: "utf8",
|
|
29142
|
+
flag: "wx",
|
|
29143
|
+
mode: 384
|
|
29144
|
+
});
|
|
29145
|
+
tokenCreated = true;
|
|
29146
|
+
await writeFile24(paths3.runtimePath, `${JSON.stringify(state, null, 2)}
|
|
29147
|
+
`, {
|
|
29148
|
+
encoding: "utf8",
|
|
29149
|
+
flag: "wx",
|
|
29150
|
+
mode: 384
|
|
29151
|
+
});
|
|
29152
|
+
runtimeCreated = true;
|
|
29153
|
+
return paths3;
|
|
29154
|
+
} catch (error) {
|
|
29155
|
+
if (runtimeCreated)
|
|
29156
|
+
await rm11(paths3.runtimePath, { force: true }).catch(() => {
|
|
29157
|
+
return;
|
|
29158
|
+
});
|
|
29159
|
+
if (tokenCreated)
|
|
29160
|
+
await rm11(paths3.tokenPath, { force: true }).catch(() => {
|
|
29161
|
+
return;
|
|
29162
|
+
});
|
|
29163
|
+
throw error;
|
|
29164
|
+
}
|
|
29165
|
+
}
|
|
29166
|
+
async function removeUiRuntimeState(input) {
|
|
29167
|
+
const paths3 = resolveUiRuntimePaths(input.homeDir);
|
|
29168
|
+
const current = await readUiRuntimeBundle(input.homeDir);
|
|
29169
|
+
if (input.expectedInstanceId !== undefined && current.state?.instanceId !== input.expectedInstanceId) {
|
|
29170
|
+
return [];
|
|
29171
|
+
}
|
|
29172
|
+
if (input.expectedToken !== undefined && current.token !== input.expectedToken)
|
|
29173
|
+
return [];
|
|
29174
|
+
const removed = [];
|
|
29175
|
+
if (current.state !== null) {
|
|
29176
|
+
await rm11(paths3.runtimePath, { force: true });
|
|
29177
|
+
removed.push(paths3.runtimePath);
|
|
29178
|
+
}
|
|
29179
|
+
if (current.token !== null) {
|
|
29180
|
+
await rm11(paths3.tokenPath, { force: true });
|
|
29181
|
+
removed.push(paths3.tokenPath);
|
|
29182
|
+
}
|
|
29183
|
+
return removed;
|
|
29184
|
+
}
|
|
29185
|
+
function buildUiRuntimeBaseUrl(state) {
|
|
29186
|
+
return `http://${state.host}:${state.port}`;
|
|
29187
|
+
}
|
|
29188
|
+
function isMatchingUiRuntime(expected, actual) {
|
|
29189
|
+
return expected.component === actual.component && expected.instanceId === actual.instanceId && expected.pid === actual.pid && expected.host === actual.host && expected.port === actual.port && expected.startedAt === actual.startedAt;
|
|
29190
|
+
}
|
|
29191
|
+
function parseUiRuntimeState(value) {
|
|
29192
|
+
let parsed;
|
|
29193
|
+
try {
|
|
29194
|
+
parsed = JSON.parse(value);
|
|
29195
|
+
} catch {
|
|
29196
|
+
throw new Error("Invalid EvoDev UI runtime state.");
|
|
29197
|
+
}
|
|
29198
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
29199
|
+
throw new Error("Invalid EvoDev UI runtime state.");
|
|
29200
|
+
}
|
|
29201
|
+
const record = parsed;
|
|
29202
|
+
if (record.version !== 1 || record.component !== "evodev-ui" || typeof record.instanceId !== "string" || !/^[a-f0-9]{32}$/u.test(record.instanceId) || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0 || record.host !== "127.0.0.1" && record.host !== "localhost" || typeof record.port !== "number" || !Number.isInteger(record.port) || record.port <= 0 || record.port > 65535 || typeof record.startedAt !== "string" || Number.isNaN(Date.parse(record.startedAt))) {
|
|
29203
|
+
throw new Error("Invalid EvoDev UI runtime state.");
|
|
29204
|
+
}
|
|
29205
|
+
return {
|
|
29206
|
+
version: 1,
|
|
29207
|
+
component: "evodev-ui",
|
|
29208
|
+
instanceId: record.instanceId,
|
|
29209
|
+
pid: record.pid,
|
|
29210
|
+
host: record.host,
|
|
29211
|
+
port: record.port,
|
|
29212
|
+
startedAt: record.startedAt
|
|
29213
|
+
};
|
|
29214
|
+
}
|
|
29215
|
+
function parseUiRuntimeToken(value) {
|
|
29216
|
+
const token = value.trim();
|
|
29217
|
+
if (!/^[a-f0-9]{64}$/u.test(token))
|
|
29218
|
+
throw new Error("Invalid EvoDev UI runtime token.");
|
|
29219
|
+
return token;
|
|
29220
|
+
}
|
|
29221
|
+
async function readOptionalFile(path2) {
|
|
29222
|
+
try {
|
|
29223
|
+
return await readFile35(path2, "utf8");
|
|
29224
|
+
} catch (error) {
|
|
29225
|
+
if (isNodeError3(error) && error.code === "ENOENT")
|
|
29226
|
+
return null;
|
|
29227
|
+
throw error;
|
|
29228
|
+
}
|
|
29229
|
+
}
|
|
29230
|
+
function isNodeError3(error) {
|
|
29231
|
+
return error instanceof Error && "code" in error;
|
|
29232
|
+
}
|
|
29233
|
+
|
|
29109
29234
|
// packages/cli/src/ui/shell.ts
|
|
29110
29235
|
function renderUiDashboardHtml() {
|
|
29111
29236
|
return `<!doctype html>
|
|
@@ -30265,6 +30390,7 @@ function sanitizeText4(value) {
|
|
|
30265
30390
|
|
|
30266
30391
|
// packages/cli/src/ui/server.ts
|
|
30267
30392
|
var DEFAULT_UI_PORT = 37646;
|
|
30393
|
+
var UI_RUNTIME_REQUEST_TIMEOUT_MS = 2000;
|
|
30268
30394
|
var MAX_UI_REQUEST_BODY_BYTES = 8 * 1024;
|
|
30269
30395
|
var UI_RESPONSE_HEADERS = {
|
|
30270
30396
|
"cache-control": "no-store",
|
|
@@ -30290,9 +30416,15 @@ function getUiHelpText() {
|
|
|
30290
30416
|
"",
|
|
30291
30417
|
"Usage:",
|
|
30292
30418
|
" evodev ui [--host 127.0.0.1|localhost] [--port <port>] [--no-open]",
|
|
30419
|
+
" evodev ui status",
|
|
30420
|
+
" evodev ui open",
|
|
30421
|
+
" evodev ui stop",
|
|
30293
30422
|
" evodev ui snapshot",
|
|
30294
30423
|
"",
|
|
30295
30424
|
"Commands:",
|
|
30425
|
+
" status Show the managed dashboard process status",
|
|
30426
|
+
" open Open the authenticated URL for the running dashboard",
|
|
30427
|
+
" stop Gracefully stop the running dashboard",
|
|
30296
30428
|
" snapshot Print the local metadata dashboard snapshot as JSON",
|
|
30297
30429
|
"",
|
|
30298
30430
|
"Options:",
|
|
@@ -30300,6 +30432,7 @@ function getUiHelpText() {
|
|
|
30300
30432
|
" --port Local bind port; defaults to 37646",
|
|
30301
30433
|
" --open Open the dashboard URL with the OS default browser (default)",
|
|
30302
30434
|
" --no-open Keep the dashboard server in the terminal without opening a browser",
|
|
30435
|
+
" Press Ctrl+C to stop a dashboard attached to the current terminal.",
|
|
30303
30436
|
" Evolution automation is owned by evodev schedule; the dashboard can trigger a manual full evolution job."
|
|
30304
30437
|
].join(`
|
|
30305
30438
|
`);
|
|
@@ -30323,6 +30456,31 @@ async function runUiCommand(argv, options = {}) {
|
|
|
30323
30456
|
write(json);
|
|
30324
30457
|
return 0;
|
|
30325
30458
|
}
|
|
30459
|
+
if (command === "status") {
|
|
30460
|
+
requireNoUiSubcommandArgs(command, argv.slice(1));
|
|
30461
|
+
return await runUiStatusCommand({
|
|
30462
|
+
homeDir,
|
|
30463
|
+
write,
|
|
30464
|
+
fetchImpl: options.fetchImpl ?? globalThis.fetch
|
|
30465
|
+
});
|
|
30466
|
+
}
|
|
30467
|
+
if (command === "open") {
|
|
30468
|
+
requireNoUiSubcommandArgs(command, argv.slice(1));
|
|
30469
|
+
return await runUiOpenCommand({
|
|
30470
|
+
homeDir,
|
|
30471
|
+
write,
|
|
30472
|
+
fetchImpl: options.fetchImpl ?? globalThis.fetch,
|
|
30473
|
+
openBrowser: options.openBrowser ?? openUiBrowser
|
|
30474
|
+
});
|
|
30475
|
+
}
|
|
30476
|
+
if (command === "stop") {
|
|
30477
|
+
requireNoUiSubcommandArgs(command, argv.slice(1));
|
|
30478
|
+
return await runUiStopCommand({
|
|
30479
|
+
homeDir,
|
|
30480
|
+
write,
|
|
30481
|
+
fetchImpl: options.fetchImpl ?? globalThis.fetch
|
|
30482
|
+
});
|
|
30483
|
+
}
|
|
30326
30484
|
const flags = parseServeFlags(argv);
|
|
30327
30485
|
return await startUiServer({
|
|
30328
30486
|
homeDir,
|
|
@@ -30330,11 +30488,215 @@ async function runUiCommand(argv, options = {}) {
|
|
|
30330
30488
|
flags,
|
|
30331
30489
|
signal: options.signal,
|
|
30332
30490
|
write,
|
|
30491
|
+
fetchImpl: options.fetchImpl ?? globalThis.fetch,
|
|
30333
30492
|
openBrowser: options.openBrowser ?? openUiBrowser,
|
|
30334
30493
|
evolutionJobService: options.evolutionJobService,
|
|
30335
30494
|
proposalExecutionService: options.proposalExecutionService
|
|
30336
30495
|
});
|
|
30337
30496
|
}
|
|
30497
|
+
async function runUiStatusCommand(input) {
|
|
30498
|
+
const runtime = await readUiRuntimeBundle(input.homeDir);
|
|
30499
|
+
if (runtime.state === null) {
|
|
30500
|
+
input.write(runtime.token === null ? "EvoDev UI: stopped" : "EvoDev UI: stopped (stale authentication state present)");
|
|
30501
|
+
return 0;
|
|
30502
|
+
}
|
|
30503
|
+
const publicProbe = await probeUiRuntime({
|
|
30504
|
+
state: runtime.state,
|
|
30505
|
+
fetchImpl: input.fetchImpl
|
|
30506
|
+
});
|
|
30507
|
+
if (publicProbe.kind !== "running") {
|
|
30508
|
+
input.write(`EvoDev UI: stale pid=${runtime.state.pid} url=${buildUiRuntimeBaseUrl(runtime.state)} (${publicProbe.reason})`);
|
|
30509
|
+
return 0;
|
|
30510
|
+
}
|
|
30511
|
+
let authentication = "ready";
|
|
30512
|
+
if (runtime.token === null) {
|
|
30513
|
+
authentication = "missing";
|
|
30514
|
+
} else {
|
|
30515
|
+
const authenticatedProbe = await probeUiRuntime({
|
|
30516
|
+
state: runtime.state,
|
|
30517
|
+
token: runtime.token,
|
|
30518
|
+
fetchImpl: input.fetchImpl
|
|
30519
|
+
});
|
|
30520
|
+
if (authenticatedProbe.kind !== "running")
|
|
30521
|
+
authentication = "unavailable";
|
|
30522
|
+
}
|
|
30523
|
+
input.write(`EvoDev UI: running pid=${runtime.state.pid} url=${buildUiRuntimeBaseUrl(runtime.state)} authentication=${authentication}`);
|
|
30524
|
+
return 0;
|
|
30525
|
+
}
|
|
30526
|
+
async function runUiOpenCommand(input) {
|
|
30527
|
+
const runtime = await readUiRuntimeBundle(input.homeDir);
|
|
30528
|
+
if (runtime.state === null)
|
|
30529
|
+
throw new Error("EvoDev UI is not running. Start it with evodev ui.");
|
|
30530
|
+
if (runtime.token === null) {
|
|
30531
|
+
throw new Error("EvoDev UI authentication state is missing. Stop the stale runtime and restart it.");
|
|
30532
|
+
}
|
|
30533
|
+
const probe = await probeUiRuntime({
|
|
30534
|
+
state: runtime.state,
|
|
30535
|
+
token: runtime.token,
|
|
30536
|
+
fetchImpl: input.fetchImpl
|
|
30537
|
+
});
|
|
30538
|
+
if (probe.kind !== "running") {
|
|
30539
|
+
throw new Error(`Cannot open EvoDev UI: ${probe.reason}.`);
|
|
30540
|
+
}
|
|
30541
|
+
const baseUrl = buildUiRuntimeBaseUrl(runtime.state);
|
|
30542
|
+
input.openBrowser(`${baseUrl}/#token=${runtime.token}`, input.write);
|
|
30543
|
+
input.write(`EvoDev UI opened: ${baseUrl}`);
|
|
30544
|
+
return 0;
|
|
30545
|
+
}
|
|
30546
|
+
async function runUiStopCommand(input) {
|
|
30547
|
+
const runtime = await readUiRuntimeBundle(input.homeDir);
|
|
30548
|
+
if (runtime.state === null) {
|
|
30549
|
+
if (runtime.token !== null) {
|
|
30550
|
+
await removeUiRuntimeState({
|
|
30551
|
+
homeDir: input.homeDir,
|
|
30552
|
+
expectedToken: runtime.token
|
|
30553
|
+
});
|
|
30554
|
+
input.write("EvoDev UI: stopped; removed stale authentication state.");
|
|
30555
|
+
} else {
|
|
30556
|
+
input.write("EvoDev UI: already stopped.");
|
|
30557
|
+
}
|
|
30558
|
+
return 0;
|
|
30559
|
+
}
|
|
30560
|
+
if (runtime.token === null) {
|
|
30561
|
+
const probe = await probeUiRuntime({
|
|
30562
|
+
state: runtime.state,
|
|
30563
|
+
fetchImpl: input.fetchImpl
|
|
30564
|
+
});
|
|
30565
|
+
if (probe.kind === "running") {
|
|
30566
|
+
throw new Error("Cannot safely stop EvoDev UI because its authentication state is missing. Stop the owning process manually.");
|
|
30567
|
+
}
|
|
30568
|
+
if (probe.kind === "unavailable" && probe.timedOut) {
|
|
30569
|
+
throw new Error("Cannot safely stop EvoDev UI because its health check timed out.");
|
|
30570
|
+
}
|
|
30571
|
+
await removeUiRuntimeState({
|
|
30572
|
+
homeDir: input.homeDir,
|
|
30573
|
+
expectedInstanceId: runtime.state.instanceId
|
|
30574
|
+
});
|
|
30575
|
+
input.write("EvoDev UI: stopped; removed stale runtime state.");
|
|
30576
|
+
return 0;
|
|
30577
|
+
}
|
|
30578
|
+
const shutdown = await requestUiShutdown({
|
|
30579
|
+
state: runtime.state,
|
|
30580
|
+
token: runtime.token,
|
|
30581
|
+
fetchImpl: input.fetchImpl
|
|
30582
|
+
});
|
|
30583
|
+
if (!shutdown.accepted) {
|
|
30584
|
+
const probe = await probeUiRuntime({
|
|
30585
|
+
state: runtime.state,
|
|
30586
|
+
fetchImpl: input.fetchImpl
|
|
30587
|
+
});
|
|
30588
|
+
if (probe.kind === "running") {
|
|
30589
|
+
throw new Error(`Cannot safely stop EvoDev UI: ${shutdown.reason}.`);
|
|
30590
|
+
}
|
|
30591
|
+
if (shutdown.timedOut || probe.kind === "unavailable" && probe.timedOut) {
|
|
30592
|
+
throw new Error("Cannot safely stop EvoDev UI because its shutdown request timed out.");
|
|
30593
|
+
}
|
|
30594
|
+
await removeUiRuntimeState({
|
|
30595
|
+
homeDir: input.homeDir,
|
|
30596
|
+
expectedInstanceId: runtime.state.instanceId,
|
|
30597
|
+
expectedToken: runtime.token
|
|
30598
|
+
});
|
|
30599
|
+
input.write("EvoDev UI: stopped; removed stale runtime state.");
|
|
30600
|
+
return 0;
|
|
30601
|
+
}
|
|
30602
|
+
const stopped = await waitForUiRuntimeRemoval({
|
|
30603
|
+
homeDir: input.homeDir,
|
|
30604
|
+
instanceId: runtime.state.instanceId
|
|
30605
|
+
});
|
|
30606
|
+
if (!stopped)
|
|
30607
|
+
throw new Error("EvoDev UI accepted shutdown but did not stop within 2 seconds.");
|
|
30608
|
+
input.write("EvoDev UI: stopped.");
|
|
30609
|
+
return 0;
|
|
30610
|
+
}
|
|
30611
|
+
async function probeUiRuntime(input) {
|
|
30612
|
+
const controller = new AbortController;
|
|
30613
|
+
const timeout = setTimeout(() => controller.abort(), UI_RUNTIME_REQUEST_TIMEOUT_MS);
|
|
30614
|
+
try {
|
|
30615
|
+
const headers = { accept: "application/json" };
|
|
30616
|
+
if (input.token !== undefined)
|
|
30617
|
+
headers["x-evodev-token"] = input.token;
|
|
30618
|
+
const response = await input.fetchImpl(new URL("/api/health", buildUiRuntimeBaseUrl(input.state)), { headers, signal: controller.signal });
|
|
30619
|
+
if (response.status === 401) {
|
|
30620
|
+
return { kind: "unauthorized", reason: "authentication failed" };
|
|
30621
|
+
}
|
|
30622
|
+
if (!response.ok) {
|
|
30623
|
+
return {
|
|
30624
|
+
kind: "unavailable",
|
|
30625
|
+
reason: `health check returned HTTP ${response.status}`,
|
|
30626
|
+
timedOut: false
|
|
30627
|
+
};
|
|
30628
|
+
}
|
|
30629
|
+
const body = await response.json();
|
|
30630
|
+
const actual = parseUiRuntimeState(JSON.stringify(body.data));
|
|
30631
|
+
if (!isMatchingUiRuntime(input.state, actual)) {
|
|
30632
|
+
return { kind: "unavailable", reason: "runtime identity changed", timedOut: false };
|
|
30633
|
+
}
|
|
30634
|
+
return { kind: "running", state: actual };
|
|
30635
|
+
} catch (error) {
|
|
30636
|
+
const timedOut = controller.signal.aborted;
|
|
30637
|
+
return {
|
|
30638
|
+
kind: "unavailable",
|
|
30639
|
+
reason: timedOut ? "health check timed out" : `health check failed: ${describeError10(error)}`,
|
|
30640
|
+
timedOut
|
|
30641
|
+
};
|
|
30642
|
+
} finally {
|
|
30643
|
+
clearTimeout(timeout);
|
|
30644
|
+
}
|
|
30645
|
+
}
|
|
30646
|
+
async function requestUiShutdown(input) {
|
|
30647
|
+
const controller = new AbortController;
|
|
30648
|
+
const timeout = setTimeout(() => controller.abort(), UI_RUNTIME_REQUEST_TIMEOUT_MS);
|
|
30649
|
+
try {
|
|
30650
|
+
const response = await input.fetchImpl(new URL("/api/shutdown", buildUiRuntimeBaseUrl(input.state)), {
|
|
30651
|
+
method: "POST",
|
|
30652
|
+
headers: {
|
|
30653
|
+
accept: "application/json",
|
|
30654
|
+
"content-type": "application/json",
|
|
30655
|
+
"x-evodev-token": input.token
|
|
30656
|
+
},
|
|
30657
|
+
body: "{}",
|
|
30658
|
+
signal: controller.signal
|
|
30659
|
+
});
|
|
30660
|
+
if (response.status !== 202) {
|
|
30661
|
+
return {
|
|
30662
|
+
accepted: false,
|
|
30663
|
+
reason: response.status === 401 ? "authentication failed" : `shutdown returned HTTP ${response.status}`,
|
|
30664
|
+
timedOut: false
|
|
30665
|
+
};
|
|
30666
|
+
}
|
|
30667
|
+
return { accepted: true };
|
|
30668
|
+
} catch (error) {
|
|
30669
|
+
const timedOut = controller.signal.aborted;
|
|
30670
|
+
return {
|
|
30671
|
+
accepted: false,
|
|
30672
|
+
reason: timedOut ? "shutdown request timed out" : `shutdown failed: ${describeError10(error)}`,
|
|
30673
|
+
timedOut
|
|
30674
|
+
};
|
|
30675
|
+
} finally {
|
|
30676
|
+
clearTimeout(timeout);
|
|
30677
|
+
}
|
|
30678
|
+
}
|
|
30679
|
+
async function waitForUiRuntimeRemoval(input) {
|
|
30680
|
+
const deadline = Date.now() + UI_RUNTIME_REQUEST_TIMEOUT_MS;
|
|
30681
|
+
while (Date.now() < deadline) {
|
|
30682
|
+
const runtime = await readUiRuntimeBundle(input.homeDir);
|
|
30683
|
+
if (runtime.state === null) {
|
|
30684
|
+
if (runtime.token === null)
|
|
30685
|
+
return true;
|
|
30686
|
+
} else if (runtime.state.instanceId !== input.instanceId) {
|
|
30687
|
+
return true;
|
|
30688
|
+
}
|
|
30689
|
+
await delay(25);
|
|
30690
|
+
}
|
|
30691
|
+
return false;
|
|
30692
|
+
}
|
|
30693
|
+
function requireNoUiSubcommandArgs(command, argv) {
|
|
30694
|
+
if (argv.length > 0)
|
|
30695
|
+
throw new Error(`Unknown ui ${command} option: ${argv[0] ?? ""}`.trim());
|
|
30696
|
+
}
|
|
30697
|
+
async function delay(milliseconds) {
|
|
30698
|
+
await new Promise((resolve11) => setTimeout(resolve11, milliseconds));
|
|
30699
|
+
}
|
|
30338
30700
|
async function writeProcessStdout(value) {
|
|
30339
30701
|
await new Promise((resolve11, reject) => {
|
|
30340
30702
|
process.stdout.write(value, (error) => {
|
|
@@ -30347,10 +30709,27 @@ async function writeProcessStdout(value) {
|
|
|
30347
30709
|
}
|
|
30348
30710
|
async function startUiServer(input) {
|
|
30349
30711
|
const token = randomBytes4(32).toString("hex");
|
|
30712
|
+
const instanceId = randomBytes4(16).toString("hex");
|
|
30713
|
+
const existingRuntime = await readUiRuntimeBundle(input.homeDir);
|
|
30714
|
+
if (existingRuntime.state !== null) {
|
|
30715
|
+
const existingProbe = await probeUiRuntime({
|
|
30716
|
+
state: existingRuntime.state,
|
|
30717
|
+
fetchImpl: input.fetchImpl
|
|
30718
|
+
});
|
|
30719
|
+
if (existingProbe.kind === "running") {
|
|
30720
|
+
throw new Error(`EvoDev UI is already running at ${buildUiRuntimeBaseUrl(existingRuntime.state)}. Use evodev ui open or evodev ui stop.`);
|
|
30721
|
+
}
|
|
30722
|
+
if (existingProbe.kind === "unavailable" && existingProbe.timedOut) {
|
|
30723
|
+
throw new Error("Existing EvoDev UI runtime state could not be verified because its health check timed out. Use evodev ui status or stop the owning process manually.");
|
|
30724
|
+
}
|
|
30725
|
+
}
|
|
30350
30726
|
const html = renderUiDashboardHtml();
|
|
30351
30727
|
const webAssets = await loadUiWebAssets();
|
|
30352
30728
|
const proposalExecution = input.proposalExecutionService ?? new RepoProposalExecutionService;
|
|
30353
30729
|
const evolutionJobs = input.evolutionJobService ?? new EvolutionJobService({ homeDir: input.homeDir });
|
|
30730
|
+
let runtimeState = null;
|
|
30731
|
+
let requestClose = null;
|
|
30732
|
+
let shutdownRequested = false;
|
|
30354
30733
|
const server = createServer3(async (request, response) => {
|
|
30355
30734
|
try {
|
|
30356
30735
|
const url2 = new URL(request.url ?? "/", `http://${input.flags.host}:${input.flags.port}`);
|
|
@@ -30396,6 +30775,32 @@ async function startUiServer(input) {
|
|
|
30396
30775
|
response.end(isScript ? webAssets.script : webAssets.styles);
|
|
30397
30776
|
return;
|
|
30398
30777
|
}
|
|
30778
|
+
if (url2.pathname === "/api/health") {
|
|
30779
|
+
if (request.method !== "GET") {
|
|
30780
|
+
writeMethodNotAllowed(response, "GET");
|
|
30781
|
+
return;
|
|
30782
|
+
}
|
|
30783
|
+
if (hasRequestToken(request) && !hasValidRequestToken(request, token)) {
|
|
30784
|
+
writeJsonResponse(response, 401, { ok: false, error: "unauthorized" });
|
|
30785
|
+
return;
|
|
30786
|
+
}
|
|
30787
|
+
if (runtimeState === null) {
|
|
30788
|
+
writeJsonResponse(response, 503, { ok: false, error: "dashboard is starting" });
|
|
30789
|
+
return;
|
|
30790
|
+
}
|
|
30791
|
+
writeJsonResponse(response, 200, { ok: true, data: runtimeState });
|
|
30792
|
+
return;
|
|
30793
|
+
}
|
|
30794
|
+
if (url2.pathname === "/api/shutdown") {
|
|
30795
|
+
requireAuthenticatedJsonPost({ request, response, token });
|
|
30796
|
+
if (response.writableEnded)
|
|
30797
|
+
return;
|
|
30798
|
+
await readRequestJson(request);
|
|
30799
|
+
writeJsonResponse(response, 202, { ok: true, data: { stopping: true } });
|
|
30800
|
+
shutdownRequested = true;
|
|
30801
|
+
queueMicrotask(() => requestClose?.());
|
|
30802
|
+
return;
|
|
30803
|
+
}
|
|
30399
30804
|
if (url2.pathname === "/api/snapshot") {
|
|
30400
30805
|
if (request.method !== "GET") {
|
|
30401
30806
|
writeMethodNotAllowed(response, "GET");
|
|
@@ -30582,7 +30987,7 @@ async function startUiServer(input) {
|
|
|
30582
30987
|
await new Promise((resolve11, reject) => {
|
|
30583
30988
|
const onError = (error) => {
|
|
30584
30989
|
server.off("listening", onListening);
|
|
30585
|
-
reject(error);
|
|
30990
|
+
reject(error.code === "EADDRINUSE" ? new Error(`Cannot start EvoDev UI: ${input.flags.host}:${input.flags.port} is already in use and no running managed UI matched it.`) : error);
|
|
30586
30991
|
};
|
|
30587
30992
|
const onListening = () => {
|
|
30588
30993
|
server.off("error", onError);
|
|
@@ -30594,6 +30999,31 @@ async function startUiServer(input) {
|
|
|
30594
30999
|
});
|
|
30595
31000
|
const address = server.address();
|
|
30596
31001
|
const port = typeof address === "object" && address !== null ? address.port : input.flags.port;
|
|
31002
|
+
if (existingRuntime.state !== null || existingRuntime.token !== null) {
|
|
31003
|
+
await removeUiRuntimeState({
|
|
31004
|
+
homeDir: input.homeDir,
|
|
31005
|
+
expectedInstanceId: existingRuntime.state?.instanceId,
|
|
31006
|
+
expectedToken: existingRuntime.token ?? undefined
|
|
31007
|
+
});
|
|
31008
|
+
}
|
|
31009
|
+
runtimeState = {
|
|
31010
|
+
version: 1,
|
|
31011
|
+
component: "evodev-ui",
|
|
31012
|
+
instanceId,
|
|
31013
|
+
pid: process.pid,
|
|
31014
|
+
host: input.flags.host,
|
|
31015
|
+
port,
|
|
31016
|
+
startedAt: new Date().toISOString()
|
|
31017
|
+
};
|
|
31018
|
+
try {
|
|
31019
|
+
await writeUiRuntimeState({ homeDir: input.homeDir, state: runtimeState, token });
|
|
31020
|
+
} catch (error) {
|
|
31021
|
+
await Promise.all([proposalExecution.shutdown(), evolutionJobs.shutdown()]).catch(() => {
|
|
31022
|
+
return;
|
|
31023
|
+
});
|
|
31024
|
+
await new Promise((resolve11) => server.close(() => resolve11()));
|
|
31025
|
+
throw error;
|
|
31026
|
+
}
|
|
30597
31027
|
const url = `http://${input.flags.host}:${port}/#token=${token}`;
|
|
30598
31028
|
input.write(`EvoDev UI: ${url}`);
|
|
30599
31029
|
input.write("Mode: local-only dashboard; only concrete repo proposals require review. Press Ctrl+C to stop.");
|
|
@@ -30605,7 +31035,13 @@ async function startUiServer(input) {
|
|
|
30605
31035
|
process.off("SIGINT", close);
|
|
30606
31036
|
process.off("SIGTERM", close);
|
|
30607
31037
|
input.signal?.removeEventListener("abort", close);
|
|
30608
|
-
|
|
31038
|
+
removeUiRuntimeState({
|
|
31039
|
+
homeDir: input.homeDir,
|
|
31040
|
+
expectedInstanceId: instanceId,
|
|
31041
|
+
expectedToken: token
|
|
31042
|
+
}).catch((error) => {
|
|
31043
|
+
input.write(`Warning: dashboard stopped, but runtime state cleanup failed: ${describeError10(error)}`);
|
|
31044
|
+
}).finally(() => resolve11(0));
|
|
30609
31045
|
};
|
|
30610
31046
|
const close = () => {
|
|
30611
31047
|
if (closing)
|
|
@@ -30615,10 +31051,11 @@ async function startUiServer(input) {
|
|
|
30615
31051
|
return;
|
|
30616
31052
|
}).finally(() => server.close(finish));
|
|
30617
31053
|
};
|
|
31054
|
+
requestClose = close;
|
|
30618
31055
|
process.once("SIGINT", close);
|
|
30619
31056
|
process.once("SIGTERM", close);
|
|
30620
31057
|
input.signal?.addEventListener("abort", close, { once: true });
|
|
30621
|
-
if (input.signal?.aborted)
|
|
31058
|
+
if (input.signal?.aborted || shutdownRequested)
|
|
30622
31059
|
close();
|
|
30623
31060
|
});
|
|
30624
31061
|
}
|
|
@@ -30668,13 +31105,19 @@ function readHeader2(request, name) {
|
|
|
30668
31105
|
return value ?? null;
|
|
30669
31106
|
}
|
|
30670
31107
|
function hasValidRequestToken(request, expectedToken) {
|
|
30671
|
-
const requestToken =
|
|
31108
|
+
const requestToken = readRequestToken(request);
|
|
30672
31109
|
if (requestToken === null)
|
|
30673
31110
|
return false;
|
|
30674
31111
|
const actual = Buffer.from(requestToken);
|
|
30675
31112
|
const expected = Buffer.from(expectedToken);
|
|
30676
31113
|
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
30677
31114
|
}
|
|
31115
|
+
function hasRequestToken(request) {
|
|
31116
|
+
return readRequestToken(request) !== null;
|
|
31117
|
+
}
|
|
31118
|
+
function readRequestToken(request) {
|
|
31119
|
+
return readHeader2(request, "authorization")?.replace(/^Bearer\s+/i, "") ?? readHeader2(request, "x-evodev-token");
|
|
31120
|
+
}
|
|
30678
31121
|
function writeJsonResponse(response, status, body) {
|
|
30679
31122
|
response.writeHead(status, {
|
|
30680
31123
|
...UI_RESPONSE_HEADERS,
|
|
@@ -30864,10 +31307,13 @@ function openUiBrowser(url, write, spawnProcess = spawn8) {
|
|
|
30864
31307
|
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
30865
31308
|
const child = spawnProcess(command, args, { detached: true, stdio: "ignore" });
|
|
30866
31309
|
child.once("error", (error) => {
|
|
30867
|
-
write(`Warning: dashboard
|
|
31310
|
+
write(`Warning: dashboard URL is available, but the browser could not be opened: ${error.message}`);
|
|
30868
31311
|
});
|
|
30869
31312
|
child.unref();
|
|
30870
31313
|
}
|
|
31314
|
+
function describeError10(error) {
|
|
31315
|
+
return error instanceof Error ? error.message : String(error);
|
|
31316
|
+
}
|
|
30871
31317
|
function resolveHomeDir13(homeDir) {
|
|
30872
31318
|
if (homeDir !== undefined)
|
|
30873
31319
|
return homeDir;
|
|
@@ -30878,7 +31324,7 @@ function resolveHomeDir13(homeDir) {
|
|
|
30878
31324
|
return envHome;
|
|
30879
31325
|
}
|
|
30880
31326
|
// packages/cli/src/workflow.ts
|
|
30881
|
-
import { join as
|
|
31327
|
+
import { join as join37 } from "node:path";
|
|
30882
31328
|
function getWorkflowHelpText() {
|
|
30883
31329
|
return [
|
|
30884
31330
|
"EvoDev workflow",
|
|
@@ -30941,7 +31387,7 @@ function parseWorkflowDryRunFlags(argv) {
|
|
|
30941
31387
|
return { workflowId };
|
|
30942
31388
|
}
|
|
30943
31389
|
function resolveDefaultWorkflowsDir() {
|
|
30944
|
-
return
|
|
31390
|
+
return join37(resolveDefaultAssetsRootDir(), "workflows");
|
|
30945
31391
|
}
|
|
30946
31392
|
function isHelpArg3(arg) {
|
|
30947
31393
|
return arg === undefined || arg === "help" || arg === "--help" || arg === "-h";
|
|
@@ -30978,6 +31424,9 @@ function getHelpText() {
|
|
|
30978
31424
|
" workflow list List built-in workflow manifests",
|
|
30979
31425
|
" workflow dry-run Plan workflow steps without execution",
|
|
30980
31426
|
" ui Start the local-only EvoDev web dashboard",
|
|
31427
|
+
" ui status Show the managed dashboard process status",
|
|
31428
|
+
" ui open Open the authenticated URL for the running dashboard",
|
|
31429
|
+
" ui stop Gracefully stop the running dashboard",
|
|
30981
31430
|
" ui snapshot Print dashboard metadata without starting a server",
|
|
30982
31431
|
" learn review Preview learning candidates without writing memory",
|
|
30983
31432
|
" learn lint Validate learning candidate privacy/provenance fields",
|
|
@@ -31023,7 +31472,7 @@ function getHelpText() {
|
|
|
31023
31472
|
].join(`
|
|
31024
31473
|
`);
|
|
31025
31474
|
}
|
|
31026
|
-
var CLI_VERSION = "0.0.1-alpha.
|
|
31475
|
+
var CLI_VERSION = "0.0.1-alpha.11";
|
|
31027
31476
|
function getVersionText() {
|
|
31028
31477
|
return `evodev ${CLI_VERSION}`;
|
|
31029
31478
|
}
|