@lunora/cli 1.0.0-alpha.1 → 1.0.0-alpha.100
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/LICENSE.md +6 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/bin.mjs +1 -1
- package/dist/index.d.mts +473 -344
- package/dist/index.d.ts +473 -344
- package/dist/index.mjs +10 -11
- package/dist/packem_chunks/handler.mjs +91 -7
- package/dist/packem_chunks/handler10.mjs +8 -14
- package/dist/packem_chunks/handler11.mjs +20 -189
- package/dist/packem_chunks/handler12.mjs +176 -115
- package/dist/packem_chunks/handler13.mjs +118 -52
- package/dist/packem_chunks/handler14.mjs +50 -43
- package/dist/packem_chunks/handler15.mjs +27 -6
- package/dist/packem_chunks/handler16.mjs +6 -3
- package/dist/packem_chunks/handler17.mjs +1 -1
- package/dist/packem_chunks/handler18.mjs +5 -7
- package/dist/packem_chunks/handler19.mjs +44 -5
- package/dist/packem_chunks/handler2.mjs +6 -4
- package/dist/packem_chunks/handler20.mjs +2 -2
- package/dist/packem_chunks/handler21.mjs +158 -267
- package/dist/packem_chunks/handler3.mjs +8 -4
- package/dist/packem_chunks/handler4.mjs +2 -2
- package/dist/packem_chunks/handler5.mjs +7 -4
- package/dist/packem_chunks/handler6.mjs +10 -7
- package/dist/packem_chunks/handler7.mjs +2 -2
- package/dist/packem_chunks/handler8.mjs +1 -1
- package/dist/packem_chunks/handler9.mjs +330 -12
- package/dist/packem_chunks/planDevCommand.mjs +759 -121
- package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
- package/dist/packem_chunks/runDeployCommand.mjs +137 -22
- package/dist/packem_chunks/runInitCommand.mjs +2053 -112
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +98 -51
- package/dist/packem_chunks/runResetCommand.mjs +4 -4
- package/dist/packem_chunks/runRpcCommand.mjs +3 -2
- package/dist/packem_shared/{COMMANDS-1V_KEx35.mjs → COMMANDS-BOJIDoVY.mjs} +154 -28
- package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-D0VOTerB.mjs} +7 -2
- package/dist/packem_shared/{api-spec-CtA6ilu4.mjs → api-spec-Bx0iKbxA.mjs} +3 -1
- package/dist/packem_shared/{buildRegistryIndex-BcYe607_.mjs → buildRegistryIndex-BS5ig822.mjs} +1 -1
- package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +31 -0
- package/dist/packem_shared/{command-BDXcJCCJ.mjs → command-lYnl4QyF.mjs} +6 -1
- package/dist/packem_shared/{runAddCommand-BZGkRnBs.mjs → commands-D5Yxt9VY.mjs} +331 -33
- package/dist/packem_shared/{createLogger-CHPNjFw2.mjs → createLogger-CIWSHrTL.mjs} +40 -8
- package/dist/packem_shared/{defaultSpawner-DxI3mebw.mjs → createRecordingSpawner-WuSn20kb.mjs} +23 -2
- package/dist/packem_shared/detect-package-manager-v4hHpQd0.mjs +62 -0
- package/dist/packem_shared/{diffSnapshots-RR2ZE8Ya.mjs → diffSnapshots-BeDvvNiF.mjs} +1 -1
- package/dist/packem_shared/{insertSchemaExtension-BuzF6-t2.mjs → insertSchemaExtension-DAqbfr9Z.mjs} +15 -10
- package/dist/packem_shared/{open-url-Dfq6fAyT.mjs → open-url-4PBLY9X0.mjs} +3 -2
- package/dist/packem_shared/{output-format-7gyGR3h8.mjs → output-format-B4642rjE.mjs} +1 -1
- package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-Dbp-Q2q3.mjs} +36 -9
- package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
- package/dist/packem_shared/runAddCommand-D1hgfqFQ.mjs +4 -0
- package/dist/packem_shared/{schemaIrToSnapshot-aBTo7TM5.mjs → schemaIrToSnapshot-DdsljJT-.mjs} +1 -1
- package/dist/packem_shared/storage-BXU4ax4O.mjs +84 -0
- package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +658 -0
- package/dist/packem_shared/wrangler-secrets-Coni-mER.mjs +49 -0
- package/package.json +23 -17
- package/skills/lunora-quickstart/SKILL.md +26 -5
- package/skills/lunora-setup-storage/SKILL.md +7 -3
- package/dist/packem_shared/features-ocSSpZtS.mjs +0 -24
- package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync, watch, readFileSync } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
1
|
+
import { spawnSync, spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, watch, readFileSync, statSync, openSync, readSync, closeSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
6
4
|
import { runCodegen } from '@lunora/codegen';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
5
|
+
import { detectAgentRules, claimDevServerState, clearDevServerState, readDevServerState, readLiveDevServerState, detectFramework, readProjectDependencyNames, DEV_HANDOFF_ENV, isRecordedProcessCurrent, DEV_LOG_FILE, DEV_LOG_FILE_ENV, DEV_DAEMON_ENV, updateDevServerState, detectAiAgent, resolveRemoteEnabled, readProjectRemotePreference, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, fillDevSecrets, claimAgentRulesHint, AGENT_RULES_HINT, findWranglerFile, readWranglerJsonc, discoverContainerInfo, streamContainerLogs, materializeRemoteWranglerConfig, formatLunoraEvent } from '@lunora/config';
|
|
6
|
+
import { p as parseApiSpec } from '../packem_shared/api-spec-Bx0iKbxA.mjs';
|
|
7
|
+
import { r as renderCodegenFailure } from '../packem_shared/codegen-error-DJG-ghs_.mjs';
|
|
8
|
+
import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
|
|
9
|
+
import { d as detectPackageManager, e as execArgsFor, r as runScriptCommand } from '../packem_shared/detect-package-manager-v4hHpQd0.mjs';
|
|
10
|
+
import { createServer, connect } from 'node:net';
|
|
11
|
+
import { forceJsonLogging } from '../packem_shared/createLogger-CIWSHrTL.mjs';
|
|
12
|
+
import { networkInterfaces } from 'node:os';
|
|
13
|
+
import { spawnShellCompat } from '../packem_shared/createRecordingSpawner-WuSn20kb.mjs';
|
|
14
|
+
import { createServer as createServer$1, request } from 'node:http';
|
|
15
|
+
import { loadStudioAssets, studioAssetsStamp, renderStudioHtml, resolveAdminToken, SCHEMA_EDIT_ENDPOINT, POLICY_SCAFFOLD_ENDPOINT, SEED_ENDPOINT, serveJsonHandler, isStandaloneModulePath, readStandaloneAsset, assetContentType, handleSchemaEditRequest, handlePolicyScaffoldRequest, handleSeedRequest } from '@lunora/config/studio-host';
|
|
16
|
+
import { c as createTuiConfirm } from '../packem_shared/tui-prompts-BjEN8XgP.mjs';
|
|
17
|
+
import { p as printJson } from '../packem_shared/output-format-B4642rjE.mjs';
|
|
12
18
|
|
|
13
19
|
const DEFAULT_DEBOUNCE_MS = 100;
|
|
14
20
|
const PATH_SEGMENT_SEPARATOR = /[/\\]/u;
|
|
@@ -17,7 +23,7 @@ const runOnce = (projectRoot, lunoraDirectory, apiSpec, logger, reason) => {
|
|
|
17
23
|
runCodegen({ apiSpec, lunoraDirectory, projectRoot });
|
|
18
24
|
logger.success(`codegen: wrote ${lunoraDirectory}/_generated (${reason})`);
|
|
19
25
|
} catch (error) {
|
|
20
|
-
logger.error(
|
|
26
|
+
logger.error(renderCodegenFailure(error, reason));
|
|
21
27
|
}
|
|
22
28
|
};
|
|
23
29
|
const startCodegenWatch = (options) => {
|
|
@@ -70,50 +76,32 @@ const startCodegenWatch = (options) => {
|
|
|
70
76
|
};
|
|
71
77
|
};
|
|
72
78
|
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
};
|
|
93
|
-
const detectPackageManager = (startDirectory) => {
|
|
94
|
-
let directory = startDirectory;
|
|
95
|
-
while (directory && directory !== dirname(directory)) {
|
|
96
|
-
const declared = readDeclaredManager(directory);
|
|
97
|
-
if (declared !== void 0) {
|
|
98
|
-
return declared;
|
|
79
|
+
const DEFAULT_PROBE_ATTEMPTS = 64;
|
|
80
|
+
const LOOPBACK_HOST = "127.0.0.1";
|
|
81
|
+
const MAX_PORT = 65535;
|
|
82
|
+
const isPortFree = (port, host = LOOPBACK_HOST) => new Promise((resolve) => {
|
|
83
|
+
const server = createServer();
|
|
84
|
+
server.once("error", () => {
|
|
85
|
+
resolve(false);
|
|
86
|
+
});
|
|
87
|
+
server.once("listening", () => {
|
|
88
|
+
server.close(() => {
|
|
89
|
+
resolve(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
server.listen(port, host);
|
|
93
|
+
});
|
|
94
|
+
const findAvailablePort = async (preferred, host = LOOPBACK_HOST, attempts = DEFAULT_PROBE_ATTEMPTS) => {
|
|
95
|
+
for (let port = preferred; port < preferred + attempts && port <= MAX_PORT; port += 1) {
|
|
96
|
+
if (await isPortFree(port, host)) {
|
|
97
|
+
return port;
|
|
99
98
|
}
|
|
100
|
-
directory = dirname(directory);
|
|
101
99
|
}
|
|
102
|
-
return
|
|
103
|
-
};
|
|
104
|
-
const execArgsFor = (manager, command, args) => {
|
|
105
|
-
if (manager === "yarn") {
|
|
106
|
-
return { args: [command, ...args], command: "yarn" };
|
|
107
|
-
}
|
|
108
|
-
if (manager === "bun") {
|
|
109
|
-
return { args: ["x", command, ...args], command: "bun" };
|
|
110
|
-
}
|
|
111
|
-
if (manager === "npm") {
|
|
112
|
-
return { args: ["--", command, ...args], command: "npx" };
|
|
113
|
-
}
|
|
114
|
-
return { args: ["exec", command, ...args], command: "pnpm" };
|
|
100
|
+
return preferred;
|
|
115
101
|
};
|
|
116
102
|
|
|
103
|
+
const hasIpv6Loopback = (readInterfaces = networkInterfaces) => Object.values(readInterfaces()).some((addresses) => addresses?.some((address) => address.internal && address.address === "::1"));
|
|
104
|
+
|
|
117
105
|
const PROXY_PREFIX = "/_lunora";
|
|
118
106
|
const pathnameOf = (url) => {
|
|
119
107
|
const queryIndex = url.indexOf("?");
|
|
@@ -208,7 +196,9 @@ const startStudioServer = async (options) => {
|
|
|
208
196
|
response.end(deniedMessage);
|
|
209
197
|
};
|
|
210
198
|
const serveStaticAsset = (pathname, response) => {
|
|
211
|
-
|
|
199
|
+
const isStyle = pathname === "/styles.css";
|
|
200
|
+
const isModule = isStandaloneModulePath(pathname);
|
|
201
|
+
if (!isStyle && !isModule) {
|
|
212
202
|
return false;
|
|
213
203
|
}
|
|
214
204
|
const stamp = studioAssetsStamp(import.meta.url);
|
|
@@ -222,12 +212,23 @@ const startStudioServer = async (options) => {
|
|
|
222
212
|
response.end("Lunora studio assets not found — install and build @lunora/studio.");
|
|
223
213
|
return true;
|
|
224
214
|
}
|
|
225
|
-
|
|
226
|
-
|
|
215
|
+
if (isStyle) {
|
|
216
|
+
sendAsset(response, assets.styles, "text/css; charset=utf-8");
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
const fileName = pathname.slice(pathname.lastIndexOf("/") + 1);
|
|
220
|
+
const bytes = readStandaloneAsset(fileName, import.meta.url);
|
|
221
|
+
if (bytes === void 0) {
|
|
222
|
+
response.statusCode = 404;
|
|
223
|
+
response.setHeader("Content-Type", "text/plain");
|
|
224
|
+
response.end("Not found");
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
sendAsset(response, bytes, assetContentType(fileName));
|
|
227
228
|
return true;
|
|
228
229
|
};
|
|
229
230
|
const document = Buffer.from(html);
|
|
230
|
-
const server = createServer((request, response) => {
|
|
231
|
+
const server = createServer$1((request, response) => {
|
|
231
232
|
const pathname = pathnameOf(request.url ?? "/");
|
|
232
233
|
if (isLoopback && !isLoopbackHost(request.headers.host)) {
|
|
233
234
|
response.statusCode = 403;
|
|
@@ -299,8 +300,416 @@ const startStudioServer = async (options) => {
|
|
|
299
300
|
};
|
|
300
301
|
};
|
|
301
302
|
|
|
303
|
+
const DEFAULT_READY_TIMEOUT_MS = 12e4;
|
|
304
|
+
const POLL_INTERVAL_MS = 250;
|
|
305
|
+
const STOP_GRACE_MS = 1e4;
|
|
306
|
+
const DEFAULT_LOG_LINES = 100;
|
|
307
|
+
const FAILURE_LOG_TAIL_LINES = 40;
|
|
308
|
+
const LOG_TAIL_MAX_BYTES = 256 * 1024;
|
|
309
|
+
const READY_TIMEOUT_ENV = "LUNORA_DEV_READY_TIMEOUT_MS";
|
|
310
|
+
const SIDECAR_FRAMEWORKS = /* @__PURE__ */ new Set(["nuxt", "sveltekit"]);
|
|
311
|
+
const detectDevFlavor = (cwd) => {
|
|
312
|
+
if (SIDECAR_FRAMEWORKS.has(detectFramework(cwd).framework)) {
|
|
313
|
+
return "framework-worker";
|
|
314
|
+
}
|
|
315
|
+
return readProjectDependencyNames(cwd).has("@lunora/vite") ? "vite" : "wrangler";
|
|
316
|
+
};
|
|
317
|
+
const viteDevCommand = (cwd) => {
|
|
318
|
+
const manager = detectPackageManager(cwd);
|
|
319
|
+
let script;
|
|
320
|
+
try {
|
|
321
|
+
const raw = readFileSync(join(cwd, "package.json"), "utf8");
|
|
322
|
+
script = JSON.parse(raw).scripts?.dev;
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
if (script === void 0 || script.trim() === "" || script.includes("lunora")) {
|
|
326
|
+
const exec = execArgsFor(manager, "vite", ["dev"]);
|
|
327
|
+
return { args: exec.args, command: exec.command };
|
|
328
|
+
}
|
|
329
|
+
return { args: ["run", "dev"], command: manager };
|
|
330
|
+
};
|
|
331
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
332
|
+
setTimeout(resolve, ms);
|
|
333
|
+
});
|
|
334
|
+
const defaultProbe = async (origin) => {
|
|
335
|
+
try {
|
|
336
|
+
await fetch(new URL("/_lunora/status", origin), { signal: AbortSignal.timeout(1e3) });
|
|
337
|
+
return true;
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const defaultDetachedSpawner = (descriptor) => {
|
|
343
|
+
mkdirSync(dirname(descriptor.logPath), { recursive: true });
|
|
344
|
+
const logFd = openSync(descriptor.logPath, "w", 384);
|
|
345
|
+
let child;
|
|
346
|
+
const exec = spawnShellCompat(descriptor.command, descriptor.args);
|
|
347
|
+
try {
|
|
348
|
+
child = spawn(exec.command, exec.args, {
|
|
349
|
+
cwd: descriptor.cwd,
|
|
350
|
+
detached: true,
|
|
351
|
+
env: descriptor.env,
|
|
352
|
+
shell: exec.shell,
|
|
353
|
+
stdio: ["ignore", logFd, logFd],
|
|
354
|
+
windowsHide: true
|
|
355
|
+
});
|
|
356
|
+
} finally {
|
|
357
|
+
closeSync(logFd);
|
|
358
|
+
}
|
|
359
|
+
child.unref();
|
|
360
|
+
return {
|
|
361
|
+
exited: new Promise((resolve) => {
|
|
362
|
+
child.on("error", () => {
|
|
363
|
+
resolve(1);
|
|
364
|
+
});
|
|
365
|
+
child.on("exit", (code, signal) => {
|
|
366
|
+
resolve(code ?? (signal ? 1 : 0));
|
|
367
|
+
});
|
|
368
|
+
}),
|
|
369
|
+
pid: child.pid
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
const readLogWindow = (path) => {
|
|
373
|
+
try {
|
|
374
|
+
const { size } = statSync(path);
|
|
375
|
+
if (size <= LOG_TAIL_MAX_BYTES) {
|
|
376
|
+
return readFileSync(path, "utf8");
|
|
377
|
+
}
|
|
378
|
+
const fd = openSync(path, "r");
|
|
379
|
+
try {
|
|
380
|
+
const buffer = Buffer.alloc(LOG_TAIL_MAX_BYTES);
|
|
381
|
+
const read = readSync(fd, buffer, 0, LOG_TAIL_MAX_BYTES, size - LOG_TAIL_MAX_BYTES);
|
|
382
|
+
const text = buffer.toString("utf8", 0, read);
|
|
383
|
+
return text.slice(text.indexOf("\n") + 1);
|
|
384
|
+
} finally {
|
|
385
|
+
closeSync(fd);
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
return void 0;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const readLogTail = (path, count) => {
|
|
392
|
+
const text = readLogWindow(path);
|
|
393
|
+
if (text === void 0) {
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
const lines = text.split("\n");
|
|
397
|
+
if (lines.at(-1) === "") {
|
|
398
|
+
lines.pop();
|
|
399
|
+
}
|
|
400
|
+
return count > 0 ? lines.slice(-count) : lines;
|
|
401
|
+
};
|
|
402
|
+
const resolveLogPath = (cwd, state) => state?.logFile ?? join(cwd, DEV_LOG_FILE);
|
|
403
|
+
const printLifecycleHints = (logger) => {
|
|
404
|
+
logger.info(" Stop: lunora dev stop");
|
|
405
|
+
logger.info(" Status: lunora dev status");
|
|
406
|
+
logger.info(" Logs: lunora dev logs");
|
|
407
|
+
};
|
|
408
|
+
const reportExistingServer = (logger, existing) => {
|
|
409
|
+
logger.warn(`Dev server already running at ${existing.url} (pid ${String(existing.pid)})`);
|
|
410
|
+
printLifecycleHints(logger);
|
|
411
|
+
};
|
|
412
|
+
const printBackgroundBanner = (logger, state) => {
|
|
413
|
+
logger.success(`Dev server running at ${state.url} (pid ${String(state.pid)})`);
|
|
414
|
+
if (state.studioUrl !== void 0) {
|
|
415
|
+
logger.info(` Studio: ${state.studioUrl}`);
|
|
416
|
+
}
|
|
417
|
+
printLifecycleHints(logger);
|
|
418
|
+
};
|
|
419
|
+
const waitUntilReady = async (parameters) => {
|
|
420
|
+
const { child, cwd, deadline, pollIntervalMs, probe } = parameters;
|
|
421
|
+
const exited = child.exited.then((code) => {
|
|
422
|
+
return { exitCode: code };
|
|
423
|
+
});
|
|
424
|
+
while (Date.now() < deadline) {
|
|
425
|
+
const state = readLiveDevServerState(cwd);
|
|
426
|
+
if (state !== void 0 && state.pid !== process.pid && await probe(state.url)) {
|
|
427
|
+
return { state, status: "ready" };
|
|
428
|
+
}
|
|
429
|
+
const raced = await Promise.race([exited, sleep(pollIntervalMs)]);
|
|
430
|
+
if (raced !== void 0) {
|
|
431
|
+
return { ...raced, status: "exited" };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { status: "timeout" };
|
|
435
|
+
};
|
|
436
|
+
const runDevBackground = async (options) => {
|
|
437
|
+
const { cwd, logger } = options;
|
|
438
|
+
const logPath = resolveLogPath(cwd, void 0);
|
|
439
|
+
const environmentTimeout = Number(process.env[READY_TIMEOUT_ENV] ?? "");
|
|
440
|
+
const timeout = options.readyTimeoutMs ?? (Number.isFinite(environmentTimeout) && environmentTimeout > 0 ? environmentTimeout : DEFAULT_READY_TIMEOUT_MS);
|
|
441
|
+
const child = (options.spawnDetached ?? defaultDetachedSpawner)({
|
|
442
|
+
args: options.command.args,
|
|
443
|
+
command: options.command.command,
|
|
444
|
+
cwd,
|
|
445
|
+
env: {
|
|
446
|
+
...process.env,
|
|
447
|
+
...options.env,
|
|
448
|
+
[DEV_DAEMON_ENV]: "1",
|
|
449
|
+
[DEV_LOG_FILE_ENV]: logPath,
|
|
450
|
+
// The capture log is read back by `lunora dev logs` (and often an
|
|
451
|
+
// agent) — ANSI colour codes would only garble it.
|
|
452
|
+
...process.env.FORCE_COLOR === void 0 ? { NO_COLOR: "1" } : {},
|
|
453
|
+
...options.json ? { LUNORA_LOG_JSON: "1" } : {}
|
|
454
|
+
},
|
|
455
|
+
logPath
|
|
456
|
+
});
|
|
457
|
+
const outcome = await waitUntilReady({
|
|
458
|
+
child,
|
|
459
|
+
cwd,
|
|
460
|
+
deadline: Date.now() + timeout,
|
|
461
|
+
pollIntervalMs: options.pollIntervalMs ?? POLL_INTERVAL_MS,
|
|
462
|
+
probe: options.probe ?? defaultProbe
|
|
463
|
+
});
|
|
464
|
+
if (outcome.status === "ready") {
|
|
465
|
+
printBackgroundBanner(logger, outcome.state);
|
|
466
|
+
return { code: 0 };
|
|
467
|
+
}
|
|
468
|
+
if (outcome.status === "exited") {
|
|
469
|
+
logger.error(`dev server exited before becoming ready (exit code ${String(outcome.exitCode)}). Last output:`);
|
|
470
|
+
for (const line of readLogTail(logPath, FAILURE_LOG_TAIL_LINES)) {
|
|
471
|
+
logger.error(` ${line}`);
|
|
472
|
+
}
|
|
473
|
+
clearDevServerState(cwd, child.pid ?? process.pid);
|
|
474
|
+
return { code: outcome.exitCode === 0 ? 1 : outcome.exitCode };
|
|
475
|
+
}
|
|
476
|
+
if (child.pid !== void 0) {
|
|
477
|
+
const current = readDevServerState(cwd);
|
|
478
|
+
if (current?.pid === process.pid) {
|
|
479
|
+
updateDevServerState(cwd, { logFile: logPath, pid: child.pid });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const pidHint = child.pid === void 0 ? "" : ` (pid ${String(child.pid)})`;
|
|
483
|
+
logger.warn(
|
|
484
|
+
`dev server did not confirm ready within ${String(Math.round(timeout / 1e3))}s — it may still be compiling${pidHint}. Check \`lunora dev status\` and \`lunora dev logs\`; \`lunora dev stop\` shuts it down.`
|
|
485
|
+
);
|
|
486
|
+
return { code: 1 };
|
|
487
|
+
};
|
|
488
|
+
const daemonArguments = (options, remote) => {
|
|
489
|
+
const args = ["dev"];
|
|
490
|
+
if (options.apiSpec !== void 0) {
|
|
491
|
+
args.push("--api-spec", options.apiSpec);
|
|
492
|
+
}
|
|
493
|
+
if (options.port !== void 0) {
|
|
494
|
+
args.push("--port", String(options.port));
|
|
495
|
+
}
|
|
496
|
+
if (options.workerPort !== void 0) {
|
|
497
|
+
args.push("--worker-port", String(options.workerPort));
|
|
498
|
+
}
|
|
499
|
+
if (options.codegen === false) {
|
|
500
|
+
args.push("--no-codegen");
|
|
501
|
+
}
|
|
502
|
+
if (options.studio === false) {
|
|
503
|
+
args.push("--no-studio");
|
|
504
|
+
}
|
|
505
|
+
if (remote) {
|
|
506
|
+
args.push("--remote");
|
|
507
|
+
}
|
|
508
|
+
return args;
|
|
509
|
+
};
|
|
510
|
+
const startBackground = async (context) => {
|
|
511
|
+
const { cwd, jsonLogs, logger, options, remote } = context;
|
|
512
|
+
const run = context.run ?? runDevBackground;
|
|
513
|
+
const flavor = detectDevFlavor(cwd);
|
|
514
|
+
const url = flavor === "wrangler" ? `http://localhost:${String(options.workerPort ?? 8787)}` : "http://localhost:5173";
|
|
515
|
+
const claim = claimDevServerState(cwd, {
|
|
516
|
+
background: true,
|
|
517
|
+
mode: "cli",
|
|
518
|
+
pid: process.pid,
|
|
519
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
520
|
+
url
|
|
521
|
+
});
|
|
522
|
+
if (!claim.ok) {
|
|
523
|
+
if (claim.existing !== void 0) {
|
|
524
|
+
reportExistingServer(logger, claim.existing);
|
|
525
|
+
}
|
|
526
|
+
return { code: 0 };
|
|
527
|
+
}
|
|
528
|
+
const handoff = { [DEV_HANDOFF_ENV]: String(process.pid) };
|
|
529
|
+
try {
|
|
530
|
+
if (flavor === "vite") {
|
|
531
|
+
return await run({
|
|
532
|
+
command: viteDevCommand(cwd),
|
|
533
|
+
cwd,
|
|
534
|
+
env: { ...remote ? { LUNORA_REMOTE: "1" } : {}, ...handoff },
|
|
535
|
+
json: jsonLogs,
|
|
536
|
+
logger
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return await run({
|
|
540
|
+
command: { args: [process.argv[1] ?? "lunora", ...daemonArguments(options, remote)], command: process.execPath },
|
|
541
|
+
cwd,
|
|
542
|
+
env: handoff,
|
|
543
|
+
json: jsonLogs,
|
|
544
|
+
logger
|
|
545
|
+
});
|
|
546
|
+
} finally {
|
|
547
|
+
clearDevServerState(cwd, process.pid);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
const processGroupId = (pid) => {
|
|
551
|
+
try {
|
|
552
|
+
const result = spawnSync("ps", ["-o", "pgid=", "-p", String(pid)], { encoding: "utf8" });
|
|
553
|
+
const pgid = Number.parseInt(result.stdout.trim(), 10);
|
|
554
|
+
return Number.isInteger(pgid) && pgid > 0 ? pgid : void 0;
|
|
555
|
+
} catch {
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
const forceKillRecordedServer = (state, signal, platform = process.platform, spawnSyncImpl = spawnSync) => {
|
|
560
|
+
if (platform === "win32") {
|
|
561
|
+
try {
|
|
562
|
+
spawnSyncImpl("taskkill", ["/pid", String(state.pid), "/T", "/F"], { stdio: "ignore" });
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (state.background === true) {
|
|
568
|
+
try {
|
|
569
|
+
signal(-(processGroupId(state.pid) ?? state.pid), "SIGKILL");
|
|
570
|
+
return;
|
|
571
|
+
} catch {
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
signal(state.pid, "SIGKILL");
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
const reportSurvivedForceKill = (state, options) => {
|
|
580
|
+
if (options.json) {
|
|
581
|
+
printJson({ pid: state.pid, stopped: false });
|
|
582
|
+
} else {
|
|
583
|
+
options.logger.error(
|
|
584
|
+
`dev server (pid ${String(state.pid)}) survived the force-kill — record kept; retry \`lunora dev stop\` or kill the process manually.`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return { code: 1 };
|
|
588
|
+
};
|
|
589
|
+
const runDevStop = async (options) => {
|
|
590
|
+
const { cwd, logger } = options;
|
|
591
|
+
const state = readDevServerState(cwd);
|
|
592
|
+
const alive = options.alive ?? ((pid) => pid === state?.pid && isRecordedProcessCurrent(state));
|
|
593
|
+
const signal = options.signal ?? ((pid, sig) => {
|
|
594
|
+
process.kill(pid, sig);
|
|
595
|
+
});
|
|
596
|
+
const pollInterval = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
597
|
+
const grace = options.stopGraceMs ?? STOP_GRACE_MS;
|
|
598
|
+
const platform = options.platform ?? process.platform;
|
|
599
|
+
const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
|
|
600
|
+
if (state === void 0 || !alive(state.pid)) {
|
|
601
|
+
if (state !== void 0) {
|
|
602
|
+
clearDevServerState(cwd, state.pid);
|
|
603
|
+
}
|
|
604
|
+
if (options.json) {
|
|
605
|
+
printJson({ running: false, stopped: false });
|
|
606
|
+
} else {
|
|
607
|
+
logger.info("No dev server running.");
|
|
608
|
+
}
|
|
609
|
+
return { code: 0 };
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
signal(state.pid, "SIGTERM");
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
const deadline = Date.now() + grace;
|
|
616
|
+
while (alive(state.pid) && Date.now() < deadline) {
|
|
617
|
+
await sleep(pollInterval);
|
|
618
|
+
}
|
|
619
|
+
if (alive(state.pid)) {
|
|
620
|
+
forceKillRecordedServer(state, signal, platform, spawnSyncImpl);
|
|
621
|
+
await sleep(pollInterval);
|
|
622
|
+
if (alive(state.pid)) {
|
|
623
|
+
return reportSurvivedForceKill(state, options);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
clearDevServerState(cwd, state.pid);
|
|
627
|
+
if (options.json) {
|
|
628
|
+
printJson({ pid: state.pid, stopped: true });
|
|
629
|
+
} else {
|
|
630
|
+
logger.success(`Stopped dev server (pid ${String(state.pid)}).`);
|
|
631
|
+
}
|
|
632
|
+
return { code: 0 };
|
|
633
|
+
};
|
|
634
|
+
const runDevStatus = (options) => {
|
|
635
|
+
const { cwd, logger } = options;
|
|
636
|
+
const state = readLiveDevServerState(cwd);
|
|
637
|
+
if (state === void 0) {
|
|
638
|
+
if (options.json) {
|
|
639
|
+
printJson({ running: false });
|
|
640
|
+
} else {
|
|
641
|
+
logger.info("No dev server running.");
|
|
642
|
+
}
|
|
643
|
+
return { code: 0 };
|
|
644
|
+
}
|
|
645
|
+
const now = options.now ?? Date.now;
|
|
646
|
+
const startedAtMs = state.startedAt === void 0 ? Number.NaN : Date.parse(state.startedAt);
|
|
647
|
+
const uptimeSeconds = Number.isFinite(startedAtMs) ? Math.max(0, Math.round((now() - startedAtMs) / 1e3)) : void 0;
|
|
648
|
+
if (options.json) {
|
|
649
|
+
printJson({
|
|
650
|
+
background: state.background === true,
|
|
651
|
+
logFile: state.logFile,
|
|
652
|
+
mode: state.mode,
|
|
653
|
+
pid: state.pid,
|
|
654
|
+
running: true,
|
|
655
|
+
startedAt: state.startedAt,
|
|
656
|
+
studioUrl: state.studioUrl,
|
|
657
|
+
uptimeSeconds,
|
|
658
|
+
url: state.url
|
|
659
|
+
});
|
|
660
|
+
return { code: 0 };
|
|
661
|
+
}
|
|
662
|
+
const details = [
|
|
663
|
+
`pid ${String(state.pid)}`,
|
|
664
|
+
...uptimeSeconds === void 0 ? [] : [`uptime ${String(uptimeSeconds)}s`],
|
|
665
|
+
state.background === true ? "background" : "foreground"
|
|
666
|
+
];
|
|
667
|
+
logger.success(`Dev server running at ${state.url} (${details.join(", ")})`);
|
|
668
|
+
if (state.studioUrl !== void 0) {
|
|
669
|
+
logger.info(` Studio: ${state.studioUrl}`);
|
|
670
|
+
}
|
|
671
|
+
if (state.logFile !== void 0) {
|
|
672
|
+
logger.info(` Logs: lunora dev logs (${state.logFile})`);
|
|
673
|
+
}
|
|
674
|
+
return { code: 0 };
|
|
675
|
+
};
|
|
676
|
+
const runDevLogs = (options) => {
|
|
677
|
+
const { cwd, logger } = options;
|
|
678
|
+
const state = readDevServerState(cwd);
|
|
679
|
+
const logPath = resolveLogPath(cwd, state);
|
|
680
|
+
if (!existsSync(logPath)) {
|
|
681
|
+
logger.info("No dev server logs found — output is captured when the server runs in background mode (`lunora dev --background`).");
|
|
682
|
+
return { code: 0 };
|
|
683
|
+
}
|
|
684
|
+
const tail = readLogTail(logPath, options.lines ?? DEFAULT_LOG_LINES);
|
|
685
|
+
if (tail.length > 0) {
|
|
686
|
+
process.stdout.write(`${tail.join("\n")}
|
|
687
|
+
`);
|
|
688
|
+
}
|
|
689
|
+
return { code: 0 };
|
|
690
|
+
};
|
|
691
|
+
const runLifecycleSubcommand = (parameters) => {
|
|
692
|
+
const { cwd, json, lines, logger, subcommand } = parameters;
|
|
693
|
+
if (subcommand === "stop") {
|
|
694
|
+
return runDevStop({ cwd, json, logger });
|
|
695
|
+
}
|
|
696
|
+
if (subcommand === "status") {
|
|
697
|
+
return runDevStatus({ cwd, json, logger });
|
|
698
|
+
}
|
|
699
|
+
if (subcommand === "logs") {
|
|
700
|
+
return runDevLogs({ cwd, lines, logger });
|
|
701
|
+
}
|
|
702
|
+
if (subcommand !== void 0) {
|
|
703
|
+
logger.error(`dev: unknown subcommand "${subcommand}" — expected stop | status | logs (or no subcommand to start the dev server)`);
|
|
704
|
+
return { code: 1 };
|
|
705
|
+
}
|
|
706
|
+
return void 0;
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const DEV_WRANGLER_CONFIG = "wrangler.dev.jsonc";
|
|
302
710
|
const DEFAULT_STUDIO_PORT = 6173;
|
|
303
711
|
const DEFAULT_WORKER_PORT = 8787;
|
|
712
|
+
const DEFAULT_VITE_PORT = 5173;
|
|
304
713
|
const SIGINT_GRACE_MS = 5e3;
|
|
305
714
|
const resolveRemotePlan = (options, cwd) => {
|
|
306
715
|
const noopCleanup = () => {
|
|
@@ -317,14 +726,76 @@ const resolveRemotePlan = (options, cwd) => {
|
|
|
317
726
|
}
|
|
318
727
|
return { args: ["--config", result.configPath], plan: { bindings, cleanup, enabled: true } };
|
|
319
728
|
};
|
|
729
|
+
const readDevIp = (wranglerPath) => readWranglerJsonc(wranglerPath).parsed?.dev?.ip;
|
|
730
|
+
const resolveLoopbackArgs = (cwd, hasLoopback, sidecarConfigFile) => {
|
|
731
|
+
if (sidecarConfigFile !== void 0) {
|
|
732
|
+
const sidecarConfigPath = join(cwd, sidecarConfigFile);
|
|
733
|
+
if (existsSync(sidecarConfigPath) && readDevIp(sidecarConfigPath) !== void 0) {
|
|
734
|
+
return [];
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const wranglerPath = findWranglerFile(cwd);
|
|
738
|
+
if (wranglerPath !== void 0 && readDevIp(wranglerPath) !== void 0) {
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
return hasLoopback() ? [] : ["--ip", "127.0.0.1"];
|
|
742
|
+
};
|
|
743
|
+
const resolveWorkerPort = async (options, cwd) => {
|
|
744
|
+
if (options.workerPort !== void 0) {
|
|
745
|
+
return options.workerPort;
|
|
746
|
+
}
|
|
747
|
+
const wranglerPath = findWranglerFile(cwd);
|
|
748
|
+
if (wranglerPath !== void 0) {
|
|
749
|
+
const { parsed } = readWranglerJsonc(wranglerPath);
|
|
750
|
+
if (typeof parsed?.dev?.port === "number") {
|
|
751
|
+
return parsed.dev.port;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return (options.findFreePort ?? findAvailablePort)(DEFAULT_WORKER_PORT);
|
|
755
|
+
};
|
|
320
756
|
const planDevCommand = (options) => {
|
|
321
757
|
const cwd = options.cwd ?? process.cwd();
|
|
322
|
-
const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
|
|
323
758
|
const manager = detectPackageManager(cwd);
|
|
759
|
+
const flavor = options.flavor ?? detectDevFlavor(cwd);
|
|
760
|
+
if (flavor === "vite" || flavor === "framework-worker") {
|
|
761
|
+
const exec2 = viteDevCommand(cwd);
|
|
762
|
+
let sidecar;
|
|
763
|
+
if (flavor === "framework-worker") {
|
|
764
|
+
const loopbackArgs2 = resolveLoopbackArgs(cwd, options.hasIpv6Loopback ?? hasIpv6Loopback, DEV_WRANGLER_CONFIG);
|
|
765
|
+
const sidecarExec = execArgsFor(manager, "wrangler", ["dev", "--config", DEV_WRANGLER_CONFIG, ...loopbackArgs2, "--var", "WORKER_ENV:development"]);
|
|
766
|
+
sidecar = { args: sidecarExec.args, command: sidecarExec.command, cwd, tag: "worker" };
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
codegenEnabled: false,
|
|
770
|
+
flavor,
|
|
771
|
+
ipv4LoopbackForced: false,
|
|
772
|
+
remote: { bindings: [], cleanup: () => {
|
|
773
|
+
}, enabled: options.remote === true },
|
|
774
|
+
...sidecar ? { sidecar } : {},
|
|
775
|
+
studioEnabled: false,
|
|
776
|
+
studioPort: options.port ?? DEFAULT_STUDIO_PORT,
|
|
777
|
+
workerOrigin: `http://localhost:${String(DEFAULT_VITE_PORT)}`,
|
|
778
|
+
workerPort: DEFAULT_VITE_PORT,
|
|
779
|
+
wrangler: {
|
|
780
|
+
args: exec2.args,
|
|
781
|
+
command: exec2.command,
|
|
782
|
+
cwd,
|
|
783
|
+
...options.remote === true ? { env: { LUNORA_REMOTE: "1" } } : {},
|
|
784
|
+
tag: "vite"
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
const detection = detectFramework(cwd);
|
|
789
|
+
const frameworkHint = detection.framework === "none" ? void 0 : `this project uses ${detection.framework} — the worker runs inside Vite there. run \`${runScriptCommand(manager, "dev")}\` for the full app (frontend + HMR); \`lunora dev\` starts only the worker.`;
|
|
790
|
+
const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
|
|
324
791
|
const remote = resolveRemotePlan(options, cwd);
|
|
325
|
-
const
|
|
792
|
+
const loopbackArgs = resolveLoopbackArgs(cwd, options.hasIpv6Loopback ?? hasIpv6Loopback);
|
|
793
|
+
const exec = execArgsFor(manager, "wrangler", ["dev", "--port", String(workerPort), ...loopbackArgs, "--var", "WORKER_ENV:development", ...remote.args]);
|
|
326
794
|
return {
|
|
327
795
|
codegenEnabled: options.codegen !== false,
|
|
796
|
+
flavor,
|
|
797
|
+
frameworkHint,
|
|
798
|
+
ipv4LoopbackForced: loopbackArgs.length > 0,
|
|
328
799
|
remote: remote.plan,
|
|
329
800
|
studioEnabled: options.studio !== false,
|
|
330
801
|
studioPort: options.port ?? DEFAULT_STUDIO_PORT,
|
|
@@ -372,9 +843,11 @@ const pipeChildOutput = (child, tag, logger) => {
|
|
|
372
843
|
pumpStream(child.stderr, "stderr");
|
|
373
844
|
};
|
|
374
845
|
const defaultWorkerSpawner = (descriptor, logger) => {
|
|
375
|
-
const
|
|
846
|
+
const exec = spawnShellCompat(descriptor.command, descriptor.args);
|
|
847
|
+
const child = spawn(exec.command, exec.args, {
|
|
376
848
|
cwd: descriptor.cwd ?? process.cwd(),
|
|
377
|
-
env: process.env,
|
|
849
|
+
env: descriptor.env ? { ...process.env, ...descriptor.env } : process.env,
|
|
850
|
+
shell: exec.shell,
|
|
378
851
|
stdio: ["inherit", "pipe", "pipe"]
|
|
379
852
|
});
|
|
380
853
|
pipeChildOutput(child, descriptor.tag, logger);
|
|
@@ -422,8 +895,35 @@ const printAgentRulesHint = (logger, cwd) => {
|
|
|
422
895
|
logger.info(` ⓘ ${AGENT_RULES_HINT}`);
|
|
423
896
|
logger.info("");
|
|
424
897
|
};
|
|
898
|
+
const startContainerLogStreaming = (cwd, logger) => {
|
|
899
|
+
if (process.env.LUNORA_CONTAINER_LOGS === "0") {
|
|
900
|
+
return void 0;
|
|
901
|
+
}
|
|
902
|
+
const discovery = discoverContainerInfo(cwd, "lunora");
|
|
903
|
+
const containers = discovery.containers.map((container) => {
|
|
904
|
+
return { className: container.className, exportName: container.exportName };
|
|
905
|
+
});
|
|
906
|
+
if (containers.length === 0) {
|
|
907
|
+
return void 0;
|
|
908
|
+
}
|
|
909
|
+
return streamContainerLogs({
|
|
910
|
+
containers,
|
|
911
|
+
onLine: (line) => {
|
|
912
|
+
const tagged = `[container:${line.name}] ${line.text}`;
|
|
913
|
+
if (line.level === "error") {
|
|
914
|
+
logger.warn(tagged);
|
|
915
|
+
} else {
|
|
916
|
+
logger.info(tagged);
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
onUnavailable: (message) => {
|
|
920
|
+
logger.warn(`[container] Docker engine unreachable — container logs unavailable (${message})`);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
};
|
|
425
924
|
const teardown = async (handles) => {
|
|
426
925
|
handles.codegen?.close();
|
|
926
|
+
handles.containerLogs?.close();
|
|
427
927
|
await handles.studio?.close().catch(() => void 0);
|
|
428
928
|
try {
|
|
429
929
|
handles.remoteCleanup?.();
|
|
@@ -441,7 +941,7 @@ const offerDevVariablesScaffold = async (options, cwd) => {
|
|
|
441
941
|
} catch {
|
|
442
942
|
}
|
|
443
943
|
const result = await (options.ensureEnv ?? ensureDevVariables)({
|
|
444
|
-
confirm:
|
|
944
|
+
confirm: createTuiConfirm(),
|
|
445
945
|
cwd,
|
|
446
946
|
info: (message) => {
|
|
447
947
|
options.logger.info(message);
|
|
@@ -452,72 +952,218 @@ const offerDevVariablesScaffold = async (options, cwd) => {
|
|
|
452
952
|
`hint: ${DEV_VARS_FILE} was not scaffolded (non-interactive run). Copy ${DEV_VARS_EXAMPLE_FILE} → ${DEV_VARS_FILE} and fill in secrets, or run \`lunora dev\` in an interactive terminal to scaffold automatically.`
|
|
453
953
|
);
|
|
454
954
|
}
|
|
955
|
+
try {
|
|
956
|
+
(options.fillSecrets ?? fillDevSecrets)({
|
|
957
|
+
cwd,
|
|
958
|
+
info: (message) => {
|
|
959
|
+
options.logger.info(message);
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
} catch {
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
const afterWorkerSpawn = (plan, cwd, logger, studioUrl) => {
|
|
966
|
+
if (plan.flavor !== "wrangler") {
|
|
967
|
+
return void 0;
|
|
968
|
+
}
|
|
969
|
+
if (studioUrl !== void 0) {
|
|
970
|
+
updateDevServerState(cwd, { studioUrl });
|
|
971
|
+
}
|
|
972
|
+
let containerLogs;
|
|
973
|
+
try {
|
|
974
|
+
containerLogs = startContainerLogStreaming(cwd, logger);
|
|
975
|
+
} catch {
|
|
976
|
+
}
|
|
977
|
+
printBanner(logger, plan, studioUrl);
|
|
978
|
+
return containerLogs;
|
|
979
|
+
};
|
|
980
|
+
const claimStartRecord = (plan, cwd) => {
|
|
981
|
+
const handoffPid = Number(process.env[DEV_HANDOFF_ENV]);
|
|
982
|
+
const claim = claimDevServerState(
|
|
983
|
+
cwd,
|
|
984
|
+
{
|
|
985
|
+
background: process.env[DEV_DAEMON_ENV] === "1",
|
|
986
|
+
logFile: process.env[DEV_LOG_FILE_ENV],
|
|
987
|
+
mode: "cli",
|
|
988
|
+
pid: process.pid,
|
|
989
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
990
|
+
url: plan.workerOrigin
|
|
991
|
+
},
|
|
992
|
+
Number.isInteger(handoffPid) && handoffPid > 0 ? { supersedePid: handoffPid } : void 0
|
|
993
|
+
);
|
|
994
|
+
return claim.ok ? void 0 : claim.existing;
|
|
995
|
+
};
|
|
996
|
+
const buildDevPlan = async (options) => {
|
|
997
|
+
const cwd = options.cwd ?? process.cwd();
|
|
998
|
+
const flavor = options.flavor ?? detectDevFlavor(cwd);
|
|
999
|
+
const workerPort = flavor === "wrangler" ? await resolveWorkerPort(options, cwd) : options.workerPort;
|
|
1000
|
+
return planDevCommand({ ...options, cwd, flavor, workerPort });
|
|
1001
|
+
};
|
|
1002
|
+
const superviseWorkers = async (worker, sidecar, logger) => {
|
|
1003
|
+
let sigintCount = 0;
|
|
1004
|
+
let escalationTimer;
|
|
1005
|
+
const killChildren = (signal) => {
|
|
1006
|
+
worker.kill(signal);
|
|
1007
|
+
sidecar?.kill(signal);
|
|
1008
|
+
};
|
|
1009
|
+
const onSigint = () => {
|
|
1010
|
+
sigintCount += 1;
|
|
1011
|
+
if (sigintCount === 1) {
|
|
1012
|
+
logger.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)");
|
|
1013
|
+
killChildren("SIGTERM");
|
|
1014
|
+
escalationTimer = setTimeout(() => {
|
|
1015
|
+
killChildren("SIGKILL");
|
|
1016
|
+
}, SIGINT_GRACE_MS);
|
|
1017
|
+
escalationTimer.unref();
|
|
1018
|
+
} else {
|
|
1019
|
+
killChildren("SIGKILL");
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
const onSigterm = () => {
|
|
1023
|
+
killChildren("SIGTERM");
|
|
1024
|
+
};
|
|
1025
|
+
process.on("SIGINT", onSigint);
|
|
1026
|
+
process.on("SIGTERM", onSigterm);
|
|
1027
|
+
const exits = [
|
|
1028
|
+
worker.exited.then((code) => {
|
|
1029
|
+
return { code, who: "worker" };
|
|
1030
|
+
})
|
|
1031
|
+
];
|
|
1032
|
+
if (sidecar !== void 0) {
|
|
1033
|
+
exits.push(
|
|
1034
|
+
sidecar.exited.then((code) => {
|
|
1035
|
+
return { code, who: "sidecar" };
|
|
1036
|
+
})
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
const first = await Promise.race(exits);
|
|
1040
|
+
if (sidecar !== void 0) {
|
|
1041
|
+
if (first.who === "sidecar") {
|
|
1042
|
+
logger.warn("[worker] the Lunora sidecar (wrangler dev) exited — shutting down the framework dev server");
|
|
1043
|
+
worker.kill("SIGTERM");
|
|
1044
|
+
} else {
|
|
1045
|
+
sidecar.kill("SIGTERM");
|
|
1046
|
+
}
|
|
1047
|
+
await Promise.allSettled([worker.exited, sidecar.exited]);
|
|
1048
|
+
}
|
|
1049
|
+
if (escalationTimer) {
|
|
1050
|
+
clearTimeout(escalationTimer);
|
|
1051
|
+
}
|
|
1052
|
+
process.off("SIGINT", onSigint);
|
|
1053
|
+
process.off("SIGTERM", onSigterm);
|
|
1054
|
+
return first.code;
|
|
1055
|
+
};
|
|
1056
|
+
const startStudioBestEffort = async (options, plan, cwd, logger) => {
|
|
1057
|
+
if (!plan.studioEnabled) {
|
|
1058
|
+
return void 0;
|
|
1059
|
+
}
|
|
1060
|
+
try {
|
|
1061
|
+
return await (options.startStudio ?? startStudioServer)({
|
|
1062
|
+
cwd,
|
|
1063
|
+
logger: {
|
|
1064
|
+
warnOnce: (message) => {
|
|
1065
|
+
logger.warn(message);
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
port: plan.studioPort,
|
|
1069
|
+
workerOrigin: plan.workerOrigin
|
|
1070
|
+
});
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
logger.warn(`studio server failed to start (${error instanceof Error ? error.message : String(error)}) — continuing without it`);
|
|
1073
|
+
return void 0;
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
const ensureSidecarGenerated = (plan, options, cwd, logger) => {
|
|
1077
|
+
if (plan.sidecar === void 0) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
runCodegen({ apiSpec: options.apiSpec, lunoraDirectory: "lunora", projectRoot: cwd });
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
logger.warn(`codegen (pre-sidecar) failed: ${error instanceof Error ? error.message : String(error)} — the framework dev server will retry`);
|
|
1084
|
+
}
|
|
455
1085
|
};
|
|
456
1086
|
const runDevCommand = async (options) => {
|
|
457
|
-
const plan =
|
|
1087
|
+
const plan = await buildDevPlan(options);
|
|
458
1088
|
const { logger } = options;
|
|
459
1089
|
const cwd = plan.wrangler.cwd ?? process.cwd();
|
|
460
1090
|
const handles = { remoteCleanup: plan.remote.cleanup };
|
|
461
1091
|
try {
|
|
1092
|
+
const handoffPid = Number(process.env[DEV_HANDOFF_ENV]);
|
|
1093
|
+
const existing = readLiveDevServerState(cwd);
|
|
1094
|
+
if (existing !== void 0 && existing.pid !== process.pid && existing.pid !== handoffPid) {
|
|
1095
|
+
reportExistingServer(logger, existing);
|
|
1096
|
+
return { code: 0, plan };
|
|
1097
|
+
}
|
|
1098
|
+
const incumbent = claimStartRecord(plan, cwd);
|
|
1099
|
+
if (incumbent !== void 0) {
|
|
1100
|
+
reportExistingServer(logger, incumbent);
|
|
1101
|
+
return { code: 0, plan };
|
|
1102
|
+
}
|
|
1103
|
+
if (plan.flavor === "vite" || plan.flavor === "framework-worker") {
|
|
1104
|
+
plan.wrangler.env = { ...plan.wrangler.env, [DEV_HANDOFF_ENV]: String(process.pid) };
|
|
1105
|
+
}
|
|
462
1106
|
await offerDevVariablesScaffold(options, cwd);
|
|
463
|
-
logger.info(
|
|
1107
|
+
logger.info(
|
|
1108
|
+
plan.flavor === "vite" ? "starting vite dev (worker + studio + codegen run inside Vite via @lunora/vite)" : "starting wrangler dev + studio"
|
|
1109
|
+
);
|
|
1110
|
+
if (plan.ipv4LoopbackForced) {
|
|
1111
|
+
logger.info(
|
|
1112
|
+
"no IPv6 loopback (::1) on this host — binding the worker to 127.0.0.1 (--ip) so wrangler dev doesn't crash. Pin `dev.ip` in wrangler.jsonc to override."
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
464
1115
|
if (plan.codegenEnabled) {
|
|
465
1116
|
handles.codegen = (options.startCodegen ?? startCodegenWatch)({ apiSpec: options.apiSpec, logger, projectRoot: cwd });
|
|
466
1117
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
cwd,
|
|
472
|
-
logger: {
|
|
473
|
-
warnOnce: (message) => {
|
|
474
|
-
logger.warn(message);
|
|
475
|
-
}
|
|
476
|
-
},
|
|
477
|
-
port: plan.studioPort,
|
|
478
|
-
workerOrigin: plan.workerOrigin
|
|
479
|
-
});
|
|
480
|
-
studioUrl = handles.studio.url;
|
|
481
|
-
} catch (error) {
|
|
482
|
-
logger.warn(`studio server failed to start (${error instanceof Error ? error.message : String(error)}) — continuing without it`);
|
|
483
|
-
}
|
|
1118
|
+
handles.studio = await startStudioBestEffort(options, plan, cwd, logger);
|
|
1119
|
+
const studioUrl = handles.studio?.url;
|
|
1120
|
+
if (plan.frameworkHint !== void 0) {
|
|
1121
|
+
logger.warn(plan.frameworkHint);
|
|
484
1122
|
}
|
|
485
|
-
|
|
486
|
-
|
|
1123
|
+
ensureSidecarGenerated(plan, options, cwd, logger);
|
|
1124
|
+
const spawn = options.startWorker ?? defaultWorkerSpawner;
|
|
1125
|
+
const worker = spawn(plan.wrangler, logger);
|
|
1126
|
+
const sidecar = plan.sidecar === void 0 ? void 0 : spawn(plan.sidecar, logger);
|
|
1127
|
+
handles.containerLogs = afterWorkerSpawn(plan, cwd, logger, studioUrl);
|
|
487
1128
|
printAgentRulesHint(logger, cwd);
|
|
488
|
-
|
|
489
|
-
let escalationTimer;
|
|
490
|
-
const onSigint = () => {
|
|
491
|
-
sigintCount += 1;
|
|
492
|
-
if (sigintCount === 1) {
|
|
493
|
-
logger.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)");
|
|
494
|
-
worker.kill("SIGTERM");
|
|
495
|
-
escalationTimer = setTimeout(() => {
|
|
496
|
-
worker.kill("SIGKILL");
|
|
497
|
-
}, SIGINT_GRACE_MS);
|
|
498
|
-
escalationTimer.unref();
|
|
499
|
-
} else {
|
|
500
|
-
worker.kill("SIGKILL");
|
|
501
|
-
}
|
|
502
|
-
};
|
|
503
|
-
const onSigterm = () => {
|
|
504
|
-
worker.kill("SIGTERM");
|
|
505
|
-
};
|
|
506
|
-
process.on("SIGINT", onSigint);
|
|
507
|
-
process.on("SIGTERM", onSigterm);
|
|
508
|
-
const code = await worker.exited;
|
|
509
|
-
if (escalationTimer) {
|
|
510
|
-
clearTimeout(escalationTimer);
|
|
511
|
-
}
|
|
512
|
-
process.off("SIGINT", onSigint);
|
|
513
|
-
process.off("SIGTERM", onSigterm);
|
|
1129
|
+
const code = await superviseWorkers(worker, sidecar, logger);
|
|
514
1130
|
return { code, plan };
|
|
515
1131
|
} finally {
|
|
1132
|
+
clearDevServerState(cwd, process.pid);
|
|
516
1133
|
await teardown(handles);
|
|
517
1134
|
}
|
|
518
1135
|
};
|
|
519
|
-
const execute = defineHandler(
|
|
520
|
-
|
|
1136
|
+
const execute = defineHandler(async ({ argument, cwd, logger, options }) => {
|
|
1137
|
+
const json = options.json === true;
|
|
1138
|
+
const dispatched = runLifecycleSubcommand({ cwd, json, lines: options.lines, logger, subcommand: argument[0] });
|
|
1139
|
+
if (dispatched !== void 0) {
|
|
1140
|
+
return dispatched;
|
|
1141
|
+
}
|
|
1142
|
+
const isDaemon = process.env[DEV_DAEMON_ENV] === "1";
|
|
1143
|
+
const agent = isDaemon ? void 0 : detectAiAgent();
|
|
1144
|
+
const jsonLogs = json || agent !== void 0;
|
|
1145
|
+
if (jsonLogs) {
|
|
1146
|
+
forceJsonLogging();
|
|
1147
|
+
}
|
|
1148
|
+
if (agent !== void 0 && options.background !== true) {
|
|
1149
|
+
logger.info(
|
|
1150
|
+
`AI agent detected (${agent.name} via ${agent.variable}) — starting the dev server in background mode with JSON logs. Set LUNORA_AGENT_MODE=0 to opt out.`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
const remote = resolveRemoteEnabled({
|
|
1154
|
+
configPreference: readProjectRemotePreference(cwd),
|
|
1155
|
+
envValue: process.env["LUNORA_REMOTE"],
|
|
1156
|
+
flag: options.remote
|
|
1157
|
+
});
|
|
1158
|
+
if (!isDaemon && (options.background === true || agent !== void 0)) {
|
|
1159
|
+
const existing = readLiveDevServerState(cwd);
|
|
1160
|
+
if (existing !== void 0) {
|
|
1161
|
+
reportExistingServer(logger, existing);
|
|
1162
|
+
return { code: 0 };
|
|
1163
|
+
}
|
|
1164
|
+
return startBackground({ cwd, jsonLogs, logger, options, remote });
|
|
1165
|
+
}
|
|
1166
|
+
return runDevCommand({
|
|
521
1167
|
apiSpec: parseApiSpec(options.apiSpec),
|
|
522
1168
|
// cerebro parses `--no-codegen`/`--no-studio` as the negation of the
|
|
523
1169
|
// `codegen`/`studio` booleans (runtime key drops the `no-` prefix), so a
|
|
@@ -526,18 +1172,10 @@ const execute = defineHandler(
|
|
|
526
1172
|
cwd,
|
|
527
1173
|
logger,
|
|
528
1174
|
port: options.port,
|
|
529
|
-
|
|
530
|
-
// flag wins, then `LUNORA_REMOTE` in the environment, then the `remote`
|
|
531
|
-
// key in the project's `lunora.json` (a project default). See
|
|
532
|
-
// `resolveRemoteEnabled` in @lunora/config.
|
|
533
|
-
remote: resolveRemoteEnabled({
|
|
534
|
-
configPreference: readProjectRemotePreference(cwd),
|
|
535
|
-
envValue: process.env["LUNORA_REMOTE"],
|
|
536
|
-
flag: options.remote
|
|
537
|
-
}),
|
|
1175
|
+
remote,
|
|
538
1176
|
studio: options.studio === false ? false : void 0,
|
|
539
1177
|
workerPort: options.workerPort
|
|
540
|
-
})
|
|
541
|
-
);
|
|
1178
|
+
});
|
|
1179
|
+
});
|
|
542
1180
|
|
|
543
|
-
export { execute, planDevCommand, resolveRemotePlan, runDevCommand };
|
|
1181
|
+
export { detectDevFlavor, execute, planDevCommand, resolveRemotePlan, resolveWorkerPort, runDevCommand };
|