@lunora/cli 1.0.0-alpha.6 → 1.0.0-alpha.61

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 (58) hide show
  1. package/__assets__/package-og.svg +1 -1
  2. package/dist/bin.mjs +1 -1
  3. package/dist/index.d.mts +238 -115
  4. package/dist/index.d.ts +238 -115
  5. package/dist/index.mjs +8 -8
  6. package/dist/packem_chunks/handler.mjs +87 -6
  7. package/dist/packem_chunks/handler10.mjs +8 -14
  8. package/dist/packem_chunks/handler11.mjs +19 -189
  9. package/dist/packem_chunks/handler12.mjs +176 -115
  10. package/dist/packem_chunks/handler13.mjs +118 -52
  11. package/dist/packem_chunks/handler14.mjs +50 -43
  12. package/dist/packem_chunks/handler15.mjs +48 -67
  13. package/dist/packem_chunks/handler16.mjs +74 -37
  14. package/dist/packem_chunks/handler17.mjs +39 -100
  15. package/dist/packem_chunks/handler18.mjs +87 -152
  16. package/dist/packem_chunks/handler19.mjs +148 -67
  17. package/dist/packem_chunks/handler2.mjs +6 -4
  18. package/dist/packem_chunks/handler20.mjs +77 -75
  19. package/dist/packem_chunks/handler21.mjs +71 -288
  20. package/dist/packem_chunks/handler3.mjs +6 -3
  21. package/dist/packem_chunks/handler4.mjs +2 -2
  22. package/dist/packem_chunks/handler5.mjs +7 -4
  23. package/dist/packem_chunks/handler6.mjs +10 -7
  24. package/dist/packem_chunks/handler7.mjs +2 -2
  25. package/dist/packem_chunks/handler8.mjs +1 -1
  26. package/dist/packem_chunks/handler9.mjs +315 -12
  27. package/dist/packem_chunks/planDevCommand.mjs +561 -32
  28. package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
  29. package/dist/packem_chunks/runDeployCommand.mjs +115 -20
  30. package/dist/packem_chunks/runInitCommand.mjs +1601 -172
  31. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +5 -4
  32. package/dist/packem_chunks/runResetCommand.mjs +2 -2
  33. package/dist/packem_chunks/runRpcCommand.mjs +3 -2
  34. package/dist/packem_shared/{COMMANDS-Bn8luojF.mjs → COMMANDS-0BD3K1pb.mjs} +79 -21
  35. package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-CRQmmBZM.mjs} +3 -2
  36. package/dist/packem_shared/{api-spec-CtA6ilu4.mjs → api-spec-Bx0iKbxA.mjs} +3 -1
  37. package/dist/packem_shared/{buildRegistryIndex-BcYe607_.mjs → buildRegistryIndex-CqM5FwGI.mjs} +1 -1
  38. package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +31 -0
  39. package/dist/packem_shared/{command-BDXcJCCJ.mjs → command-lYnl4QyF.mjs} +6 -1
  40. package/dist/packem_shared/{commands-DqsEzojt.mjs → commands-vnRRkhNE.mjs} +123 -15
  41. package/dist/packem_shared/{createLogger-CHPNjFw2.mjs → createLogger-CIWSHrTL.mjs} +40 -8
  42. package/dist/packem_shared/{createRecordingSpawner-DxI3mebw.mjs → createRecordingSpawner-Cw5Iu73G.mjs} +12 -2
  43. package/dist/packem_shared/{detect-package-manager-DYp7n3mJ.mjs → detect-package-manager-Cxo6Tpyx.mjs} +10 -1
  44. package/dist/packem_shared/{insertSchemaExtension-BuzF6-t2.mjs → insertSchemaExtension-DAqbfr9Z.mjs} +15 -10
  45. package/dist/packem_shared/{open-url-Dfq6fAyT.mjs → open-url-4PBLY9X0.mjs} +3 -2
  46. package/dist/packem_shared/{output-format-7gyGR3h8.mjs → output-format-B4642rjE.mjs} +1 -1
  47. package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-BBrXCg9V.mjs} +16 -9
  48. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
  49. package/dist/packem_shared/runAddCommand-BCcJzs_k.mjs +4 -0
  50. package/dist/packem_shared/storage-bGfgwOgF.mjs +84 -0
  51. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +658 -0
  52. package/dist/packem_shared/wrangler-secrets-P2_ZUR-k.mjs +47 -0
  53. package/package.json +9 -7
  54. package/skills/lunora-quickstart/SKILL.md +25 -5
  55. package/skills/lunora-setup-storage/SKILL.md +7 -3
  56. package/dist/packem_shared/features-ocSSpZtS.mjs +0 -24
  57. package/dist/packem_shared/runAddCommand-G544_v6e.mjs +0 -4
  58. package/dist/packem_shared/tui-prompts-XHFxlOg5.mjs +0 -269
@@ -1,15 +1,19 @@
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, detectAiAgent, resolveRemoteEnabled, readProjectRemotePreference, detectFramework, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, fillDevSecrets, updateDevServerState, claimAgentRulesHint, AGENT_RULES_HINT, 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';
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-Cxo6Tpyx.mjs';
10
+ import { forceJsonLogging } from '../packem_shared/createLogger-CIWSHrTL.mjs';
11
+ import { spawnShellCompat } from '../packem_shared/createRecordingSpawner-Cw5Iu73G.mjs';
9
12
  import { createServer, request } from 'node:http';
10
13
  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';
14
+ 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';
15
+ import { c as createTuiConfirm } from '../packem_shared/tui-prompts-BjEN8XgP.mjs';
16
+ import { p as printJson } from '../packem_shared/output-format-B4642rjE.mjs';
13
17
 
14
18
  const DEFAULT_DEBOUNCE_MS = 100;
15
19
  const PATH_SEGMENT_SEPARATOR = /[/\\]/u;
@@ -18,7 +22,7 @@ const runOnce = (projectRoot, lunoraDirectory, apiSpec, logger, reason) => {
18
22
  runCodegen({ apiSpec, lunoraDirectory, projectRoot });
19
23
  logger.success(`codegen: wrote ${lunoraDirectory}/_generated (${reason})`);
20
24
  } catch (error) {
21
- logger.error(`codegen failed (${reason}): ${error instanceof Error ? error.message : String(error)}`);
25
+ logger.error(renderCodegenFailure(error, reason));
22
26
  }
23
27
  };
24
28
  const startCodegenWatch = (options) => {
@@ -165,7 +169,9 @@ const startStudioServer = async (options) => {
165
169
  response.end(deniedMessage);
166
170
  };
167
171
  const serveStaticAsset = (pathname, response) => {
168
- if (pathname !== "/studio.js" && pathname !== "/styles.css") {
172
+ const isStyle = pathname === "/styles.css";
173
+ const isModule = isStandaloneModulePath(pathname);
174
+ if (!isStyle && !isModule) {
169
175
  return false;
170
176
  }
171
177
  const stamp = studioAssetsStamp(import.meta.url);
@@ -179,8 +185,19 @@ const startStudioServer = async (options) => {
179
185
  response.end("Lunora studio assets not found — install and build @lunora/studio.");
180
186
  return true;
181
187
  }
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");
188
+ if (isStyle) {
189
+ sendAsset(response, assets.styles, "text/css; charset=utf-8");
190
+ return true;
191
+ }
192
+ const fileName = pathname.slice(pathname.lastIndexOf("/") + 1);
193
+ const bytes = readStandaloneAsset(fileName, import.meta.url);
194
+ if (bytes === void 0) {
195
+ response.statusCode = 404;
196
+ response.setHeader("Content-Type", "text/plain");
197
+ response.end("Not found");
198
+ return true;
199
+ }
200
+ sendAsset(response, bytes, assetContentType(fileName));
184
201
  return true;
185
202
  };
186
203
  const document = Buffer.from(html);
@@ -256,8 +273,386 @@ const startStudioServer = async (options) => {
256
273
  };
257
274
  };
258
275
 
276
+ const DEFAULT_READY_TIMEOUT_MS = 12e4;
277
+ const POLL_INTERVAL_MS = 250;
278
+ const STOP_GRACE_MS = 1e4;
279
+ const DEFAULT_LOG_LINES = 100;
280
+ const FAILURE_LOG_TAIL_LINES = 40;
281
+ const LOG_TAIL_MAX_BYTES = 256 * 1024;
282
+ const READY_TIMEOUT_ENV = "LUNORA_DEV_READY_TIMEOUT_MS";
283
+ const detectDevFlavor = (cwd) => readProjectDependencyNames(cwd).has("@lunora/vite") ? "vite" : "wrangler";
284
+ const viteDevCommand = (cwd) => {
285
+ const manager = detectPackageManager(cwd);
286
+ let script;
287
+ try {
288
+ const raw = readFileSync(join(cwd, "package.json"), "utf8");
289
+ script = JSON.parse(raw).scripts?.dev;
290
+ } catch {
291
+ }
292
+ if (script === void 0 || script.trim() === "" || script.includes("lunora")) {
293
+ const exec = execArgsFor(manager, "vite", ["dev"]);
294
+ return { args: exec.args, command: exec.command };
295
+ }
296
+ return { args: ["run", "dev"], command: manager };
297
+ };
298
+ const sleep = (ms) => new Promise((resolve) => {
299
+ setTimeout(resolve, ms);
300
+ });
301
+ const defaultProbe = async (origin) => {
302
+ try {
303
+ await fetch(new URL("/_lunora/status", origin), { signal: AbortSignal.timeout(1e3) });
304
+ return true;
305
+ } catch {
306
+ return false;
307
+ }
308
+ };
309
+ const defaultDetachedSpawner = (descriptor) => {
310
+ mkdirSync(dirname(descriptor.logPath), { recursive: true });
311
+ const logFd = openSync(descriptor.logPath, "w", 384);
312
+ let child;
313
+ const exec = spawnShellCompat(descriptor.command, descriptor.args);
314
+ try {
315
+ child = spawn(exec.command, exec.args, {
316
+ cwd: descriptor.cwd,
317
+ detached: true,
318
+ env: descriptor.env,
319
+ shell: exec.shell,
320
+ stdio: ["ignore", logFd, logFd],
321
+ windowsHide: true
322
+ });
323
+ } finally {
324
+ closeSync(logFd);
325
+ }
326
+ child.unref();
327
+ return {
328
+ exited: new Promise((resolve) => {
329
+ child.on("error", () => {
330
+ resolve(1);
331
+ });
332
+ child.on("exit", (code, signal) => {
333
+ resolve(code ?? (signal ? 1 : 0));
334
+ });
335
+ }),
336
+ pid: child.pid
337
+ };
338
+ };
339
+ const readLogWindow = (path) => {
340
+ try {
341
+ const { size } = statSync(path);
342
+ if (size <= LOG_TAIL_MAX_BYTES) {
343
+ return readFileSync(path, "utf8");
344
+ }
345
+ const fd = openSync(path, "r");
346
+ try {
347
+ const buffer = Buffer.alloc(LOG_TAIL_MAX_BYTES);
348
+ const read = readSync(fd, buffer, 0, LOG_TAIL_MAX_BYTES, size - LOG_TAIL_MAX_BYTES);
349
+ const text = buffer.toString("utf8", 0, read);
350
+ return text.slice(text.indexOf("\n") + 1);
351
+ } finally {
352
+ closeSync(fd);
353
+ }
354
+ } catch {
355
+ return void 0;
356
+ }
357
+ };
358
+ const readLogTail = (path, count) => {
359
+ const text = readLogWindow(path);
360
+ if (text === void 0) {
361
+ return [];
362
+ }
363
+ const lines = text.split("\n");
364
+ if (lines.at(-1) === "") {
365
+ lines.pop();
366
+ }
367
+ return count > 0 ? lines.slice(-count) : lines;
368
+ };
369
+ const resolveLogPath = (cwd, state) => state?.logFile ?? join(cwd, DEV_LOG_FILE);
370
+ const printLifecycleHints = (logger) => {
371
+ logger.info(" Stop: lunora dev stop");
372
+ logger.info(" Status: lunora dev status");
373
+ logger.info(" Logs: lunora dev logs");
374
+ };
375
+ const reportExistingServer = (logger, existing) => {
376
+ logger.warn(`Dev server already running at ${existing.url} (pid ${String(existing.pid)})`);
377
+ printLifecycleHints(logger);
378
+ };
379
+ const printBackgroundBanner = (logger, state) => {
380
+ logger.success(`Dev server running at ${state.url} (pid ${String(state.pid)})`);
381
+ if (state.studioUrl !== void 0) {
382
+ logger.info(` Studio: ${state.studioUrl}`);
383
+ }
384
+ printLifecycleHints(logger);
385
+ };
386
+ const waitUntilReady = async (parameters) => {
387
+ const { child, cwd, deadline, pollIntervalMs, probe } = parameters;
388
+ const exited = child.exited.then((code) => {
389
+ return { exitCode: code };
390
+ });
391
+ while (Date.now() < deadline) {
392
+ const state = readLiveDevServerState(cwd);
393
+ if (state !== void 0 && state.pid !== process.pid && await probe(state.url)) {
394
+ return { state, status: "ready" };
395
+ }
396
+ const raced = await Promise.race([exited, sleep(pollIntervalMs)]);
397
+ if (raced !== void 0) {
398
+ return { ...raced, status: "exited" };
399
+ }
400
+ }
401
+ return { status: "timeout" };
402
+ };
403
+ const runDevBackground = async (options) => {
404
+ const { cwd, logger } = options;
405
+ const logPath = resolveLogPath(cwd, void 0);
406
+ const environmentTimeout = Number(process.env[READY_TIMEOUT_ENV] ?? "");
407
+ const timeout = options.readyTimeoutMs ?? (Number.isFinite(environmentTimeout) && environmentTimeout > 0 ? environmentTimeout : DEFAULT_READY_TIMEOUT_MS);
408
+ const child = (options.spawnDetached ?? defaultDetachedSpawner)({
409
+ args: options.command.args,
410
+ command: options.command.command,
411
+ cwd,
412
+ env: {
413
+ ...process.env,
414
+ ...options.env,
415
+ [DEV_DAEMON_ENV]: "1",
416
+ [DEV_LOG_FILE_ENV]: logPath,
417
+ // The capture log is read back by `lunora dev logs` (and often an
418
+ // agent) — ANSI colour codes would only garble it.
419
+ ...process.env.FORCE_COLOR === void 0 ? { NO_COLOR: "1" } : {},
420
+ ...options.json ? { LUNORA_LOG_JSON: "1" } : {}
421
+ },
422
+ logPath
423
+ });
424
+ const outcome = await waitUntilReady({
425
+ child,
426
+ cwd,
427
+ deadline: Date.now() + timeout,
428
+ pollIntervalMs: options.pollIntervalMs ?? POLL_INTERVAL_MS,
429
+ probe: options.probe ?? defaultProbe
430
+ });
431
+ if (outcome.status === "ready") {
432
+ printBackgroundBanner(logger, outcome.state);
433
+ return { code: 0 };
434
+ }
435
+ if (outcome.status === "exited") {
436
+ logger.error(`dev server exited before becoming ready (exit code ${String(outcome.exitCode)}). Last output:`);
437
+ for (const line of readLogTail(logPath, FAILURE_LOG_TAIL_LINES)) {
438
+ logger.error(` ${line}`);
439
+ }
440
+ clearDevServerState(cwd, child.pid);
441
+ return { code: outcome.exitCode === 0 ? 1 : outcome.exitCode };
442
+ }
443
+ logger.warn(
444
+ `dev server did not confirm ready within ${String(Math.round(timeout / 1e3))}s — it may still be compiling. Check \`lunora dev status\` and \`lunora dev logs\`; \`lunora dev stop\` shuts it down.`
445
+ );
446
+ return { code: 1 };
447
+ };
448
+ const daemonArguments = (options, remote) => {
449
+ const args = ["dev"];
450
+ if (options.apiSpec !== void 0) {
451
+ args.push("--api-spec", options.apiSpec);
452
+ }
453
+ if (options.port !== void 0) {
454
+ args.push("--port", String(options.port));
455
+ }
456
+ if (options.workerPort !== void 0) {
457
+ args.push("--worker-port", String(options.workerPort));
458
+ }
459
+ if (options.codegen === false) {
460
+ args.push("--no-codegen");
461
+ }
462
+ if (options.studio === false) {
463
+ args.push("--no-studio");
464
+ }
465
+ if (remote) {
466
+ args.push("--remote");
467
+ }
468
+ return args;
469
+ };
470
+ const startBackground = async (context) => {
471
+ const { cwd, jsonLogs, logger, options, remote } = context;
472
+ const run = context.run ?? runDevBackground;
473
+ const flavor = detectDevFlavor(cwd);
474
+ const url = flavor === "vite" ? "http://localhost:5173" : `http://localhost:${String(options.workerPort ?? 8787)}`;
475
+ const claim = claimDevServerState(cwd, {
476
+ background: true,
477
+ mode: "cli",
478
+ pid: process.pid,
479
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
480
+ url
481
+ });
482
+ if (!claim.ok) {
483
+ if (claim.existing !== void 0) {
484
+ reportExistingServer(logger, claim.existing);
485
+ }
486
+ return { code: 0 };
487
+ }
488
+ const handoff = { [DEV_HANDOFF_ENV]: String(process.pid) };
489
+ try {
490
+ if (flavor === "vite") {
491
+ return await run({
492
+ command: viteDevCommand(cwd),
493
+ cwd,
494
+ env: { ...remote ? { LUNORA_REMOTE: "1" } : {}, ...handoff },
495
+ json: jsonLogs,
496
+ logger
497
+ });
498
+ }
499
+ return await run({
500
+ command: { args: [process.argv[1] ?? "lunora", ...daemonArguments(options, remote)], command: process.execPath },
501
+ cwd,
502
+ env: handoff,
503
+ json: jsonLogs,
504
+ logger
505
+ });
506
+ } finally {
507
+ clearDevServerState(cwd, process.pid);
508
+ }
509
+ };
510
+ const processGroupId = (pid) => {
511
+ try {
512
+ const result = spawnSync("ps", ["-o", "pgid=", "-p", String(pid)], { encoding: "utf8" });
513
+ const pgid = Number.parseInt(result.stdout.trim(), 10);
514
+ return Number.isInteger(pgid) && pgid > 0 ? pgid : void 0;
515
+ } catch {
516
+ return void 0;
517
+ }
518
+ };
519
+ const forceKillRecordedServer = (state, signal) => {
520
+ if (process.platform === "win32") {
521
+ try {
522
+ spawnSync("taskkill", ["/pid", String(state.pid), "/T", "/F"], { stdio: "ignore" });
523
+ } catch {
524
+ }
525
+ return;
526
+ }
527
+ if (state.background === true) {
528
+ try {
529
+ signal(-(processGroupId(state.pid) ?? state.pid), "SIGKILL");
530
+ return;
531
+ } catch {
532
+ }
533
+ }
534
+ try {
535
+ signal(state.pid, "SIGKILL");
536
+ } catch {
537
+ }
538
+ };
539
+ const runDevStop = async (options) => {
540
+ const { cwd, logger } = options;
541
+ const state = readDevServerState(cwd);
542
+ const alive = options.alive ?? ((pid) => pid === state?.pid && isRecordedProcessCurrent(state));
543
+ const signal = options.signal ?? ((pid, sig) => {
544
+ process.kill(pid, sig);
545
+ });
546
+ const pollInterval = options.pollIntervalMs ?? POLL_INTERVAL_MS;
547
+ const grace = options.stopGraceMs ?? STOP_GRACE_MS;
548
+ if (state === void 0 || !alive(state.pid)) {
549
+ if (state !== void 0) {
550
+ clearDevServerState(cwd, state.pid);
551
+ }
552
+ if (options.json) {
553
+ printJson({ running: false, stopped: false });
554
+ } else {
555
+ logger.info("No dev server running.");
556
+ }
557
+ return { code: 0 };
558
+ }
559
+ try {
560
+ signal(state.pid, "SIGTERM");
561
+ } catch {
562
+ }
563
+ const deadline = Date.now() + grace;
564
+ while (alive(state.pid) && Date.now() < deadline) {
565
+ await sleep(pollInterval);
566
+ }
567
+ if (alive(state.pid)) {
568
+ forceKillRecordedServer(state, signal);
569
+ }
570
+ clearDevServerState(cwd, state.pid);
571
+ if (options.json) {
572
+ printJson({ pid: state.pid, stopped: true });
573
+ } else {
574
+ logger.success(`Stopped dev server (pid ${String(state.pid)}).`);
575
+ }
576
+ return { code: 0 };
577
+ };
578
+ const runDevStatus = (options) => {
579
+ const { cwd, logger } = options;
580
+ const state = readLiveDevServerState(cwd);
581
+ if (state === void 0) {
582
+ if (options.json) {
583
+ printJson({ running: false });
584
+ } else {
585
+ logger.info("No dev server running.");
586
+ }
587
+ return { code: 0 };
588
+ }
589
+ const now = options.now ?? Date.now;
590
+ const startedAtMs = state.startedAt === void 0 ? Number.NaN : Date.parse(state.startedAt);
591
+ const uptimeSeconds = Number.isFinite(startedAtMs) ? Math.max(0, Math.round((now() - startedAtMs) / 1e3)) : void 0;
592
+ if (options.json) {
593
+ printJson({
594
+ background: state.background === true,
595
+ logFile: state.logFile,
596
+ mode: state.mode,
597
+ pid: state.pid,
598
+ running: true,
599
+ startedAt: state.startedAt,
600
+ studioUrl: state.studioUrl,
601
+ uptimeSeconds,
602
+ url: state.url
603
+ });
604
+ return { code: 0 };
605
+ }
606
+ const details = [
607
+ `pid ${String(state.pid)}`,
608
+ ...uptimeSeconds === void 0 ? [] : [`uptime ${String(uptimeSeconds)}s`],
609
+ state.background === true ? "background" : "foreground"
610
+ ];
611
+ logger.success(`Dev server running at ${state.url} (${details.join(", ")})`);
612
+ if (state.studioUrl !== void 0) {
613
+ logger.info(` Studio: ${state.studioUrl}`);
614
+ }
615
+ if (state.logFile !== void 0) {
616
+ logger.info(` Logs: lunora dev logs (${state.logFile})`);
617
+ }
618
+ return { code: 0 };
619
+ };
620
+ const runDevLogs = (options) => {
621
+ const { cwd, logger } = options;
622
+ const state = readDevServerState(cwd);
623
+ const logPath = resolveLogPath(cwd, state);
624
+ if (!existsSync(logPath)) {
625
+ logger.info("No dev server logs found — output is captured when the server runs in background mode (`lunora dev --background`).");
626
+ return { code: 0 };
627
+ }
628
+ const tail = readLogTail(logPath, options.lines ?? DEFAULT_LOG_LINES);
629
+ if (tail.length > 0) {
630
+ process.stdout.write(`${tail.join("\n")}
631
+ `);
632
+ }
633
+ return { code: 0 };
634
+ };
635
+ const runLifecycleSubcommand = (parameters) => {
636
+ const { cwd, json, lines, logger, subcommand } = parameters;
637
+ if (subcommand === "stop") {
638
+ return runDevStop({ cwd, json, logger });
639
+ }
640
+ if (subcommand === "status") {
641
+ return runDevStatus({ cwd, json, logger });
642
+ }
643
+ if (subcommand === "logs") {
644
+ return runDevLogs({ cwd, lines, logger });
645
+ }
646
+ if (subcommand !== void 0) {
647
+ logger.error(`dev: unknown subcommand "${subcommand}" — expected stop | status | logs (or no subcommand to start the dev server)`);
648
+ return { code: 1 };
649
+ }
650
+ return void 0;
651
+ };
652
+
259
653
  const DEFAULT_STUDIO_PORT = 6173;
260
654
  const DEFAULT_WORKER_PORT = 8787;
655
+ const DEFAULT_VITE_PORT = 5173;
261
656
  const SIGINT_GRACE_MS = 5e3;
262
657
  const resolveRemotePlan = (options, cwd) => {
263
658
  const noopCleanup = () => {
@@ -276,12 +671,37 @@ const resolveRemotePlan = (options, cwd) => {
276
671
  };
277
672
  const planDevCommand = (options) => {
278
673
  const cwd = options.cwd ?? process.cwd();
279
- const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
280
674
  const manager = detectPackageManager(cwd);
675
+ const flavor = options.flavor ?? detectDevFlavor(cwd);
676
+ if (flavor === "vite") {
677
+ const exec2 = viteDevCommand(cwd);
678
+ return {
679
+ codegenEnabled: false,
680
+ flavor,
681
+ remote: { bindings: [], cleanup: () => {
682
+ }, enabled: options.remote === true },
683
+ studioEnabled: false,
684
+ studioPort: options.port ?? DEFAULT_STUDIO_PORT,
685
+ workerOrigin: `http://localhost:${String(DEFAULT_VITE_PORT)}`,
686
+ workerPort: DEFAULT_VITE_PORT,
687
+ wrangler: {
688
+ args: exec2.args,
689
+ command: exec2.command,
690
+ cwd,
691
+ ...options.remote === true ? { env: { LUNORA_REMOTE: "1" } } : {},
692
+ tag: "vite"
693
+ }
694
+ };
695
+ }
696
+ const detection = detectFramework(cwd);
697
+ 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.`;
698
+ const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
281
699
  const remote = resolveRemotePlan(options, cwd);
282
700
  const exec = execArgsFor(manager, "wrangler", ["dev", "--port", String(workerPort), "--var", "WORKER_ENV:development", ...remote.args]);
283
701
  return {
284
702
  codegenEnabled: options.codegen !== false,
703
+ flavor,
704
+ frameworkHint,
285
705
  remote: remote.plan,
286
706
  studioEnabled: options.studio !== false,
287
707
  studioPort: options.port ?? DEFAULT_STUDIO_PORT,
@@ -329,9 +749,11 @@ const pipeChildOutput = (child, tag, logger) => {
329
749
  pumpStream(child.stderr, "stderr");
330
750
  };
331
751
  const defaultWorkerSpawner = (descriptor, logger) => {
332
- const child = spawn(descriptor.command, [...descriptor.args], {
752
+ const exec = spawnShellCompat(descriptor.command, descriptor.args);
753
+ const child = spawn(exec.command, exec.args, {
333
754
  cwd: descriptor.cwd ?? process.cwd(),
334
- env: process.env,
755
+ env: descriptor.env ? { ...process.env, ...descriptor.env } : process.env,
756
+ shell: exec.shell,
335
757
  stdio: ["inherit", "pipe", "pipe"]
336
758
  });
337
759
  pipeChildOutput(child, descriptor.tag, logger);
@@ -379,8 +801,35 @@ const printAgentRulesHint = (logger, cwd) => {
379
801
  logger.info(` ⓘ ${AGENT_RULES_HINT}`);
380
802
  logger.info("");
381
803
  };
804
+ const startContainerLogStreaming = (cwd, logger) => {
805
+ if (process.env.LUNORA_CONTAINER_LOGS === "0") {
806
+ return void 0;
807
+ }
808
+ const discovery = discoverContainerInfo(cwd, "lunora");
809
+ const containers = discovery.containers.map((container) => {
810
+ return { className: container.className, exportName: container.exportName };
811
+ });
812
+ if (containers.length === 0) {
813
+ return void 0;
814
+ }
815
+ return streamContainerLogs({
816
+ containers,
817
+ onLine: (line) => {
818
+ const tagged = `[container:${line.name}] ${line.text}`;
819
+ if (line.level === "error") {
820
+ logger.warn(tagged);
821
+ } else {
822
+ logger.info(tagged);
823
+ }
824
+ },
825
+ onUnavailable: (message) => {
826
+ logger.warn(`[container] Docker engine unreachable — container logs unavailable (${message})`);
827
+ }
828
+ });
829
+ };
382
830
  const teardown = async (handles) => {
383
831
  handles.codegen?.close();
832
+ handles.containerLogs?.close();
384
833
  await handles.studio?.close().catch(() => void 0);
385
834
  try {
386
835
  handles.remoteCleanup?.();
@@ -409,6 +858,46 @@ const offerDevVariablesScaffold = async (options, cwd) => {
409
858
  `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
859
  );
411
860
  }
861
+ try {
862
+ (options.fillSecrets ?? fillDevSecrets)({
863
+ cwd,
864
+ info: (message) => {
865
+ options.logger.info(message);
866
+ }
867
+ });
868
+ } catch {
869
+ }
870
+ };
871
+ const afterWorkerSpawn = (plan, cwd, logger, studioUrl) => {
872
+ if (plan.flavor !== "wrangler") {
873
+ return void 0;
874
+ }
875
+ if (studioUrl !== void 0) {
876
+ updateDevServerState(cwd, { studioUrl });
877
+ }
878
+ let containerLogs;
879
+ try {
880
+ containerLogs = startContainerLogStreaming(cwd, logger);
881
+ } catch {
882
+ }
883
+ printBanner(logger, plan, studioUrl);
884
+ return containerLogs;
885
+ };
886
+ const claimStartRecord = (plan, cwd) => {
887
+ const handoffPid = Number(process.env[DEV_HANDOFF_ENV]);
888
+ const claim = claimDevServerState(
889
+ cwd,
890
+ {
891
+ background: process.env[DEV_DAEMON_ENV] === "1",
892
+ logFile: process.env[DEV_LOG_FILE_ENV],
893
+ mode: "cli",
894
+ pid: process.pid,
895
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
896
+ url: plan.workerOrigin
897
+ },
898
+ Number.isInteger(handoffPid) && handoffPid > 0 ? { supersedePid: handoffPid } : void 0
899
+ );
900
+ return claim.ok ? void 0 : claim.existing;
412
901
  };
413
902
  const runDevCommand = async (options) => {
414
903
  const plan = planDevCommand(options);
@@ -416,8 +905,23 @@ const runDevCommand = async (options) => {
416
905
  const cwd = plan.wrangler.cwd ?? process.cwd();
417
906
  const handles = { remoteCleanup: plan.remote.cleanup };
418
907
  try {
908
+ const existing = readLiveDevServerState(cwd);
909
+ if (existing !== void 0 && existing.pid !== process.pid) {
910
+ reportExistingServer(logger, existing);
911
+ return { code: 0, plan };
912
+ }
913
+ const incumbent = claimStartRecord(plan, cwd);
914
+ if (incumbent !== void 0) {
915
+ reportExistingServer(logger, incumbent);
916
+ return { code: 0, plan };
917
+ }
918
+ if (plan.flavor === "vite") {
919
+ plan.wrangler.env = { ...plan.wrangler.env, [DEV_HANDOFF_ENV]: String(process.pid) };
920
+ }
419
921
  await offerDevVariablesScaffold(options, cwd);
420
- logger.info("starting wrangler dev + studio");
922
+ logger.info(
923
+ plan.flavor === "vite" ? "starting vite dev (worker + studio + codegen run inside Vite via @lunora/vite)" : "starting wrangler dev + studio"
924
+ );
421
925
  if (plan.codegenEnabled) {
422
926
  handles.codegen = (options.startCodegen ?? startCodegenWatch)({ apiSpec: options.apiSpec, logger, projectRoot: cwd });
423
927
  }
@@ -439,8 +943,11 @@ const runDevCommand = async (options) => {
439
943
  logger.warn(`studio server failed to start (${error instanceof Error ? error.message : String(error)}) — continuing without it`);
440
944
  }
441
945
  }
946
+ if (plan.frameworkHint !== void 0) {
947
+ logger.warn(plan.frameworkHint);
948
+ }
442
949
  const worker = (options.startWorker ?? defaultWorkerSpawner)(plan.wrangler, logger);
443
- printBanner(logger, plan, studioUrl);
950
+ handles.containerLogs = afterWorkerSpawn(plan, cwd, logger, studioUrl);
444
951
  printAgentRulesHint(logger, cwd);
445
952
  let sigintCount = 0;
446
953
  let escalationTimer;
@@ -470,11 +977,41 @@ const runDevCommand = async (options) => {
470
977
  process.off("SIGTERM", onSigterm);
471
978
  return { code, plan };
472
979
  } finally {
980
+ clearDevServerState(cwd, process.pid);
473
981
  await teardown(handles);
474
982
  }
475
983
  };
476
- const execute = defineHandler(
477
- ({ cwd, logger, options }) => runDevCommand({
984
+ const execute = defineHandler(async ({ argument, cwd, logger, options }) => {
985
+ const json = options.json === true;
986
+ const dispatched = runLifecycleSubcommand({ cwd, json, lines: options.lines, logger, subcommand: argument[0] });
987
+ if (dispatched !== void 0) {
988
+ return dispatched;
989
+ }
990
+ const isDaemon = process.env[DEV_DAEMON_ENV] === "1";
991
+ const agent = isDaemon ? void 0 : detectAiAgent();
992
+ const jsonLogs = json || agent !== void 0;
993
+ if (jsonLogs) {
994
+ forceJsonLogging();
995
+ }
996
+ if (agent !== void 0 && options.background !== true) {
997
+ logger.info(
998
+ `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.`
999
+ );
1000
+ }
1001
+ const remote = resolveRemoteEnabled({
1002
+ configPreference: readProjectRemotePreference(cwd),
1003
+ envValue: process.env["LUNORA_REMOTE"],
1004
+ flag: options.remote
1005
+ });
1006
+ if (!isDaemon && (options.background === true || agent !== void 0)) {
1007
+ const existing = readLiveDevServerState(cwd);
1008
+ if (existing !== void 0) {
1009
+ reportExistingServer(logger, existing);
1010
+ return { code: 0 };
1011
+ }
1012
+ return startBackground({ cwd, jsonLogs, logger, options, remote });
1013
+ }
1014
+ return runDevCommand({
478
1015
  apiSpec: parseApiSpec(options.apiSpec),
479
1016
  // cerebro parses `--no-codegen`/`--no-studio` as the negation of the
480
1017
  // `codegen`/`studio` booleans (runtime key drops the `no-` prefix), so a
@@ -483,18 +1020,10 @@ const execute = defineHandler(
483
1020
  cwd,
484
1021
  logger,
485
1022
  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
- }),
1023
+ remote,
495
1024
  studio: options.studio === false ? false : void 0,
496
1025
  workerPort: options.workerPort
497
- })
498
- );
1026
+ });
1027
+ });
499
1028
 
500
- export { execute, planDevCommand, resolveRemotePlan, runDevCommand };
1029
+ export { detectDevFlavor, execute, planDevCommand, resolveRemotePlan, runDevCommand };