@lunora/cli 1.0.0-alpha.7 → 1.0.0-alpha.70

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 +248 -115
  5. package/dist/index.d.ts +248 -115
  6. package/dist/index.mjs +8 -8
  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 +19 -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 +74 -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 +6 -3
  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 +315 -12
  28. package/dist/packem_chunks/planDevCommand.mjs +653 -37
  29. package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
  30. package/dist/packem_chunks/runDeployCommand.mjs +115 -20
  31. package/dist/packem_chunks/runInitCommand.mjs +1602 -172
  32. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +5 -4
  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-0BD3K1pb.mjs} +79 -21
  36. package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-CRQmmBZM.mjs} +3 -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-CqM5FwGI.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-vnRRkhNE.mjs} +123 -15
  42. package/dist/packem_shared/{createLogger-CHPNjFw2.mjs → createLogger-CIWSHrTL.mjs} +40 -8
  43. package/dist/packem_shared/{createRecordingSpawner-DxI3mebw.mjs → createRecordingSpawner-Cw5Iu73G.mjs} +12 -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-BBrXCg9V.mjs} +16 -9
  49. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
  50. package/dist/packem_shared/runAddCommand-BCcJzs_k.mjs +4 -0
  51. package/dist/packem_shared/storage-bGfgwOgF.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 +10 -7
  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/runAddCommand-G544_v6e.mjs +0 -4
  59. 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';
2
+ import { detectAgentRules, claimDevServerState, clearDevServerState, readDevServerState, readLiveDevServerState, 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, detectFramework, discoverContainerInfo, streamContainerLogs, materializeRemoteWranglerConfig, formatLunoraEvent } from '@lunora/config';
3
+ import { p as parseApiSpec } from '../packem_shared/api-spec-Bx0iKbxA.mjs';
4
+ import { existsSync, watch, readFileSync, statSync, openSync, readSync, closeSync, mkdirSync } from 'node:fs';
5
+ import { join, dirname } from 'node:path';
6
6
  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';
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-Cw5Iu73G.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,409 @@ 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 detectDevFlavor = (cwd) => readProjectDependencyNames(cwd).has("@lunora/vite") ? "vite" : "wrangler";
311
+ const viteDevCommand = (cwd) => {
312
+ const manager = detectPackageManager(cwd);
313
+ let script;
314
+ try {
315
+ const raw = readFileSync(join(cwd, "package.json"), "utf8");
316
+ script = JSON.parse(raw).scripts?.dev;
317
+ } catch {
318
+ }
319
+ if (script === void 0 || script.trim() === "" || script.includes("lunora")) {
320
+ const exec = execArgsFor(manager, "vite", ["dev"]);
321
+ return { args: exec.args, command: exec.command };
322
+ }
323
+ return { args: ["run", "dev"], command: manager };
324
+ };
325
+ const sleep = (ms) => new Promise((resolve) => {
326
+ setTimeout(resolve, ms);
327
+ });
328
+ const defaultProbe = async (origin) => {
329
+ try {
330
+ await fetch(new URL("/_lunora/status", origin), { signal: AbortSignal.timeout(1e3) });
331
+ return true;
332
+ } catch {
333
+ return false;
334
+ }
335
+ };
336
+ const defaultDetachedSpawner = (descriptor) => {
337
+ mkdirSync(dirname(descriptor.logPath), { recursive: true });
338
+ const logFd = openSync(descriptor.logPath, "w", 384);
339
+ let child;
340
+ const exec = spawnShellCompat(descriptor.command, descriptor.args);
341
+ try {
342
+ child = spawn(exec.command, exec.args, {
343
+ cwd: descriptor.cwd,
344
+ detached: true,
345
+ env: descriptor.env,
346
+ shell: exec.shell,
347
+ stdio: ["ignore", logFd, logFd],
348
+ windowsHide: true
349
+ });
350
+ } finally {
351
+ closeSync(logFd);
352
+ }
353
+ child.unref();
354
+ return {
355
+ exited: new Promise((resolve) => {
356
+ child.on("error", () => {
357
+ resolve(1);
358
+ });
359
+ child.on("exit", (code, signal) => {
360
+ resolve(code ?? (signal ? 1 : 0));
361
+ });
362
+ }),
363
+ pid: child.pid
364
+ };
365
+ };
366
+ const readLogWindow = (path) => {
367
+ try {
368
+ const { size } = statSync(path);
369
+ if (size <= LOG_TAIL_MAX_BYTES) {
370
+ return readFileSync(path, "utf8");
371
+ }
372
+ const fd = openSync(path, "r");
373
+ try {
374
+ const buffer = Buffer.alloc(LOG_TAIL_MAX_BYTES);
375
+ const read = readSync(fd, buffer, 0, LOG_TAIL_MAX_BYTES, size - LOG_TAIL_MAX_BYTES);
376
+ const text = buffer.toString("utf8", 0, read);
377
+ return text.slice(text.indexOf("\n") + 1);
378
+ } finally {
379
+ closeSync(fd);
380
+ }
381
+ } catch {
382
+ return void 0;
383
+ }
384
+ };
385
+ const readLogTail = (path, count) => {
386
+ const text = readLogWindow(path);
387
+ if (text === void 0) {
388
+ return [];
389
+ }
390
+ const lines = text.split("\n");
391
+ if (lines.at(-1) === "") {
392
+ lines.pop();
393
+ }
394
+ return count > 0 ? lines.slice(-count) : lines;
395
+ };
396
+ const resolveLogPath = (cwd, state) => state?.logFile ?? join(cwd, DEV_LOG_FILE);
397
+ const printLifecycleHints = (logger) => {
398
+ logger.info(" Stop: lunora dev stop");
399
+ logger.info(" Status: lunora dev status");
400
+ logger.info(" Logs: lunora dev logs");
401
+ };
402
+ const reportExistingServer = (logger, existing) => {
403
+ logger.warn(`Dev server already running at ${existing.url} (pid ${String(existing.pid)})`);
404
+ printLifecycleHints(logger);
405
+ };
406
+ const printBackgroundBanner = (logger, state) => {
407
+ logger.success(`Dev server running at ${state.url} (pid ${String(state.pid)})`);
408
+ if (state.studioUrl !== void 0) {
409
+ logger.info(` Studio: ${state.studioUrl}`);
410
+ }
411
+ printLifecycleHints(logger);
412
+ };
413
+ const waitUntilReady = async (parameters) => {
414
+ const { child, cwd, deadline, pollIntervalMs, probe } = parameters;
415
+ const exited = child.exited.then((code) => {
416
+ return { exitCode: code };
417
+ });
418
+ while (Date.now() < deadline) {
419
+ const state = readLiveDevServerState(cwd);
420
+ if (state !== void 0 && state.pid !== process.pid && await probe(state.url)) {
421
+ return { state, status: "ready" };
422
+ }
423
+ const raced = await Promise.race([exited, sleep(pollIntervalMs)]);
424
+ if (raced !== void 0) {
425
+ return { ...raced, status: "exited" };
426
+ }
427
+ }
428
+ return { status: "timeout" };
429
+ };
430
+ const runDevBackground = async (options) => {
431
+ const { cwd, logger } = options;
432
+ const logPath = resolveLogPath(cwd, void 0);
433
+ const environmentTimeout = Number(process.env[READY_TIMEOUT_ENV] ?? "");
434
+ const timeout = options.readyTimeoutMs ?? (Number.isFinite(environmentTimeout) && environmentTimeout > 0 ? environmentTimeout : DEFAULT_READY_TIMEOUT_MS);
435
+ const child = (options.spawnDetached ?? defaultDetachedSpawner)({
436
+ args: options.command.args,
437
+ command: options.command.command,
438
+ cwd,
439
+ env: {
440
+ ...process.env,
441
+ ...options.env,
442
+ [DEV_DAEMON_ENV]: "1",
443
+ [DEV_LOG_FILE_ENV]: logPath,
444
+ // The capture log is read back by `lunora dev logs` (and often an
445
+ // agent) — ANSI colour codes would only garble it.
446
+ ...process.env.FORCE_COLOR === void 0 ? { NO_COLOR: "1" } : {},
447
+ ...options.json ? { LUNORA_LOG_JSON: "1" } : {}
448
+ },
449
+ logPath
450
+ });
451
+ const outcome = await waitUntilReady({
452
+ child,
453
+ cwd,
454
+ deadline: Date.now() + timeout,
455
+ pollIntervalMs: options.pollIntervalMs ?? POLL_INTERVAL_MS,
456
+ probe: options.probe ?? defaultProbe
457
+ });
458
+ if (outcome.status === "ready") {
459
+ printBackgroundBanner(logger, outcome.state);
460
+ return { code: 0 };
461
+ }
462
+ if (outcome.status === "exited") {
463
+ logger.error(`dev server exited before becoming ready (exit code ${String(outcome.exitCode)}). Last output:`);
464
+ for (const line of readLogTail(logPath, FAILURE_LOG_TAIL_LINES)) {
465
+ logger.error(` ${line}`);
466
+ }
467
+ clearDevServerState(cwd, child.pid ?? process.pid);
468
+ return { code: outcome.exitCode === 0 ? 1 : outcome.exitCode };
469
+ }
470
+ if (child.pid !== void 0) {
471
+ const current = readDevServerState(cwd);
472
+ if (current?.pid === process.pid) {
473
+ updateDevServerState(cwd, { logFile: logPath, pid: child.pid });
474
+ }
475
+ }
476
+ const pidHint = child.pid === void 0 ? "" : ` (pid ${String(child.pid)})`;
477
+ logger.warn(
478
+ `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.`
479
+ );
480
+ return { code: 1 };
481
+ };
482
+ const daemonArguments = (options, remote) => {
483
+ const args = ["dev"];
484
+ if (options.apiSpec !== void 0) {
485
+ args.push("--api-spec", options.apiSpec);
486
+ }
487
+ if (options.port !== void 0) {
488
+ args.push("--port", String(options.port));
489
+ }
490
+ if (options.workerPort !== void 0) {
491
+ args.push("--worker-port", String(options.workerPort));
492
+ }
493
+ if (options.codegen === false) {
494
+ args.push("--no-codegen");
495
+ }
496
+ if (options.studio === false) {
497
+ args.push("--no-studio");
498
+ }
499
+ if (remote) {
500
+ args.push("--remote");
501
+ }
502
+ return args;
503
+ };
504
+ const startBackground = async (context) => {
505
+ const { cwd, jsonLogs, logger, options, remote } = context;
506
+ const run = context.run ?? runDevBackground;
507
+ const flavor = detectDevFlavor(cwd);
508
+ const url = flavor === "vite" ? "http://localhost:5173" : `http://localhost:${String(options.workerPort ?? 8787)}`;
509
+ const claim = claimDevServerState(cwd, {
510
+ background: true,
511
+ mode: "cli",
512
+ pid: process.pid,
513
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
514
+ url
515
+ });
516
+ if (!claim.ok) {
517
+ if (claim.existing !== void 0) {
518
+ reportExistingServer(logger, claim.existing);
519
+ }
520
+ return { code: 0 };
521
+ }
522
+ const handoff = { [DEV_HANDOFF_ENV]: String(process.pid) };
523
+ try {
524
+ if (flavor === "vite") {
525
+ return await run({
526
+ command: viteDevCommand(cwd),
527
+ cwd,
528
+ env: { ...remote ? { LUNORA_REMOTE: "1" } : {}, ...handoff },
529
+ json: jsonLogs,
530
+ logger
531
+ });
532
+ }
533
+ return await run({
534
+ command: { args: [process.argv[1] ?? "lunora", ...daemonArguments(options, remote)], command: process.execPath },
535
+ cwd,
536
+ env: handoff,
537
+ json: jsonLogs,
538
+ logger
539
+ });
540
+ } finally {
541
+ clearDevServerState(cwd, process.pid);
542
+ }
543
+ };
544
+ const processGroupId = (pid) => {
545
+ try {
546
+ const result = spawnSync("ps", ["-o", "pgid=", "-p", String(pid)], { encoding: "utf8" });
547
+ const pgid = Number.parseInt(result.stdout.trim(), 10);
548
+ return Number.isInteger(pgid) && pgid > 0 ? pgid : void 0;
549
+ } catch {
550
+ return void 0;
551
+ }
552
+ };
553
+ const forceKillRecordedServer = (state, signal, platform = process.platform, spawnSyncImpl = spawnSync) => {
554
+ if (platform === "win32") {
555
+ try {
556
+ spawnSyncImpl("taskkill", ["/pid", String(state.pid), "/T", "/F"], { stdio: "ignore" });
557
+ } catch {
558
+ }
559
+ return;
560
+ }
561
+ if (state.background === true) {
562
+ try {
563
+ signal(-(processGroupId(state.pid) ?? state.pid), "SIGKILL");
564
+ return;
565
+ } catch {
566
+ }
567
+ }
568
+ try {
569
+ signal(state.pid, "SIGKILL");
570
+ } catch {
571
+ }
572
+ };
573
+ const reportSurvivedForceKill = (state, options) => {
574
+ if (options.json) {
575
+ printJson({ pid: state.pid, stopped: false });
576
+ } else {
577
+ options.logger.error(
578
+ `dev server (pid ${String(state.pid)}) survived the force-kill — record kept; retry \`lunora dev stop\` or kill the process manually.`
579
+ );
580
+ }
581
+ return { code: 1 };
582
+ };
583
+ const runDevStop = async (options) => {
584
+ const { cwd, logger } = options;
585
+ const state = readDevServerState(cwd);
586
+ const alive = options.alive ?? ((pid) => pid === state?.pid && isRecordedProcessCurrent(state));
587
+ const signal = options.signal ?? ((pid, sig) => {
588
+ process.kill(pid, sig);
589
+ });
590
+ const pollInterval = options.pollIntervalMs ?? POLL_INTERVAL_MS;
591
+ const grace = options.stopGraceMs ?? STOP_GRACE_MS;
592
+ const platform = options.platform ?? process.platform;
593
+ const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
594
+ if (state === void 0 || !alive(state.pid)) {
595
+ if (state !== void 0) {
596
+ clearDevServerState(cwd, state.pid);
597
+ }
598
+ if (options.json) {
599
+ printJson({ running: false, stopped: false });
600
+ } else {
601
+ logger.info("No dev server running.");
602
+ }
603
+ return { code: 0 };
604
+ }
605
+ try {
606
+ signal(state.pid, "SIGTERM");
607
+ } catch {
608
+ }
609
+ const deadline = Date.now() + grace;
610
+ while (alive(state.pid) && Date.now() < deadline) {
611
+ await sleep(pollInterval);
612
+ }
613
+ if (alive(state.pid)) {
614
+ forceKillRecordedServer(state, signal, platform, spawnSyncImpl);
615
+ await sleep(pollInterval);
616
+ if (alive(state.pid)) {
617
+ return reportSurvivedForceKill(state, options);
618
+ }
619
+ }
620
+ clearDevServerState(cwd, state.pid);
621
+ if (options.json) {
622
+ printJson({ pid: state.pid, stopped: true });
623
+ } else {
624
+ logger.success(`Stopped dev server (pid ${String(state.pid)}).`);
625
+ }
626
+ return { code: 0 };
627
+ };
628
+ const runDevStatus = (options) => {
629
+ const { cwd, logger } = options;
630
+ const state = readLiveDevServerState(cwd);
631
+ if (state === void 0) {
632
+ if (options.json) {
633
+ printJson({ running: false });
634
+ } else {
635
+ logger.info("No dev server running.");
636
+ }
637
+ return { code: 0 };
638
+ }
639
+ const now = options.now ?? Date.now;
640
+ const startedAtMs = state.startedAt === void 0 ? Number.NaN : Date.parse(state.startedAt);
641
+ const uptimeSeconds = Number.isFinite(startedAtMs) ? Math.max(0, Math.round((now() - startedAtMs) / 1e3)) : void 0;
642
+ if (options.json) {
643
+ printJson({
644
+ background: state.background === true,
645
+ logFile: state.logFile,
646
+ mode: state.mode,
647
+ pid: state.pid,
648
+ running: true,
649
+ startedAt: state.startedAt,
650
+ studioUrl: state.studioUrl,
651
+ uptimeSeconds,
652
+ url: state.url
653
+ });
654
+ return { code: 0 };
655
+ }
656
+ const details = [
657
+ `pid ${String(state.pid)}`,
658
+ ...uptimeSeconds === void 0 ? [] : [`uptime ${String(uptimeSeconds)}s`],
659
+ state.background === true ? "background" : "foreground"
660
+ ];
661
+ logger.success(`Dev server running at ${state.url} (${details.join(", ")})`);
662
+ if (state.studioUrl !== void 0) {
663
+ logger.info(` Studio: ${state.studioUrl}`);
664
+ }
665
+ if (state.logFile !== void 0) {
666
+ logger.info(` Logs: lunora dev logs (${state.logFile})`);
667
+ }
668
+ return { code: 0 };
669
+ };
670
+ const runDevLogs = (options) => {
671
+ const { cwd, logger } = options;
672
+ const state = readDevServerState(cwd);
673
+ const logPath = resolveLogPath(cwd, state);
674
+ if (!existsSync(logPath)) {
675
+ logger.info("No dev server logs found — output is captured when the server runs in background mode (`lunora dev --background`).");
676
+ return { code: 0 };
677
+ }
678
+ const tail = readLogTail(logPath, options.lines ?? DEFAULT_LOG_LINES);
679
+ if (tail.length > 0) {
680
+ process.stdout.write(`${tail.join("\n")}
681
+ `);
682
+ }
683
+ return { code: 0 };
684
+ };
685
+ const runLifecycleSubcommand = (parameters) => {
686
+ const { cwd, json, lines, logger, subcommand } = parameters;
687
+ if (subcommand === "stop") {
688
+ return runDevStop({ cwd, json, logger });
689
+ }
690
+ if (subcommand === "status") {
691
+ return runDevStatus({ cwd, json, logger });
692
+ }
693
+ if (subcommand === "logs") {
694
+ return runDevLogs({ cwd, lines, logger });
695
+ }
696
+ if (subcommand !== void 0) {
697
+ logger.error(`dev: unknown subcommand "${subcommand}" — expected stop | status | logs (or no subcommand to start the dev server)`);
698
+ return { code: 1 };
699
+ }
700
+ return void 0;
701
+ };
702
+
259
703
  const DEFAULT_STUDIO_PORT = 6173;
260
704
  const DEFAULT_WORKER_PORT = 8787;
705
+ const DEFAULT_VITE_PORT = 5173;
261
706
  const SIGINT_GRACE_MS = 5e3;
262
707
  const resolveRemotePlan = (options, cwd) => {
263
708
  const noopCleanup = () => {
@@ -274,14 +719,65 @@ const resolveRemotePlan = (options, cwd) => {
274
719
  }
275
720
  return { args: ["--config", result.configPath], plan: { bindings, cleanup, enabled: true } };
276
721
  };
722
+ const resolveLoopbackArgs = (cwd, hasLoopback) => {
723
+ const wranglerPath = findWranglerFile(cwd);
724
+ if (wranglerPath !== void 0) {
725
+ const { parsed } = readWranglerJsonc(wranglerPath);
726
+ if (parsed?.dev?.ip !== void 0) {
727
+ return [];
728
+ }
729
+ }
730
+ return hasLoopback() ? [] : ["--ip", "127.0.0.1"];
731
+ };
732
+ const resolveWorkerPort = async (options, cwd) => {
733
+ if (options.workerPort !== void 0) {
734
+ return options.workerPort;
735
+ }
736
+ const wranglerPath = findWranglerFile(cwd);
737
+ if (wranglerPath !== void 0) {
738
+ const { parsed } = readWranglerJsonc(wranglerPath);
739
+ if (typeof parsed?.dev?.port === "number") {
740
+ return parsed.dev.port;
741
+ }
742
+ }
743
+ return (options.findFreePort ?? findAvailablePort)(DEFAULT_WORKER_PORT);
744
+ };
277
745
  const planDevCommand = (options) => {
278
746
  const cwd = options.cwd ?? process.cwd();
279
- const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
280
747
  const manager = detectPackageManager(cwd);
748
+ const flavor = options.flavor ?? detectDevFlavor(cwd);
749
+ if (flavor === "vite") {
750
+ const exec2 = viteDevCommand(cwd);
751
+ return {
752
+ codegenEnabled: false,
753
+ flavor,
754
+ ipv4LoopbackForced: false,
755
+ remote: { bindings: [], cleanup: () => {
756
+ }, enabled: options.remote === true },
757
+ studioEnabled: false,
758
+ studioPort: options.port ?? DEFAULT_STUDIO_PORT,
759
+ workerOrigin: `http://localhost:${String(DEFAULT_VITE_PORT)}`,
760
+ workerPort: DEFAULT_VITE_PORT,
761
+ wrangler: {
762
+ args: exec2.args,
763
+ command: exec2.command,
764
+ cwd,
765
+ ...options.remote === true ? { env: { LUNORA_REMOTE: "1" } } : {},
766
+ tag: "vite"
767
+ }
768
+ };
769
+ }
770
+ const detection = detectFramework(cwd);
771
+ 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.`;
772
+ const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
281
773
  const remote = resolveRemotePlan(options, cwd);
282
- const exec = execArgsFor(manager, "wrangler", ["dev", "--port", String(workerPort), "--var", "WORKER_ENV:development", ...remote.args]);
774
+ const loopbackArgs = resolveLoopbackArgs(cwd, options.hasIpv6Loopback ?? hasIpv6Loopback);
775
+ const exec = execArgsFor(manager, "wrangler", ["dev", "--port", String(workerPort), ...loopbackArgs, "--var", "WORKER_ENV:development", ...remote.args]);
283
776
  return {
284
777
  codegenEnabled: options.codegen !== false,
778
+ flavor,
779
+ frameworkHint,
780
+ ipv4LoopbackForced: loopbackArgs.length > 0,
285
781
  remote: remote.plan,
286
782
  studioEnabled: options.studio !== false,
287
783
  studioPort: options.port ?? DEFAULT_STUDIO_PORT,
@@ -329,9 +825,11 @@ const pipeChildOutput = (child, tag, logger) => {
329
825
  pumpStream(child.stderr, "stderr");
330
826
  };
331
827
  const defaultWorkerSpawner = (descriptor, logger) => {
332
- const child = spawn(descriptor.command, [...descriptor.args], {
828
+ const exec = spawnShellCompat(descriptor.command, descriptor.args);
829
+ const child = spawn(exec.command, exec.args, {
333
830
  cwd: descriptor.cwd ?? process.cwd(),
334
- env: process.env,
831
+ env: descriptor.env ? { ...process.env, ...descriptor.env } : process.env,
832
+ shell: exec.shell,
335
833
  stdio: ["inherit", "pipe", "pipe"]
336
834
  });
337
835
  pipeChildOutput(child, descriptor.tag, logger);
@@ -379,8 +877,35 @@ const printAgentRulesHint = (logger, cwd) => {
379
877
  logger.info(` ⓘ ${AGENT_RULES_HINT}`);
380
878
  logger.info("");
381
879
  };
880
+ const startContainerLogStreaming = (cwd, logger) => {
881
+ if (process.env.LUNORA_CONTAINER_LOGS === "0") {
882
+ return void 0;
883
+ }
884
+ const discovery = discoverContainerInfo(cwd, "lunora");
885
+ const containers = discovery.containers.map((container) => {
886
+ return { className: container.className, exportName: container.exportName };
887
+ });
888
+ if (containers.length === 0) {
889
+ return void 0;
890
+ }
891
+ return streamContainerLogs({
892
+ containers,
893
+ onLine: (line) => {
894
+ const tagged = `[container:${line.name}] ${line.text}`;
895
+ if (line.level === "error") {
896
+ logger.warn(tagged);
897
+ } else {
898
+ logger.info(tagged);
899
+ }
900
+ },
901
+ onUnavailable: (message) => {
902
+ logger.warn(`[container] Docker engine unreachable — container logs unavailable (${message})`);
903
+ }
904
+ });
905
+ };
382
906
  const teardown = async (handles) => {
383
907
  handles.codegen?.close();
908
+ handles.containerLogs?.close();
384
909
  await handles.studio?.close().catch(() => void 0);
385
910
  try {
386
911
  handles.remoteCleanup?.();
@@ -409,15 +934,81 @@ const offerDevVariablesScaffold = async (options, cwd) => {
409
934
  `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
935
  );
411
936
  }
937
+ try {
938
+ (options.fillSecrets ?? fillDevSecrets)({
939
+ cwd,
940
+ info: (message) => {
941
+ options.logger.info(message);
942
+ }
943
+ });
944
+ } catch {
945
+ }
946
+ };
947
+ const afterWorkerSpawn = (plan, cwd, logger, studioUrl) => {
948
+ if (plan.flavor !== "wrangler") {
949
+ return void 0;
950
+ }
951
+ if (studioUrl !== void 0) {
952
+ updateDevServerState(cwd, { studioUrl });
953
+ }
954
+ let containerLogs;
955
+ try {
956
+ containerLogs = startContainerLogStreaming(cwd, logger);
957
+ } catch {
958
+ }
959
+ printBanner(logger, plan, studioUrl);
960
+ return containerLogs;
961
+ };
962
+ const claimStartRecord = (plan, cwd) => {
963
+ const handoffPid = Number(process.env[DEV_HANDOFF_ENV]);
964
+ const claim = claimDevServerState(
965
+ cwd,
966
+ {
967
+ background: process.env[DEV_DAEMON_ENV] === "1",
968
+ logFile: process.env[DEV_LOG_FILE_ENV],
969
+ mode: "cli",
970
+ pid: process.pid,
971
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
972
+ url: plan.workerOrigin
973
+ },
974
+ Number.isInteger(handoffPid) && handoffPid > 0 ? { supersedePid: handoffPid } : void 0
975
+ );
976
+ return claim.ok ? void 0 : claim.existing;
977
+ };
978
+ const buildDevPlan = async (options) => {
979
+ const cwd = options.cwd ?? process.cwd();
980
+ const flavor = options.flavor ?? detectDevFlavor(cwd);
981
+ const workerPort = flavor === "wrangler" ? await resolveWorkerPort(options, cwd) : options.workerPort;
982
+ return planDevCommand({ ...options, cwd, flavor, workerPort });
412
983
  };
413
984
  const runDevCommand = async (options) => {
414
- const plan = planDevCommand(options);
985
+ const plan = await buildDevPlan(options);
415
986
  const { logger } = options;
416
987
  const cwd = plan.wrangler.cwd ?? process.cwd();
417
988
  const handles = { remoteCleanup: plan.remote.cleanup };
418
989
  try {
990
+ const existing = readLiveDevServerState(cwd);
991
+ if (existing !== void 0 && existing.pid !== process.pid) {
992
+ reportExistingServer(logger, existing);
993
+ return { code: 0, plan };
994
+ }
995
+ const incumbent = claimStartRecord(plan, cwd);
996
+ if (incumbent !== void 0) {
997
+ reportExistingServer(logger, incumbent);
998
+ return { code: 0, plan };
999
+ }
1000
+ if (plan.flavor === "vite") {
1001
+ plan.wrangler.env = { ...plan.wrangler.env, [DEV_HANDOFF_ENV]: String(process.pid) };
1002
+ }
419
1003
  await offerDevVariablesScaffold(options, cwd);
420
- logger.info("starting wrangler dev + studio");
1004
+ logger.info(
1005
+ plan.flavor === "vite" ? "starting vite dev (worker + studio + codegen run inside Vite via @lunora/vite)" : "starting wrangler dev + studio"
1006
+ );
1007
+ if (plan.ipv4LoopbackForced) {
1008
+ logger.info(
1009
+ "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."
1010
+ );
1011
+ }
421
1012
  if (plan.codegenEnabled) {
422
1013
  handles.codegen = (options.startCodegen ?? startCodegenWatch)({ apiSpec: options.apiSpec, logger, projectRoot: cwd });
423
1014
  }
@@ -439,8 +1030,11 @@ const runDevCommand = async (options) => {
439
1030
  logger.warn(`studio server failed to start (${error instanceof Error ? error.message : String(error)}) — continuing without it`);
440
1031
  }
441
1032
  }
1033
+ if (plan.frameworkHint !== void 0) {
1034
+ logger.warn(plan.frameworkHint);
1035
+ }
442
1036
  const worker = (options.startWorker ?? defaultWorkerSpawner)(plan.wrangler, logger);
443
- printBanner(logger, plan, studioUrl);
1037
+ handles.containerLogs = afterWorkerSpawn(plan, cwd, logger, studioUrl);
444
1038
  printAgentRulesHint(logger, cwd);
445
1039
  let sigintCount = 0;
446
1040
  let escalationTimer;
@@ -470,11 +1064,41 @@ const runDevCommand = async (options) => {
470
1064
  process.off("SIGTERM", onSigterm);
471
1065
  return { code, plan };
472
1066
  } finally {
1067
+ clearDevServerState(cwd, process.pid);
473
1068
  await teardown(handles);
474
1069
  }
475
1070
  };
476
- const execute = defineHandler(
477
- ({ cwd, logger, options }) => runDevCommand({
1071
+ const execute = defineHandler(async ({ argument, cwd, logger, options }) => {
1072
+ const json = options.json === true;
1073
+ const dispatched = runLifecycleSubcommand({ cwd, json, lines: options.lines, logger, subcommand: argument[0] });
1074
+ if (dispatched !== void 0) {
1075
+ return dispatched;
1076
+ }
1077
+ const isDaemon = process.env[DEV_DAEMON_ENV] === "1";
1078
+ const agent = isDaemon ? void 0 : detectAiAgent();
1079
+ const jsonLogs = json || agent !== void 0;
1080
+ if (jsonLogs) {
1081
+ forceJsonLogging();
1082
+ }
1083
+ if (agent !== void 0 && options.background !== true) {
1084
+ logger.info(
1085
+ `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.`
1086
+ );
1087
+ }
1088
+ const remote = resolveRemoteEnabled({
1089
+ configPreference: readProjectRemotePreference(cwd),
1090
+ envValue: process.env["LUNORA_REMOTE"],
1091
+ flag: options.remote
1092
+ });
1093
+ if (!isDaemon && (options.background === true || agent !== void 0)) {
1094
+ const existing = readLiveDevServerState(cwd);
1095
+ if (existing !== void 0) {
1096
+ reportExistingServer(logger, existing);
1097
+ return { code: 0 };
1098
+ }
1099
+ return startBackground({ cwd, jsonLogs, logger, options, remote });
1100
+ }
1101
+ return runDevCommand({
478
1102
  apiSpec: parseApiSpec(options.apiSpec),
479
1103
  // cerebro parses `--no-codegen`/`--no-studio` as the negation of the
480
1104
  // `codegen`/`studio` booleans (runtime key drops the `no-` prefix), so a
@@ -483,18 +1107,10 @@ const execute = defineHandler(
483
1107
  cwd,
484
1108
  logger,
485
1109
  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
- }),
1110
+ remote,
495
1111
  studio: options.studio === false ? false : void 0,
496
1112
  workerPort: options.workerPort
497
- })
498
- );
1113
+ });
1114
+ });
499
1115
 
500
- export { execute, planDevCommand, resolveRemotePlan, runDevCommand };
1116
+ export { detectDevFlavor, execute, planDevCommand, resolveRemotePlan, resolveWorkerPort, runDevCommand };