@lunora/cli 1.0.0-alpha.9 → 1.0.0-alpha.91

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 (59) 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 +358 -300
  5. package/dist/index.d.ts +358 -300
  6. package/dist/index.mjs +8 -9
  7. package/dist/packem_chunks/handler.mjs +15 -5
  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 +94 -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 +330 -12
  28. package/dist/packem_chunks/planDevCommand.mjs +762 -81
  29. package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
  30. package/dist/packem_chunks/runDeployCommand.mjs +137 -22
  31. package/dist/packem_chunks/runInitCommand.mjs +1648 -188
  32. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +94 -47
  33. package/dist/packem_chunks/runResetCommand.mjs +2 -2
  34. package/dist/packem_chunks/runRpcCommand.mjs +3 -2
  35. package/dist/packem_shared/{COMMANDS-Dh0bSERi.mjs → COMMANDS-DZ0NnuqI.mjs} +103 -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-CkkATMMx.mjs → commands-D5Yxt9VY.mjs} +199 -20
  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-v4hHpQd0.mjs +62 -0
  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-D1hgfqFQ.mjs +4 -0
  51. package/dist/packem_shared/{storage-Bjo35hPa.mjs → storage-BXU4ax4O.mjs} +1 -1
  52. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +658 -0
  53. package/dist/packem_shared/wrangler-secrets-Coni-mER.mjs +49 -0
  54. package/package.json +17 -14
  55. package/skills/lunora-quickstart/SKILL.md +26 -5
  56. package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
  57. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
  58. package/dist/packem_shared/runAddCommand-DepS9W9W.mjs +0 -4
  59. package/dist/packem_shared/tui-prompts-CA9lngSS.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';
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 { 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-CA9lngSS.mjs';
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';
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,76 @@ const resolveRemotePlan = (options, cwd) => {
274
726
  }
275
727
  return { args: ["--config", result.configPath], plan: { bindings, cleanup, enabled: true } };
276
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
+ };
277
756
  const planDevCommand = (options) => {
278
757
  const cwd = options.cwd ?? process.cwd();
279
- const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
280
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;
281
791
  const remote = resolveRemotePlan(options, cwd);
282
- const exec = execArgsFor(manager, "wrangler", ["dev", "--port", String(workerPort), "--var", "WORKER_ENV:development", ...remote.args]);
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]);
283
794
  return {
284
795
  codegenEnabled: options.codegen !== false,
796
+ flavor,
797
+ frameworkHint,
798
+ ipv4LoopbackForced: loopbackArgs.length > 0,
285
799
  remote: remote.plan,
286
800
  studioEnabled: options.studio !== false,
287
801
  studioPort: options.port ?? DEFAULT_STUDIO_PORT,
@@ -329,9 +843,11 @@ const pipeChildOutput = (child, tag, logger) => {
329
843
  pumpStream(child.stderr, "stderr");
330
844
  };
331
845
  const defaultWorkerSpawner = (descriptor, logger) => {
332
- const child = spawn(descriptor.command, [...descriptor.args], {
846
+ const exec = spawnShellCompat(descriptor.command, descriptor.args);
847
+ const child = spawn(exec.command, exec.args, {
333
848
  cwd: descriptor.cwd ?? process.cwd(),
334
- env: process.env,
849
+ env: descriptor.env ? { ...process.env, ...descriptor.env } : process.env,
850
+ shell: exec.shell,
335
851
  stdio: ["inherit", "pipe", "pipe"]
336
852
  });
337
853
  pipeChildOutput(child, descriptor.tag, logger);
@@ -379,8 +895,35 @@ const printAgentRulesHint = (logger, cwd) => {
379
895
  logger.info(` ⓘ ${AGENT_RULES_HINT}`);
380
896
  logger.info("");
381
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
+ };
382
924
  const teardown = async (handles) => {
383
925
  handles.codegen?.close();
926
+ handles.containerLogs?.close();
384
927
  await handles.studio?.close().catch(() => void 0);
385
928
  try {
386
929
  handles.remoteCleanup?.();
@@ -409,72 +952,218 @@ const offerDevVariablesScaffold = async (options, cwd) => {
409
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.`
410
953
  );
411
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
+ }
412
1085
  };
413
1086
  const runDevCommand = async (options) => {
414
- const plan = planDevCommand(options);
1087
+ const plan = await buildDevPlan(options);
415
1088
  const { logger } = options;
416
1089
  const cwd = plan.wrangler.cwd ?? process.cwd();
417
1090
  const handles = { remoteCleanup: plan.remote.cleanup };
418
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
+ }
419
1106
  await offerDevVariablesScaffold(options, cwd);
420
- logger.info("starting wrangler dev + studio");
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
+ }
421
1115
  if (plan.codegenEnabled) {
422
1116
  handles.codegen = (options.startCodegen ?? startCodegenWatch)({ apiSpec: options.apiSpec, logger, projectRoot: cwd });
423
1117
  }
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
- }
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);
441
1122
  }
442
- const worker = (options.startWorker ?? defaultWorkerSpawner)(plan.wrangler, logger);
443
- printBanner(logger, plan, studioUrl);
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);
444
1128
  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);
1129
+ const code = await superviseWorkers(worker, sidecar, logger);
471
1130
  return { code, plan };
472
1131
  } finally {
1132
+ clearDevServerState(cwd, process.pid);
473
1133
  await teardown(handles);
474
1134
  }
475
1135
  };
476
- const execute = defineHandler(
477
- ({ cwd, logger, options }) => runDevCommand({
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({
478
1167
  apiSpec: parseApiSpec(options.apiSpec),
479
1168
  // cerebro parses `--no-codegen`/`--no-studio` as the negation of the
480
1169
  // `codegen`/`studio` booleans (runtime key drops the `no-` prefix), so a
@@ -483,18 +1172,10 @@ const execute = defineHandler(
483
1172
  cwd,
484
1173
  logger,
485
1174
  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
- }),
1175
+ remote,
495
1176
  studio: options.studio === false ? false : void 0,
496
1177
  workerPort: options.workerPort
497
- })
498
- );
1178
+ });
1179
+ });
499
1180
 
500
- export { execute, planDevCommand, resolveRemotePlan, runDevCommand };
1181
+ export { detectDevFlavor, execute, planDevCommand, resolveRemotePlan, resolveWorkerPort, runDevCommand };