@lunora/cli 1.0.0-alpha.8 → 1.0.0-alpha.81

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.
Files changed (60) hide show
  1. package/LICENSE.md +6 -0
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/bin.mjs +1 -1
  4. package/dist/index.d.mts +293 -134
  5. package/dist/index.d.ts +293 -134
  6. package/dist/index.mjs +8 -9
  7. package/dist/packem_chunks/handler.mjs +87 -6
  8. package/dist/packem_chunks/handler10.mjs +8 -14
  9. package/dist/packem_chunks/handler11.mjs +20 -189
  10. package/dist/packem_chunks/handler12.mjs +176 -115
  11. package/dist/packem_chunks/handler13.mjs +118 -52
  12. package/dist/packem_chunks/handler14.mjs +50 -43
  13. package/dist/packem_chunks/handler15.mjs +48 -67
  14. package/dist/packem_chunks/handler16.mjs +85 -37
  15. package/dist/packem_chunks/handler17.mjs +39 -100
  16. package/dist/packem_chunks/handler18.mjs +87 -152
  17. package/dist/packem_chunks/handler19.mjs +148 -67
  18. package/dist/packem_chunks/handler2.mjs +6 -4
  19. package/dist/packem_chunks/handler20.mjs +77 -75
  20. package/dist/packem_chunks/handler21.mjs +71 -288
  21. package/dist/packem_chunks/handler3.mjs +8 -4
  22. package/dist/packem_chunks/handler4.mjs +2 -2
  23. package/dist/packem_chunks/handler5.mjs +7 -4
  24. package/dist/packem_chunks/handler6.mjs +10 -7
  25. package/dist/packem_chunks/handler7.mjs +2 -2
  26. package/dist/packem_chunks/handler8.mjs +1 -1
  27. package/dist/packem_chunks/handler9.mjs +323 -12
  28. package/dist/packem_chunks/planDevCommand.mjs +758 -81
  29. package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
  30. package/dist/packem_chunks/runDeployCommand.mjs +126 -20
  31. package/dist/packem_chunks/runInitCommand.mjs +1661 -179
  32. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +47 -41
  33. package/dist/packem_chunks/runResetCommand.mjs +2 -2
  34. package/dist/packem_chunks/runRpcCommand.mjs +3 -2
  35. package/dist/packem_shared/{COMMANDS-Bn8luojF.mjs → COMMANDS-D0tmnAwr.mjs} +106 -27
  36. package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-D0VOTerB.mjs} +7 -2
  37. package/dist/packem_shared/{api-spec-CtA6ilu4.mjs → api-spec-Bx0iKbxA.mjs} +3 -1
  38. package/dist/packem_shared/{buildRegistryIndex-BcYe607_.mjs → buildRegistryIndex-BS5ig822.mjs} +1 -1
  39. package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +31 -0
  40. package/dist/packem_shared/{command-BDXcJCCJ.mjs → command-lYnl4QyF.mjs} +6 -1
  41. package/dist/packem_shared/{commands-DqsEzojt.mjs → commands-ClEvcz3V.mjs} +224 -18
  42. package/dist/packem_shared/{createLogger-CHPNjFw2.mjs → createLogger-CIWSHrTL.mjs} +40 -8
  43. package/dist/packem_shared/{createRecordingSpawner-DxI3mebw.mjs → createRecordingSpawner-WuSn20kb.mjs} +23 -2
  44. package/dist/packem_shared/{detect-package-manager-DYp7n3mJ.mjs → detect-package-manager-DvEthdCw.mjs} +26 -30
  45. package/dist/packem_shared/{insertSchemaExtension-BuzF6-t2.mjs → insertSchemaExtension-DAqbfr9Z.mjs} +15 -10
  46. package/dist/packem_shared/{open-url-Dfq6fAyT.mjs → open-url-4PBLY9X0.mjs} +3 -2
  47. package/dist/packem_shared/{output-format-7gyGR3h8.mjs → output-format-B4642rjE.mjs} +1 -1
  48. package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-Dbp-Q2q3.mjs} +36 -9
  49. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
  50. package/dist/packem_shared/runAddCommand-DGjFXGqa.mjs +4 -0
  51. package/dist/packem_shared/storage-DEXpJkXh.mjs +84 -0
  52. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +658 -0
  53. package/dist/packem_shared/wrangler-secrets-Dq_Fkbm-.mjs +49 -0
  54. package/package.json +16 -13
  55. package/skills/lunora-quickstart/SKILL.md +25 -5
  56. package/skills/lunora-setup-storage/SKILL.md +7 -3
  57. package/dist/packem_shared/features-ocSSpZtS.mjs +0 -24
  58. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
  59. package/dist/packem_shared/runAddCommand-G544_v6e.mjs +0 -4
  60. package/dist/packem_shared/tui-prompts-XHFxlOg5.mjs +0 -269
@@ -1,15 +1,20 @@
1
- import { spawn } from 'node:child_process';
2
- import { detectAgentRules, resolveRemoteEnabled, readProjectRemotePreference, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, claimAgentRulesHint, AGENT_RULES_HINT, materializeRemoteWranglerConfig, formatLunoraEvent } from '@lunora/config';
3
- import { p as parseApiSpec } from '../packem_shared/api-spec-CtA6ilu4.mjs';
4
- import { existsSync, watch } from 'node:fs';
5
- import { join } from 'node:path';
1
+ import { spawnSync, spawn } from 'node:child_process';
6
2
  import { runCodegen } from '@lunora/codegen';
7
- import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
8
- import { d as detectPackageManager, e as execArgsFor } from '../packem_shared/detect-package-manager-DYp7n3mJ.mjs';
9
- import { createServer, request } from 'node:http';
10
- import { connect } from 'node:net';
11
- import { loadStudioAssets, studioAssetsStamp, renderStudioHtml, resolveAdminToken, SCHEMA_EDIT_ENDPOINT, POLICY_SCAFFOLD_ENDPOINT, SEED_ENDPOINT, serveJsonHandler, handleSchemaEditRequest, handlePolicyScaffoldRequest, handleSeedRequest } from '@lunora/config/studio-host';
12
- import { c as createTuiConfirm } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
3
+ 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';
4
+ import { p as parseApiSpec } from '../packem_shared/api-spec-Bx0iKbxA.mjs';
5
+ import { existsSync, watch, readFileSync, statSync, openSync, readSync, closeSync, mkdirSync } from 'node:fs';
6
+ import { join, dirname } from 'node:path';
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-DvEthdCw.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';
13
18
 
14
19
  const DEFAULT_DEBOUNCE_MS = 100;
15
20
  const PATH_SEGMENT_SEPARATOR = /[/\\]/u;
@@ -18,7 +23,7 @@ const runOnce = (projectRoot, lunoraDirectory, apiSpec, logger, reason) => {
18
23
  runCodegen({ apiSpec, lunoraDirectory, projectRoot });
19
24
  logger.success(`codegen: wrote ${lunoraDirectory}/_generated (${reason})`);
20
25
  } catch (error) {
21
- logger.error(`codegen failed (${reason}): ${error instanceof Error ? error.message : String(error)}`);
26
+ logger.error(renderCodegenFailure(error, reason));
22
27
  }
23
28
  };
24
29
  const startCodegenWatch = (options) => {
@@ -71,6 +76,32 @@ const startCodegenWatch = (options) => {
71
76
  };
72
77
  };
73
78
 
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;
98
+ }
99
+ }
100
+ return preferred;
101
+ };
102
+
103
+ const hasIpv6Loopback = (readInterfaces = networkInterfaces) => Object.values(readInterfaces()).some((addresses) => addresses?.some((address) => address.internal && address.address === "::1"));
104
+
74
105
  const PROXY_PREFIX = "/_lunora";
75
106
  const pathnameOf = (url) => {
76
107
  const queryIndex = url.indexOf("?");
@@ -165,7 +196,9 @@ const startStudioServer = async (options) => {
165
196
  response.end(deniedMessage);
166
197
  };
167
198
  const serveStaticAsset = (pathname, response) => {
168
- if (pathname !== "/studio.js" && pathname !== "/styles.css") {
199
+ const isStyle = pathname === "/styles.css";
200
+ const isModule = isStandaloneModulePath(pathname);
201
+ if (!isStyle && !isModule) {
169
202
  return false;
170
203
  }
171
204
  const stamp = studioAssetsStamp(import.meta.url);
@@ -179,12 +212,23 @@ const startStudioServer = async (options) => {
179
212
  response.end("Lunora studio assets not found — install and build @lunora/studio.");
180
213
  return true;
181
214
  }
182
- const isScript = pathname === "/studio.js";
183
- sendAsset(response, isScript ? assets.script : assets.styles, isScript ? "text/javascript; charset=utf-8" : "text/css; charset=utf-8");
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));
184
228
  return true;
185
229
  };
186
230
  const document = Buffer.from(html);
187
- const server = createServer((request, response) => {
231
+ const server = createServer$1((request, response) => {
188
232
  const pathname = pathnameOf(request.url ?? "/");
189
233
  if (isLoopback && !isLoopbackHost(request.headers.host)) {
190
234
  response.statusCode = 403;
@@ -256,8 +300,416 @@ const startStudioServer = async (options) => {
256
300
  };
257
301
  };
258
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";
259
710
  const DEFAULT_STUDIO_PORT = 6173;
260
711
  const DEFAULT_WORKER_PORT = 8787;
712
+ const DEFAULT_VITE_PORT = 5173;
261
713
  const SIGINT_GRACE_MS = 5e3;
262
714
  const resolveRemotePlan = (options, cwd) => {
263
715
  const noopCleanup = () => {
@@ -274,14 +726,72 @@ const resolveRemotePlan = (options, cwd) => {
274
726
  }
275
727
  return { args: ["--config", result.configPath], plan: { bindings, cleanup, enabled: true } };
276
728
  };
729
+ const resolveLoopbackArgs = (cwd, hasLoopback) => {
730
+ const wranglerPath = findWranglerFile(cwd);
731
+ if (wranglerPath !== void 0) {
732
+ const { parsed } = readWranglerJsonc(wranglerPath);
733
+ if (parsed?.dev?.ip !== void 0) {
734
+ return [];
735
+ }
736
+ }
737
+ return hasLoopback() ? [] : ["--ip", "127.0.0.1"];
738
+ };
739
+ const resolveWorkerPort = async (options, cwd) => {
740
+ if (options.workerPort !== void 0) {
741
+ return options.workerPort;
742
+ }
743
+ const wranglerPath = findWranglerFile(cwd);
744
+ if (wranglerPath !== void 0) {
745
+ const { parsed } = readWranglerJsonc(wranglerPath);
746
+ if (typeof parsed?.dev?.port === "number") {
747
+ return parsed.dev.port;
748
+ }
749
+ }
750
+ return (options.findFreePort ?? findAvailablePort)(DEFAULT_WORKER_PORT);
751
+ };
277
752
  const planDevCommand = (options) => {
278
753
  const cwd = options.cwd ?? process.cwd();
279
- const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
280
754
  const manager = detectPackageManager(cwd);
755
+ const flavor = options.flavor ?? detectDevFlavor(cwd);
756
+ if (flavor === "vite" || flavor === "framework-worker") {
757
+ const exec2 = viteDevCommand(cwd);
758
+ let sidecar;
759
+ if (flavor === "framework-worker") {
760
+ const loopbackArgs2 = resolveLoopbackArgs(cwd, options.hasIpv6Loopback ?? hasIpv6Loopback);
761
+ const sidecarExec = execArgsFor(manager, "wrangler", ["dev", "--config", DEV_WRANGLER_CONFIG, ...loopbackArgs2, "--var", "WORKER_ENV:development"]);
762
+ sidecar = { args: sidecarExec.args, command: sidecarExec.command, cwd, tag: "worker" };
763
+ }
764
+ return {
765
+ codegenEnabled: false,
766
+ flavor,
767
+ ipv4LoopbackForced: false,
768
+ remote: { bindings: [], cleanup: () => {
769
+ }, enabled: options.remote === true },
770
+ ...sidecar ? { sidecar } : {},
771
+ studioEnabled: false,
772
+ studioPort: options.port ?? DEFAULT_STUDIO_PORT,
773
+ workerOrigin: `http://localhost:${String(DEFAULT_VITE_PORT)}`,
774
+ workerPort: DEFAULT_VITE_PORT,
775
+ wrangler: {
776
+ args: exec2.args,
777
+ command: exec2.command,
778
+ cwd,
779
+ ...options.remote === true ? { env: { LUNORA_REMOTE: "1" } } : {},
780
+ tag: "vite"
781
+ }
782
+ };
783
+ }
784
+ const detection = detectFramework(cwd);
785
+ 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.`;
786
+ const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
281
787
  const remote = resolveRemotePlan(options, cwd);
282
- const exec = execArgsFor(manager, "wrangler", ["dev", "--port", String(workerPort), "--var", "WORKER_ENV:development", ...remote.args]);
788
+ const loopbackArgs = resolveLoopbackArgs(cwd, options.hasIpv6Loopback ?? hasIpv6Loopback);
789
+ const exec = execArgsFor(manager, "wrangler", ["dev", "--port", String(workerPort), ...loopbackArgs, "--var", "WORKER_ENV:development", ...remote.args]);
283
790
  return {
284
791
  codegenEnabled: options.codegen !== false,
792
+ flavor,
793
+ frameworkHint,
794
+ ipv4LoopbackForced: loopbackArgs.length > 0,
285
795
  remote: remote.plan,
286
796
  studioEnabled: options.studio !== false,
287
797
  studioPort: options.port ?? DEFAULT_STUDIO_PORT,
@@ -329,9 +839,11 @@ const pipeChildOutput = (child, tag, logger) => {
329
839
  pumpStream(child.stderr, "stderr");
330
840
  };
331
841
  const defaultWorkerSpawner = (descriptor, logger) => {
332
- const child = spawn(descriptor.command, [...descriptor.args], {
842
+ const exec = spawnShellCompat(descriptor.command, descriptor.args);
843
+ const child = spawn(exec.command, exec.args, {
333
844
  cwd: descriptor.cwd ?? process.cwd(),
334
- env: process.env,
845
+ env: descriptor.env ? { ...process.env, ...descriptor.env } : process.env,
846
+ shell: exec.shell,
335
847
  stdio: ["inherit", "pipe", "pipe"]
336
848
  });
337
849
  pipeChildOutput(child, descriptor.tag, logger);
@@ -379,8 +891,35 @@ const printAgentRulesHint = (logger, cwd) => {
379
891
  logger.info(` ⓘ ${AGENT_RULES_HINT}`);
380
892
  logger.info("");
381
893
  };
894
+ const startContainerLogStreaming = (cwd, logger) => {
895
+ if (process.env.LUNORA_CONTAINER_LOGS === "0") {
896
+ return void 0;
897
+ }
898
+ const discovery = discoverContainerInfo(cwd, "lunora");
899
+ const containers = discovery.containers.map((container) => {
900
+ return { className: container.className, exportName: container.exportName };
901
+ });
902
+ if (containers.length === 0) {
903
+ return void 0;
904
+ }
905
+ return streamContainerLogs({
906
+ containers,
907
+ onLine: (line) => {
908
+ const tagged = `[container:${line.name}] ${line.text}`;
909
+ if (line.level === "error") {
910
+ logger.warn(tagged);
911
+ } else {
912
+ logger.info(tagged);
913
+ }
914
+ },
915
+ onUnavailable: (message) => {
916
+ logger.warn(`[container] Docker engine unreachable — container logs unavailable (${message})`);
917
+ }
918
+ });
919
+ };
382
920
  const teardown = async (handles) => {
383
921
  handles.codegen?.close();
922
+ handles.containerLogs?.close();
384
923
  await handles.studio?.close().catch(() => void 0);
385
924
  try {
386
925
  handles.remoteCleanup?.();
@@ -409,72 +948,218 @@ const offerDevVariablesScaffold = async (options, cwd) => {
409
948
  `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.`
410
949
  );
411
950
  }
951
+ try {
952
+ (options.fillSecrets ?? fillDevSecrets)({
953
+ cwd,
954
+ info: (message) => {
955
+ options.logger.info(message);
956
+ }
957
+ });
958
+ } catch {
959
+ }
960
+ };
961
+ const afterWorkerSpawn = (plan, cwd, logger, studioUrl) => {
962
+ if (plan.flavor !== "wrangler") {
963
+ return void 0;
964
+ }
965
+ if (studioUrl !== void 0) {
966
+ updateDevServerState(cwd, { studioUrl });
967
+ }
968
+ let containerLogs;
969
+ try {
970
+ containerLogs = startContainerLogStreaming(cwd, logger);
971
+ } catch {
972
+ }
973
+ printBanner(logger, plan, studioUrl);
974
+ return containerLogs;
975
+ };
976
+ const claimStartRecord = (plan, cwd) => {
977
+ const handoffPid = Number(process.env[DEV_HANDOFF_ENV]);
978
+ const claim = claimDevServerState(
979
+ cwd,
980
+ {
981
+ background: process.env[DEV_DAEMON_ENV] === "1",
982
+ logFile: process.env[DEV_LOG_FILE_ENV],
983
+ mode: "cli",
984
+ pid: process.pid,
985
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
986
+ url: plan.workerOrigin
987
+ },
988
+ Number.isInteger(handoffPid) && handoffPid > 0 ? { supersedePid: handoffPid } : void 0
989
+ );
990
+ return claim.ok ? void 0 : claim.existing;
991
+ };
992
+ const buildDevPlan = async (options) => {
993
+ const cwd = options.cwd ?? process.cwd();
994
+ const flavor = options.flavor ?? detectDevFlavor(cwd);
995
+ const workerPort = flavor === "wrangler" ? await resolveWorkerPort(options, cwd) : options.workerPort;
996
+ return planDevCommand({ ...options, cwd, flavor, workerPort });
997
+ };
998
+ const superviseWorkers = async (worker, sidecar, logger) => {
999
+ let sigintCount = 0;
1000
+ let escalationTimer;
1001
+ const killChildren = (signal) => {
1002
+ worker.kill(signal);
1003
+ sidecar?.kill(signal);
1004
+ };
1005
+ const onSigint = () => {
1006
+ sigintCount += 1;
1007
+ if (sigintCount === 1) {
1008
+ logger.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)");
1009
+ killChildren("SIGTERM");
1010
+ escalationTimer = setTimeout(() => {
1011
+ killChildren("SIGKILL");
1012
+ }, SIGINT_GRACE_MS);
1013
+ escalationTimer.unref();
1014
+ } else {
1015
+ killChildren("SIGKILL");
1016
+ }
1017
+ };
1018
+ const onSigterm = () => {
1019
+ killChildren("SIGTERM");
1020
+ };
1021
+ process.on("SIGINT", onSigint);
1022
+ process.on("SIGTERM", onSigterm);
1023
+ const exits = [
1024
+ worker.exited.then((code) => {
1025
+ return { code, who: "worker" };
1026
+ })
1027
+ ];
1028
+ if (sidecar !== void 0) {
1029
+ exits.push(
1030
+ sidecar.exited.then((code) => {
1031
+ return { code, who: "sidecar" };
1032
+ })
1033
+ );
1034
+ }
1035
+ const first = await Promise.race(exits);
1036
+ if (sidecar !== void 0) {
1037
+ if (first.who === "sidecar") {
1038
+ logger.warn("[worker] the Lunora sidecar (wrangler dev) exited — shutting down the framework dev server");
1039
+ worker.kill("SIGTERM");
1040
+ } else {
1041
+ sidecar.kill("SIGTERM");
1042
+ }
1043
+ await Promise.allSettled([worker.exited, sidecar.exited]);
1044
+ }
1045
+ if (escalationTimer) {
1046
+ clearTimeout(escalationTimer);
1047
+ }
1048
+ process.off("SIGINT", onSigint);
1049
+ process.off("SIGTERM", onSigterm);
1050
+ return first.code;
1051
+ };
1052
+ const startStudioBestEffort = async (options, plan, cwd, logger) => {
1053
+ if (!plan.studioEnabled) {
1054
+ return void 0;
1055
+ }
1056
+ try {
1057
+ return await (options.startStudio ?? startStudioServer)({
1058
+ cwd,
1059
+ logger: {
1060
+ warnOnce: (message) => {
1061
+ logger.warn(message);
1062
+ }
1063
+ },
1064
+ port: plan.studioPort,
1065
+ workerOrigin: plan.workerOrigin
1066
+ });
1067
+ } catch (error) {
1068
+ logger.warn(`studio server failed to start (${error instanceof Error ? error.message : String(error)}) — continuing without it`);
1069
+ return void 0;
1070
+ }
1071
+ };
1072
+ const ensureSidecarGenerated = (plan, options, cwd, logger) => {
1073
+ if (plan.sidecar === void 0) {
1074
+ return;
1075
+ }
1076
+ try {
1077
+ runCodegen({ apiSpec: options.apiSpec, lunoraDirectory: "lunora", projectRoot: cwd });
1078
+ } catch (error) {
1079
+ logger.warn(`codegen (pre-sidecar) failed: ${error instanceof Error ? error.message : String(error)} — the framework dev server will retry`);
1080
+ }
412
1081
  };
413
1082
  const runDevCommand = async (options) => {
414
- const plan = planDevCommand(options);
1083
+ const plan = await buildDevPlan(options);
415
1084
  const { logger } = options;
416
1085
  const cwd = plan.wrangler.cwd ?? process.cwd();
417
1086
  const handles = { remoteCleanup: plan.remote.cleanup };
418
1087
  try {
1088
+ const handoffPid = Number(process.env[DEV_HANDOFF_ENV]);
1089
+ const existing = readLiveDevServerState(cwd);
1090
+ if (existing !== void 0 && existing.pid !== process.pid && existing.pid !== handoffPid) {
1091
+ reportExistingServer(logger, existing);
1092
+ return { code: 0, plan };
1093
+ }
1094
+ const incumbent = claimStartRecord(plan, cwd);
1095
+ if (incumbent !== void 0) {
1096
+ reportExistingServer(logger, incumbent);
1097
+ return { code: 0, plan };
1098
+ }
1099
+ if (plan.flavor === "vite" || plan.flavor === "framework-worker") {
1100
+ plan.wrangler.env = { ...plan.wrangler.env, [DEV_HANDOFF_ENV]: String(process.pid) };
1101
+ }
419
1102
  await offerDevVariablesScaffold(options, cwd);
420
- logger.info("starting wrangler dev + studio");
1103
+ logger.info(
1104
+ plan.flavor === "vite" ? "starting vite dev (worker + studio + codegen run inside Vite via @lunora/vite)" : "starting wrangler dev + studio"
1105
+ );
1106
+ if (plan.ipv4LoopbackForced) {
1107
+ logger.info(
1108
+ "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."
1109
+ );
1110
+ }
421
1111
  if (plan.codegenEnabled) {
422
1112
  handles.codegen = (options.startCodegen ?? startCodegenWatch)({ apiSpec: options.apiSpec, logger, projectRoot: cwd });
423
1113
  }
424
- let studioUrl;
425
- if (plan.studioEnabled) {
426
- try {
427
- handles.studio = await (options.startStudio ?? startStudioServer)({
428
- cwd,
429
- logger: {
430
- warnOnce: (message) => {
431
- logger.warn(message);
432
- }
433
- },
434
- port: plan.studioPort,
435
- workerOrigin: plan.workerOrigin
436
- });
437
- studioUrl = handles.studio.url;
438
- } catch (error) {
439
- logger.warn(`studio server failed to start (${error instanceof Error ? error.message : String(error)}) — continuing without it`);
440
- }
1114
+ handles.studio = await startStudioBestEffort(options, plan, cwd, logger);
1115
+ const studioUrl = handles.studio?.url;
1116
+ if (plan.frameworkHint !== void 0) {
1117
+ logger.warn(plan.frameworkHint);
441
1118
  }
442
- const worker = (options.startWorker ?? defaultWorkerSpawner)(plan.wrangler, logger);
443
- printBanner(logger, plan, studioUrl);
1119
+ ensureSidecarGenerated(plan, options, cwd, logger);
1120
+ const spawn = options.startWorker ?? defaultWorkerSpawner;
1121
+ const worker = spawn(plan.wrangler, logger);
1122
+ const sidecar = plan.sidecar === void 0 ? void 0 : spawn(plan.sidecar, logger);
1123
+ handles.containerLogs = afterWorkerSpawn(plan, cwd, logger, studioUrl);
444
1124
  printAgentRulesHint(logger, cwd);
445
- let sigintCount = 0;
446
- let escalationTimer;
447
- const onSigint = () => {
448
- sigintCount += 1;
449
- if (sigintCount === 1) {
450
- logger.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)");
451
- worker.kill("SIGTERM");
452
- escalationTimer = setTimeout(() => {
453
- worker.kill("SIGKILL");
454
- }, SIGINT_GRACE_MS);
455
- escalationTimer.unref();
456
- } else {
457
- worker.kill("SIGKILL");
458
- }
459
- };
460
- const onSigterm = () => {
461
- worker.kill("SIGTERM");
462
- };
463
- process.on("SIGINT", onSigint);
464
- process.on("SIGTERM", onSigterm);
465
- const code = await worker.exited;
466
- if (escalationTimer) {
467
- clearTimeout(escalationTimer);
468
- }
469
- process.off("SIGINT", onSigint);
470
- process.off("SIGTERM", onSigterm);
1125
+ const code = await superviseWorkers(worker, sidecar, logger);
471
1126
  return { code, plan };
472
1127
  } finally {
1128
+ clearDevServerState(cwd, process.pid);
473
1129
  await teardown(handles);
474
1130
  }
475
1131
  };
476
- const execute = defineHandler(
477
- ({ cwd, logger, options }) => runDevCommand({
1132
+ const execute = defineHandler(async ({ argument, cwd, logger, options }) => {
1133
+ const json = options.json === true;
1134
+ const dispatched = runLifecycleSubcommand({ cwd, json, lines: options.lines, logger, subcommand: argument[0] });
1135
+ if (dispatched !== void 0) {
1136
+ return dispatched;
1137
+ }
1138
+ const isDaemon = process.env[DEV_DAEMON_ENV] === "1";
1139
+ const agent = isDaemon ? void 0 : detectAiAgent();
1140
+ const jsonLogs = json || agent !== void 0;
1141
+ if (jsonLogs) {
1142
+ forceJsonLogging();
1143
+ }
1144
+ if (agent !== void 0 && options.background !== true) {
1145
+ logger.info(
1146
+ `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.`
1147
+ );
1148
+ }
1149
+ const remote = resolveRemoteEnabled({
1150
+ configPreference: readProjectRemotePreference(cwd),
1151
+ envValue: process.env["LUNORA_REMOTE"],
1152
+ flag: options.remote
1153
+ });
1154
+ if (!isDaemon && (options.background === true || agent !== void 0)) {
1155
+ const existing = readLiveDevServerState(cwd);
1156
+ if (existing !== void 0) {
1157
+ reportExistingServer(logger, existing);
1158
+ return { code: 0 };
1159
+ }
1160
+ return startBackground({ cwd, jsonLogs, logger, options, remote });
1161
+ }
1162
+ return runDevCommand({
478
1163
  apiSpec: parseApiSpec(options.apiSpec),
479
1164
  // cerebro parses `--no-codegen`/`--no-studio` as the negation of the
480
1165
  // `codegen`/`studio` booleans (runtime key drops the `no-` prefix), so a
@@ -483,18 +1168,10 @@ const execute = defineHandler(
483
1168
  cwd,
484
1169
  logger,
485
1170
  port: options.port,
486
- // Remote-binding mode obeys a clear precedence: an explicit `--remote`
487
- // flag wins, then `LUNORA_REMOTE` in the environment, then the `remote`
488
- // key in the project's `lunora.json` (a project default). See
489
- // `resolveRemoteEnabled` in @lunora/config.
490
- remote: resolveRemoteEnabled({
491
- configPreference: readProjectRemotePreference(cwd),
492
- envValue: process.env["LUNORA_REMOTE"],
493
- flag: options.remote
494
- }),
1171
+ remote,
495
1172
  studio: options.studio === false ? false : void 0,
496
1173
  workerPort: options.workerPort
497
- })
498
- );
1174
+ });
1175
+ });
499
1176
 
500
- export { execute, planDevCommand, resolveRemotePlan, runDevCommand };
1177
+ export { detectDevFlavor, execute, planDevCommand, resolveRemotePlan, resolveWorkerPort, runDevCommand };