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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -452,15 +452,33 @@ interface StudioServerHandle {
452
452
  url: string;
453
453
  }
454
454
  /**
455
- * How the dev child runs. `wrangler` is the classic `lunora dev` stack
456
- * (wrangler worker + embedded studio + codegen watch). `vite` is a project on
457
- * `@lunora/vite`: the Vite plugin already runs the worker, studio, and codegen
458
- * inside the Vite dev server, so `lunora dev` runs the project's own dev
459
- * script and gets out of the way — that's what makes `--background` / `stop` /
460
- * `status` / `logs` work uniformly for Vite projects too.
455
+ * How the dev child runs. `wrangler` is the classic `lunora dev` stack (wrangler
456
+ * worker + embedded studio + codegen watch) for a standalone class-C project.
457
+ * `vite` is a project on `@lunora/vite`: the plugin already runs the worker,
458
+ * studio, and codegen inside the Vite dev server, so `lunora dev` runs the
459
+ * project's own dev script and gets out of the way — this also covers class-B
460
+ * frameworks whose own dev server runs the worker in `workerd` (Astro 6 +
461
+ * `@astrojs/cloudflare`, which embeds `@cloudflare/vite-plugin` in `astro dev`:
462
+ * SSR + `/_lunora/*` + `ShardDO` in one process, HMR intact). `framework-worker`
463
+ * is a class-B framework whose dev server CANNOT host the `ShardDO` Durable
464
+ * Object (SvelteKit / Nuxt: their adapters use wrangler's `getPlatformProxy()`,
465
+ * which runs an empty-script Miniflare and does not emulate internal DOs); there
466
+ * `lunora dev` runs the framework's own dev server (front door, HMR, and — via
467
+ * its `@lunora/vite` plugin — studio + codegen) AND a second `wrangler dev`
468
+ * sidecar that owns the real `ShardDO` in `workerd`, wired via the committed
469
+ * `wrangler.dev.jsonc`.
470
+ */
471
+ type DevFlavor = "framework-worker" | "vite" | "wrangler";
472
+ /**
473
+ * Detect the dev flavor.
474
+ *
475
+ * A SvelteKit / Nuxt project needs the two-process `framework-worker` stack even
476
+ * though it also declares `@lunora/vite` (which its framework dev server uses for
477
+ * codegen/studio) — so the framework check comes FIRST. Everything else on
478
+ * `@lunora/vite` (class-A frameworks + Astro + standalone Vite) delegates to the
479
+ * project's own dev server (`vite`); a project without `@lunora/vite` is the
480
+ * classic standalone `wrangler` stack.
461
481
  */
462
- type DevFlavor = "vite" | "wrangler";
463
- /** Detect the dev flavor: a declared `@lunora/vite` dependency means Vite owns the dev server. */
464
482
  /** A running worker child the orchestrator controls: send signals, await its exit. */
465
483
  interface WorkerProcess {
466
484
  /** Resolves with the worker's exit code (1 if it failed to start). */
@@ -543,11 +561,22 @@ interface DevCommandPlan {
543
561
  ipv4LoopbackForced: boolean;
544
562
  /** The remote-binding decision: which D1/KV/R2 bindings hit the deployed worker. */
545
563
  remote: DevRemotePlan;
564
+ /**
565
+ * The `wrangler dev` sidecar for the `framework-worker` flavor (SvelteKit /
566
+ * Nuxt): a second child that owns the real `ShardDO` in `workerd`, wired via
567
+ * the committed `wrangler.dev.jsonc`. `undefined` for every other flavor —
568
+ * only the two-process class-B stack has a sidecar. When present, `wrangler`
569
+ * (above) is the framework's own dev server (the front door / HMR) and this
570
+ * is the Lunora realtime plane.
571
+ */
572
+ sidecar?: SpawnDescriptor & {
573
+ tag: string;
574
+ };
546
575
  studioEnabled: boolean;
547
576
  studioPort: number;
548
577
  workerOrigin: string;
549
578
  workerPort: number;
550
- /** The single child process `lunora dev` spawns: `wrangler dev` (or `vite dev` for the vite flavor). */
579
+ /** The primary child `lunora dev` spawns: `wrangler dev` (wrangler flavor) or the framework/`vite dev` server (vite / framework-worker). */
551
580
  wrangler: SpawnDescriptor & {
552
581
  tag: string;
553
582
  };
package/dist/index.d.ts CHANGED
@@ -452,15 +452,33 @@ interface StudioServerHandle {
452
452
  url: string;
453
453
  }
454
454
  /**
455
- * How the dev child runs. `wrangler` is the classic `lunora dev` stack
456
- * (wrangler worker + embedded studio + codegen watch). `vite` is a project on
457
- * `@lunora/vite`: the Vite plugin already runs the worker, studio, and codegen
458
- * inside the Vite dev server, so `lunora dev` runs the project's own dev
459
- * script and gets out of the way — that's what makes `--background` / `stop` /
460
- * `status` / `logs` work uniformly for Vite projects too.
455
+ * How the dev child runs. `wrangler` is the classic `lunora dev` stack (wrangler
456
+ * worker + embedded studio + codegen watch) for a standalone class-C project.
457
+ * `vite` is a project on `@lunora/vite`: the plugin already runs the worker,
458
+ * studio, and codegen inside the Vite dev server, so `lunora dev` runs the
459
+ * project's own dev script and gets out of the way — this also covers class-B
460
+ * frameworks whose own dev server runs the worker in `workerd` (Astro 6 +
461
+ * `@astrojs/cloudflare`, which embeds `@cloudflare/vite-plugin` in `astro dev`:
462
+ * SSR + `/_lunora/*` + `ShardDO` in one process, HMR intact). `framework-worker`
463
+ * is a class-B framework whose dev server CANNOT host the `ShardDO` Durable
464
+ * Object (SvelteKit / Nuxt: their adapters use wrangler's `getPlatformProxy()`,
465
+ * which runs an empty-script Miniflare and does not emulate internal DOs); there
466
+ * `lunora dev` runs the framework's own dev server (front door, HMR, and — via
467
+ * its `@lunora/vite` plugin — studio + codegen) AND a second `wrangler dev`
468
+ * sidecar that owns the real `ShardDO` in `workerd`, wired via the committed
469
+ * `wrangler.dev.jsonc`.
470
+ */
471
+ type DevFlavor = "framework-worker" | "vite" | "wrangler";
472
+ /**
473
+ * Detect the dev flavor.
474
+ *
475
+ * A SvelteKit / Nuxt project needs the two-process `framework-worker` stack even
476
+ * though it also declares `@lunora/vite` (which its framework dev server uses for
477
+ * codegen/studio) — so the framework check comes FIRST. Everything else on
478
+ * `@lunora/vite` (class-A frameworks + Astro + standalone Vite) delegates to the
479
+ * project's own dev server (`vite`); a project without `@lunora/vite` is the
480
+ * classic standalone `wrangler` stack.
461
481
  */
462
- type DevFlavor = "vite" | "wrangler";
463
- /** Detect the dev flavor: a declared `@lunora/vite` dependency means Vite owns the dev server. */
464
482
  /** A running worker child the orchestrator controls: send signals, await its exit. */
465
483
  interface WorkerProcess {
466
484
  /** Resolves with the worker's exit code (1 if it failed to start). */
@@ -543,11 +561,22 @@ interface DevCommandPlan {
543
561
  ipv4LoopbackForced: boolean;
544
562
  /** The remote-binding decision: which D1/KV/R2 bindings hit the deployed worker. */
545
563
  remote: DevRemotePlan;
564
+ /**
565
+ * The `wrangler dev` sidecar for the `framework-worker` flavor (SvelteKit /
566
+ * Nuxt): a second child that owns the real `ShardDO` in `workerd`, wired via
567
+ * the committed `wrangler.dev.jsonc`. `undefined` for every other flavor —
568
+ * only the two-process class-B stack has a sidecar. When present, `wrangler`
569
+ * (above) is the framework's own dev server (the front door / HMR) and this
570
+ * is the Lunora realtime plane.
571
+ */
572
+ sidecar?: SpawnDescriptor & {
573
+ tag: string;
574
+ };
546
575
  studioEnabled: boolean;
547
576
  studioPort: number;
548
577
  workerOrigin: string;
549
578
  workerPort: number;
550
- /** The single child process `lunora dev` spawns: `wrangler dev` (or `vite dev` for the vite flavor). */
579
+ /** The primary child `lunora dev` spawns: `wrangler dev` (wrangler flavor) or the framework/`vite dev` server (vite / framework-worker). */
551
580
  wrangler: SpawnDescriptor & {
552
581
  tag: string;
553
582
  };
@@ -1,9 +1,9 @@
1
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';
2
+ import { runCodegen } from '@lunora/codegen';
3
+ import { detectAgentRules, claimDevServerState, clearDevServerState, readDevServerState, readLiveDevServerState, detectFramework, readProjectDependencyNames, DEV_HANDOFF_ENV, isRecordedProcessCurrent, DEV_LOG_FILE, DEV_LOG_FILE_ENV, DEV_DAEMON_ENV, updateDevServerState, detectAiAgent, resolveRemoteEnabled, readProjectRemotePreference, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, fillDevSecrets, claimAgentRulesHint, AGENT_RULES_HINT, findWranglerFile, readWranglerJsonc, discoverContainerInfo, streamContainerLogs, materializeRemoteWranglerConfig, formatLunoraEvent } from '@lunora/config';
3
4
  import { p as parseApiSpec } from '../packem_shared/api-spec-Bx0iKbxA.mjs';
4
5
  import { existsSync, watch, readFileSync, statSync, openSync, readSync, closeSync, mkdirSync } from 'node:fs';
5
6
  import { join, dirname } from 'node:path';
6
- import { runCodegen } from '@lunora/codegen';
7
7
  import { r as renderCodegenFailure } from '../packem_shared/codegen-error-DJG-ghs_.mjs';
8
8
  import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
9
9
  import { d as detectPackageManager, e as execArgsFor, r as runScriptCommand } from '../packem_shared/detect-package-manager-DvEthdCw.mjs';
@@ -307,7 +307,13 @@ const DEFAULT_LOG_LINES = 100;
307
307
  const FAILURE_LOG_TAIL_LINES = 40;
308
308
  const LOG_TAIL_MAX_BYTES = 256 * 1024;
309
309
  const READY_TIMEOUT_ENV = "LUNORA_DEV_READY_TIMEOUT_MS";
310
- const detectDevFlavor = (cwd) => readProjectDependencyNames(cwd).has("@lunora/vite") ? "vite" : "wrangler";
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
+ };
311
317
  const viteDevCommand = (cwd) => {
312
318
  const manager = detectPackageManager(cwd);
313
319
  let script;
@@ -505,7 +511,7 @@ const startBackground = async (context) => {
505
511
  const { cwd, jsonLogs, logger, options, remote } = context;
506
512
  const run = context.run ?? runDevBackground;
507
513
  const flavor = detectDevFlavor(cwd);
508
- const url = flavor === "vite" ? "http://localhost:5173" : `http://localhost:${String(options.workerPort ?? 8787)}`;
514
+ const url = flavor === "wrangler" ? `http://localhost:${String(options.workerPort ?? 8787)}` : "http://localhost:5173";
509
515
  const claim = claimDevServerState(cwd, {
510
516
  background: true,
511
517
  mode: "cli",
@@ -700,6 +706,7 @@ const runLifecycleSubcommand = (parameters) => {
700
706
  return void 0;
701
707
  };
702
708
 
709
+ const DEV_WRANGLER_CONFIG = "wrangler.dev.jsonc";
703
710
  const DEFAULT_STUDIO_PORT = 6173;
704
711
  const DEFAULT_WORKER_PORT = 8787;
705
712
  const DEFAULT_VITE_PORT = 5173;
@@ -746,14 +753,21 @@ const planDevCommand = (options) => {
746
753
  const cwd = options.cwd ?? process.cwd();
747
754
  const manager = detectPackageManager(cwd);
748
755
  const flavor = options.flavor ?? detectDevFlavor(cwd);
749
- if (flavor === "vite") {
756
+ if (flavor === "vite" || flavor === "framework-worker") {
750
757
  const exec2 = viteDevCommand(cwd);
758
+ let sidecar;
759
+ if (flavor === "framework-worker") {
760
+ const loopbackArgs2 = resolveLoopbackArgs(cwd, options.hasIpv6Loopback ?? hasIpv6Loopback);
761
+ const sidecarExec = execArgsFor(manager, "wrangler", ["dev", "--config", DEV_WRANGLER_CONFIG, ...loopbackArgs2, "--var", "WORKER_ENV:development"]);
762
+ sidecar = { args: sidecarExec.args, command: sidecarExec.command, cwd, tag: "worker" };
763
+ }
751
764
  return {
752
765
  codegenEnabled: false,
753
766
  flavor,
754
767
  ipv4LoopbackForced: false,
755
768
  remote: { bindings: [], cleanup: () => {
756
769
  }, enabled: options.remote === true },
770
+ ...sidecar ? { sidecar } : {},
757
771
  studioEnabled: false,
758
772
  studioPort: options.port ?? DEFAULT_STUDIO_PORT,
759
773
  workerOrigin: `http://localhost:${String(DEFAULT_VITE_PORT)}`,
@@ -981,14 +995,99 @@ const buildDevPlan = async (options) => {
981
995
  const workerPort = flavor === "wrangler" ? await resolveWorkerPort(options, cwd) : options.workerPort;
982
996
  return planDevCommand({ ...options, cwd, flavor, workerPort });
983
997
  };
998
+ const superviseWorkers = async (worker, sidecar, logger) => {
999
+ let sigintCount = 0;
1000
+ let escalationTimer;
1001
+ const killChildren = (signal) => {
1002
+ worker.kill(signal);
1003
+ sidecar?.kill(signal);
1004
+ };
1005
+ const onSigint = () => {
1006
+ sigintCount += 1;
1007
+ if (sigintCount === 1) {
1008
+ logger.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)");
1009
+ killChildren("SIGTERM");
1010
+ escalationTimer = setTimeout(() => {
1011
+ killChildren("SIGKILL");
1012
+ }, SIGINT_GRACE_MS);
1013
+ escalationTimer.unref();
1014
+ } else {
1015
+ killChildren("SIGKILL");
1016
+ }
1017
+ };
1018
+ const onSigterm = () => {
1019
+ killChildren("SIGTERM");
1020
+ };
1021
+ process.on("SIGINT", onSigint);
1022
+ process.on("SIGTERM", onSigterm);
1023
+ const exits = [
1024
+ worker.exited.then((code) => {
1025
+ return { code, who: "worker" };
1026
+ })
1027
+ ];
1028
+ if (sidecar !== void 0) {
1029
+ exits.push(
1030
+ sidecar.exited.then((code) => {
1031
+ return { code, who: "sidecar" };
1032
+ })
1033
+ );
1034
+ }
1035
+ const first = await Promise.race(exits);
1036
+ if (sidecar !== void 0) {
1037
+ if (first.who === "sidecar") {
1038
+ logger.warn("[worker] the Lunora sidecar (wrangler dev) exited — shutting down the framework dev server");
1039
+ worker.kill("SIGTERM");
1040
+ } else {
1041
+ sidecar.kill("SIGTERM");
1042
+ }
1043
+ await Promise.allSettled([worker.exited, sidecar.exited]);
1044
+ }
1045
+ if (escalationTimer) {
1046
+ clearTimeout(escalationTimer);
1047
+ }
1048
+ process.off("SIGINT", onSigint);
1049
+ process.off("SIGTERM", onSigterm);
1050
+ return first.code;
1051
+ };
1052
+ const startStudioBestEffort = async (options, plan, cwd, logger) => {
1053
+ if (!plan.studioEnabled) {
1054
+ return void 0;
1055
+ }
1056
+ try {
1057
+ return await (options.startStudio ?? startStudioServer)({
1058
+ cwd,
1059
+ logger: {
1060
+ warnOnce: (message) => {
1061
+ logger.warn(message);
1062
+ }
1063
+ },
1064
+ port: plan.studioPort,
1065
+ workerOrigin: plan.workerOrigin
1066
+ });
1067
+ } catch (error) {
1068
+ logger.warn(`studio server failed to start (${error instanceof Error ? error.message : String(error)}) — continuing without it`);
1069
+ return void 0;
1070
+ }
1071
+ };
1072
+ const ensureSidecarGenerated = (plan, options, cwd, logger) => {
1073
+ if (plan.sidecar === void 0) {
1074
+ return;
1075
+ }
1076
+ try {
1077
+ runCodegen({ apiSpec: options.apiSpec, lunoraDirectory: "lunora", projectRoot: cwd });
1078
+ } catch (error) {
1079
+ logger.warn(`codegen (pre-sidecar) failed: ${error instanceof Error ? error.message : String(error)} — the framework dev server will retry`);
1080
+ }
1081
+ };
984
1082
  const runDevCommand = async (options) => {
985
1083
  const plan = await buildDevPlan(options);
986
1084
  const { logger } = options;
987
1085
  const cwd = plan.wrangler.cwd ?? process.cwd();
988
1086
  const handles = { remoteCleanup: plan.remote.cleanup };
989
1087
  try {
1088
+ const handoffPid = Number(process.env[DEV_HANDOFF_ENV]);
990
1089
  const existing = readLiveDevServerState(cwd);
991
- if (existing !== void 0 && existing.pid !== process.pid) {
1090
+ if (existing !== void 0 && existing.pid !== process.pid && existing.pid !== handoffPid) {
992
1091
  reportExistingServer(logger, existing);
993
1092
  return { code: 0, plan };
994
1093
  }
@@ -997,7 +1096,7 @@ const runDevCommand = async (options) => {
997
1096
  reportExistingServer(logger, incumbent);
998
1097
  return { code: 0, plan };
999
1098
  }
1000
- if (plan.flavor === "vite") {
1099
+ if (plan.flavor === "vite" || plan.flavor === "framework-worker") {
1001
1100
  plan.wrangler.env = { ...plan.wrangler.env, [DEV_HANDOFF_ENV]: String(process.pid) };
1002
1101
  }
1003
1102
  await offerDevVariablesScaffold(options, cwd);
@@ -1012,56 +1111,18 @@ const runDevCommand = async (options) => {
1012
1111
  if (plan.codegenEnabled) {
1013
1112
  handles.codegen = (options.startCodegen ?? startCodegenWatch)({ apiSpec: options.apiSpec, logger, projectRoot: cwd });
1014
1113
  }
1015
- let studioUrl;
1016
- if (plan.studioEnabled) {
1017
- try {
1018
- handles.studio = await (options.startStudio ?? startStudioServer)({
1019
- cwd,
1020
- logger: {
1021
- warnOnce: (message) => {
1022
- logger.warn(message);
1023
- }
1024
- },
1025
- port: plan.studioPort,
1026
- workerOrigin: plan.workerOrigin
1027
- });
1028
- studioUrl = handles.studio.url;
1029
- } catch (error) {
1030
- logger.warn(`studio server failed to start (${error instanceof Error ? error.message : String(error)}) — continuing without it`);
1031
- }
1032
- }
1114
+ handles.studio = await startStudioBestEffort(options, plan, cwd, logger);
1115
+ const studioUrl = handles.studio?.url;
1033
1116
  if (plan.frameworkHint !== void 0) {
1034
1117
  logger.warn(plan.frameworkHint);
1035
1118
  }
1036
- const worker = (options.startWorker ?? defaultWorkerSpawner)(plan.wrangler, logger);
1119
+ ensureSidecarGenerated(plan, options, cwd, logger);
1120
+ const spawn = options.startWorker ?? defaultWorkerSpawner;
1121
+ const worker = spawn(plan.wrangler, logger);
1122
+ const sidecar = plan.sidecar === void 0 ? void 0 : spawn(plan.sidecar, logger);
1037
1123
  handles.containerLogs = afterWorkerSpawn(plan, cwd, logger, studioUrl);
1038
1124
  printAgentRulesHint(logger, cwd);
1039
- let sigintCount = 0;
1040
- let escalationTimer;
1041
- const onSigint = () => {
1042
- sigintCount += 1;
1043
- if (sigintCount === 1) {
1044
- logger.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)");
1045
- worker.kill("SIGTERM");
1046
- escalationTimer = setTimeout(() => {
1047
- worker.kill("SIGKILL");
1048
- }, SIGINT_GRACE_MS);
1049
- escalationTimer.unref();
1050
- } else {
1051
- worker.kill("SIGKILL");
1052
- }
1053
- };
1054
- const onSigterm = () => {
1055
- worker.kill("SIGTERM");
1056
- };
1057
- process.on("SIGINT", onSigint);
1058
- process.on("SIGTERM", onSigterm);
1059
- const code = await worker.exited;
1060
- if (escalationTimer) {
1061
- clearTimeout(escalationTimer);
1062
- }
1063
- process.off("SIGINT", onSigint);
1064
- process.off("SIGTERM", onSigterm);
1125
+ const code = await superviseWorkers(worker, sidecar, logger);
1065
1126
  return { code, plan };
1066
1127
  } finally {
1067
1128
  clearDevServerState(cwd, process.pid);
@@ -1825,13 +1825,16 @@ const PNPM_BUILT_DEPENDENCIES = [
1825
1825
  "unrs-resolver",
1826
1826
  "workerd"
1827
1827
  ];
1828
+ const PNPM_DENIED_BUILD_DEPENDENCIES = ["cpu-features", "protobufjs", "ssh2"];
1828
1829
  const PNPM_WORKSPACE_FILENAME = "pnpm-workspace.yaml";
1829
1830
  const pnpmWorkspaceYaml = () => [
1830
1831
  "# pnpm reads its settings from here (the package.json `pnpm` field is no longer read).",
1831
1832
  "# Pre-approve the toolchain's native build scripts so `pnpm install` runs them",
1832
- "# without the interactive `pnpm approve-builds` step.",
1833
+ "# without the interactive `pnpm approve-builds` step; deny the optional native",
1834
+ "# builds a scaffold doesn't need (so no C/C++ toolchain is required).",
1833
1835
  "allowBuilds:",
1834
1836
  ...PNPM_BUILT_DEPENDENCIES.map((name) => ` ${name}: true`),
1837
+ ...PNPM_DENIED_BUILD_DEPENDENCIES.map((name) => ` ${name}: false`),
1835
1838
  ""
1836
1839
  ].join("\n");
1837
1840
  const collectFiles = (directory) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/cli",
3
- "version": "1.0.0-alpha.70",
3
+ "version": "1.0.0-alpha.71",
4
4
  "description": "The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands",
5
5
  "keywords": [
6
6
  "agent-skills",