@lunora/cli 1.0.0-alpha.57 → 1.0.0-alpha.59

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 (48) hide show
  1. package/dist/bin.mjs +1 -1
  2. package/dist/index.d.mts +36 -10
  3. package/dist/index.d.ts +36 -10
  4. package/dist/index.mjs +7 -7
  5. package/dist/packem_chunks/handler.mjs +4 -4
  6. package/dist/packem_chunks/handler10.mjs +2 -2
  7. package/dist/packem_chunks/handler11.mjs +2 -2
  8. package/dist/packem_chunks/handler12.mjs +1 -1
  9. package/dist/packem_chunks/handler13.mjs +1 -1
  10. package/dist/packem_chunks/handler14.mjs +1 -1
  11. package/dist/packem_chunks/handler15.mjs +2 -2
  12. package/dist/packem_chunks/handler16.mjs +2 -2
  13. package/dist/packem_chunks/handler17.mjs +2 -2
  14. package/dist/packem_chunks/handler18.mjs +1 -1
  15. package/dist/packem_chunks/handler19.mjs +2 -2
  16. package/dist/packem_chunks/handler2.mjs +2 -2
  17. package/dist/packem_chunks/handler20.mjs +4 -4
  18. package/dist/packem_chunks/handler21.mjs +2 -2
  19. package/dist/packem_chunks/handler3.mjs +6 -3
  20. package/dist/packem_chunks/handler4.mjs +2 -2
  21. package/dist/packem_chunks/handler5.mjs +2 -2
  22. package/dist/packem_chunks/handler6.mjs +2 -2
  23. package/dist/packem_chunks/handler7.mjs +2 -2
  24. package/dist/packem_chunks/handler8.mjs +1 -1
  25. package/dist/packem_chunks/handler9.mjs +2 -2
  26. package/dist/packem_chunks/planDevCommand.mjs +498 -29
  27. package/dist/packem_chunks/runCodegenCommand.mjs +3 -3
  28. package/dist/packem_chunks/runDeployCommand.mjs +4 -4
  29. package/dist/packem_chunks/runInitCommand.mjs +5 -5
  30. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +5 -4
  31. package/dist/packem_chunks/runResetCommand.mjs +1 -1
  32. package/dist/packem_chunks/runRpcCommand.mjs +3 -2
  33. package/dist/packem_shared/{COMMANDS-B0ftFD_3.mjs → COMMANDS-0BD3K1pb.mjs} +43 -3
  34. package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-CRQmmBZM.mjs} +3 -2
  35. package/dist/packem_shared/{api-spec-CtA6ilu4.mjs → api-spec-Bx0iKbxA.mjs} +3 -1
  36. package/dist/packem_shared/{buildRegistryIndex-BcYe607_.mjs → buildRegistryIndex-CqM5FwGI.mjs} +1 -1
  37. package/dist/packem_shared/{command-D3lB_4Az.mjs → command-lYnl4QyF.mjs} +1 -1
  38. package/dist/packem_shared/{commands-BK6Pt3Ni.mjs → commands-vnRRkhNE.mjs} +12 -9
  39. package/dist/packem_shared/{createLogger-B40gPzQo.mjs → createLogger-CIWSHrTL.mjs} +33 -6
  40. package/dist/packem_shared/{createRecordingSpawner-DxI3mebw.mjs → createRecordingSpawner-Cw5Iu73G.mjs} +12 -2
  41. package/dist/packem_shared/{open-url-Dfq6fAyT.mjs → open-url-4PBLY9X0.mjs} +3 -2
  42. package/dist/packem_shared/{output-format-wUvAN6AL.mjs → output-format-B4642rjE.mjs} +1 -1
  43. package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-BBrXCg9V.mjs} +16 -9
  44. package/dist/packem_shared/runAddCommand-BCcJzs_k.mjs +4 -0
  45. package/dist/packem_shared/{storage-C8SAm5zu.mjs → storage-bGfgwOgF.mjs} +1 -1
  46. package/package.json +7 -6
  47. package/skills/lunora-quickstart/SKILL.md +25 -5
  48. package/dist/packem_shared/runAddCommand-DWjH0Heb.mjs +0 -4
@@ -1,14 +1,15 @@
1
1
  import { existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync, readFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { discoverSchema, discoverMigrations } from '@lunora/codegen';
4
+ import { LunoraError } from '@lunora/errors';
4
5
  import { join } from '@visulima/path';
5
6
  import { Project } from 'ts-morph';
6
7
  import { r as resolveAdminBaseUrl } from '../packem_shared/admin-url-4UzT-CI4.mjs';
7
- import { d as defineHandler } from '../packem_shared/command-D3lB_4Az.mjs';
8
+ import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
8
9
  import { diffSnapshots, renderMigrationFile } from '../packem_shared/diffSnapshots-BeDvvNiF.mjs';
9
10
  import { a as resolveProductionWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs';
10
11
  import schemaIrToSnapshot from '../packem_shared/schemaIrToSnapshot-DdsljJT-.mjs';
11
- import { runExportCommand, runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs';
12
+ import { runExportCommand, runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-CRQmmBZM.mjs';
12
13
 
13
14
  const SNAPSHOT_FILENAME = ".snapshot.json";
14
15
  const NON_ALPHANUMERIC = /[^\da-z]+/gu;
@@ -41,12 +42,12 @@ const loadSnapshot = (path) => {
41
42
  const raw = readFileSync(path, "utf8");
42
43
  const parsed = JSON.parse(raw);
43
44
  if (parsed.version !== 1) {
44
- throw new Error(`unsupported snapshot version: ${parsed.version}`);
45
+ throw new LunoraError("INTERNAL", `unsupported snapshot version: ${parsed.version}`);
45
46
  }
46
47
  return parsed;
47
48
  } catch (error) {
48
49
  const message = error instanceof Error ? error.message : String(error);
49
- throw new Error(`failed to read ${path}: ${message}`, { cause: error });
50
+ throw new LunoraError("INTERNAL", `failed to read ${path}: ${message}`, { cause: error });
50
51
  }
51
52
  };
52
53
  const runMigrateGenerateCommand = (options) => {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, rmSync } from 'node:fs';
2
2
  import { join } from '@visulima/path';
3
- import { d as defineHandler } from '../packem_shared/command-D3lB_4Az.mjs';
3
+ import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
4
4
  import { b as tuiConfirm } from '../packem_shared/tui-prompts-BjEN8XgP.mjs';
5
5
 
6
6
  const runResetCommand = async (options) => {
@@ -1,4 +1,5 @@
1
- import { d as defineHandler } from '../packem_shared/command-D3lB_4Az.mjs';
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
2
3
  import { r as resolveWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs';
3
4
 
4
5
  const parseArgsJson = (raw) => {
@@ -9,7 +10,7 @@ const parseArgsJson = (raw) => {
9
10
  return JSON.parse(raw);
10
11
  } catch (error) {
11
12
  const message = error instanceof Error ? error.message : String(error);
12
- throw new Error(`failed to parse --args as JSON: ${message}`, { cause: error });
13
+ throw new LunoraError("INTERNAL", `failed to parse --args as JSON: ${message}`, { cause: error });
13
14
  }
14
15
  };
15
16
  const TRAILING_SLASH = /\/$/u;
@@ -1,9 +1,11 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolveHint, isLunoraError, flattenHint, findSolutionByMessage } from '@lunora/errors';
2
3
  import { createCerebro } from '@visulima/cerebro';
3
4
  import completionCommand from '@visulima/cerebro/command/completion';
4
5
  import versionCommand from '@visulima/cerebro/command/version';
5
- import { A as API_SPEC_HELP } from './api-spec-CtA6ilu4.mjs';
6
- import { createLogger } from './createLogger-B40gPzQo.mjs';
6
+ import { A as API_SPEC_HELP } from './api-spec-Bx0iKbxA.mjs';
7
+ import { createLogger } from './createLogger-CIWSHrTL.mjs';
8
+ import { VisulimaError, renderError } from '@visulima/error';
7
9
  import { tmpdir } from 'node:os';
8
10
  import { join } from 'node:path';
9
11
 
@@ -213,9 +215,19 @@ const deploymentsCommand = {
213
215
  };
214
216
 
215
217
  const devCommand = {
218
+ argument: {
219
+ description: "Optional subcommand: stop (shut the running dev server down) | status (report it) | logs (print its captured output)",
220
+ name: "args",
221
+ type: String
222
+ },
216
223
  description: "Run the dev stack: wrangler worker + studio + codegen watch",
217
224
  examples: [
218
225
  ["lunora dev", "Run the worker + studio + codegen watch"],
226
+ ["lunora dev --background", "Run detached: blocks until ready, prints URL + PID, then returns"],
227
+ ["lunora dev stop", "Stop the background/tracked dev server (idempotent)"],
228
+ ["lunora dev status", "Report the running dev server (URL, PID, uptime)"],
229
+ ["lunora dev logs", "Print the captured dev-server log (background runs)"],
230
+ ["lunora dev --json", "Machine-readable JSON log lines (also LUNORA_LOG_JSON=1)"],
219
231
  ["lunora dev --no-studio", "Skip the embedded studio server"],
220
232
  ["lunora dev --worker-port 8080", "Use a custom wrangler dev port"],
221
233
  ["lunora dev --remote", "Proxy D1/KV/R2 to the deployed worker (also LUNORA_REMOTE=1)"]
@@ -225,10 +237,19 @@ const devCommand = {
225
237
  return { default: m.execute };
226
238
  }),
227
239
  name: "dev",
240
+ // KEEP IN SYNC with `daemonArguments` in `./lifecycle.ts`: a new flag that
241
+ // must reach a `--background` daemon has to be forwarded there explicitly.
228
242
  options: [
229
243
  { description: `Which API spec(s) codegen emits: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
230
244
  { description: "Studio server port (default 6173)", name: "port", type: Number },
231
245
  { description: "wrangler dev port (default 8787)", name: "worker-port", type: Number },
246
+ {
247
+ description: "Run the dev server as a managed background process (auto-enabled when an AI agent is detected; LUNORA_AGENT_MODE=0 disables)",
248
+ name: "background",
249
+ type: Boolean
250
+ },
251
+ { description: "Emit machine-readable JSON log lines (also LUNORA_LOG_JSON=1; auto-enabled for AI agents)", name: "json", type: Boolean },
252
+ { description: "How many trailing lines `lunora dev logs` prints (default 100, 0 = all)", name: "lines", type: Number },
232
253
  { description: "Don't start the embedded studio server", name: "no-studio", type: Boolean },
233
254
  { description: "Don't watch + regenerate codegen", name: "no-codegen", type: Boolean },
234
255
  { description: "Proxy D1/KV/R2 bindings to the deployed worker (or set LUNORA_REMOTE=1)", name: "remote", type: Boolean }
@@ -711,6 +732,21 @@ const viewCommand = {
711
732
  options: [{ description: "Open the deployed worker URL instead of localhost", name: "remote", type: Boolean }]
712
733
  };
713
734
 
735
+ const NO_STACK = { filterStacktrace: () => false, hideErrorCodeView: true };
736
+ const renderLunoraError = (error, options = {}) => {
737
+ const message = error instanceof Error ? error.message : String(error);
738
+ const hint = resolveHint(isLunoraError(error) ? { code: error.code, hint: error.hint, message } : message);
739
+ const rendered = new VisulimaError({
740
+ // `renderError` iterates `hint` as lines; the shared flattener returns one
741
+ // string, so split it back to lines for the terminal renderer.
742
+ hint: hint === void 0 ? void 0 : flattenHint(hint).split("\n"),
743
+ message: options.reason === void 0 ? message : `${options.reason}: ${message}`,
744
+ name: error instanceof Error && error.name.length > 0 ? error.name : "Error"
745
+ });
746
+ rendered.stack = "";
747
+ return renderError(rendered, NO_STACK);
748
+ };
749
+
714
750
  const editDistance = (a, b) => {
715
751
  const distances = Array.from({ length: b.length + 1 }, (_, index) => index);
716
752
  for (let row = 1; row <= a.length; row += 1) {
@@ -925,7 +961,11 @@ const reportRunError = (error) => {
925
961
  const message = error instanceof Error ? error.message : String(error);
926
962
  const unknown = UNKNOWN_COMMAND.exec(message);
927
963
  if (!unknown?.groups) {
928
- logger.error(message);
964
+ if (isLunoraError(error) || findSolutionByMessage(message) !== void 0) {
965
+ logger.error(renderLunoraError(error));
966
+ } else {
967
+ logger.error(message);
968
+ }
929
969
  return;
930
970
  }
931
971
  const name = unknown.groups.name ?? "";
@@ -1,5 +1,6 @@
1
1
  import { createWriteStream, createReadStream } from 'node:fs';
2
2
  import { unlink, stat } from 'node:fs/promises';
3
+ import { LunoraError } from '@lunora/errors';
3
4
  import { r as resolveAdminBaseUrl } from './admin-url-4UzT-CI4.mjs';
4
5
 
5
6
  const EXPORT_ENDPOINT_PATH = "/_lunora/admin/export";
@@ -183,7 +184,7 @@ const runImportCommand = async (options) => {
183
184
  });
184
185
  if (!response.ok) {
185
186
  const text = await response.text().catch(() => "<no body>");
186
- throw new Error(`import batch failed (HTTP ${String(response.status)}): ${text}`);
187
+ throw new LunoraError("INTERNAL", `import batch failed (HTTP ${String(response.status)}): ${text}`);
187
188
  }
188
189
  const json = await response.json();
189
190
  if (json.inserted) {
@@ -213,7 +214,7 @@ const runImportCommand = async (options) => {
213
214
  parsedDocument = JSON.parse(trimmed);
214
215
  } catch (error) {
215
216
  const message = error instanceof Error ? error.message : String(error);
216
- throw new Error(`invalid JSON on line ${String(lineNumber)}: ${message}`, { cause: error });
217
+ throw new LunoraError("INTERNAL", `invalid JSON on line ${String(lineNumber)}: ${message}`, { cause: error });
217
218
  }
218
219
  batch.push(JSON.stringify({ doc: parsedDocument, table: options.table }));
219
220
  };
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const API_SPEC_VALUES = ["both", "none", "openapi", "openrpc"];
2
4
  const API_SPEC_HELP = API_SPEC_VALUES.join(" | ");
3
5
  const parseApiSpec = (value) => {
@@ -7,7 +9,7 @@ const parseApiSpec = (value) => {
7
9
  if (API_SPEC_VALUES.includes(value)) {
8
10
  return value;
9
11
  }
10
- throw new Error(`invalid --api-spec "${value}" — expected one of: ${API_SPEC_HELP}`);
12
+ throw new LunoraError("INTERNAL", `invalid --api-spec "${value}" — expected one of: ${API_SPEC_HELP}`);
11
13
  };
12
14
 
13
15
  export { API_SPEC_HELP as A, parseApiSpec as p };
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join } from '@visulima/path';
3
- import parseManifest from './parseManifest--vZf2FY1.mjs';
3
+ import parseManifest from './parseManifest-BBrXCg9V.mjs';
4
4
 
5
5
  const CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/gu;
6
6
  const stripControlChars = (value) => value === void 0 ? void 0 : value.replaceAll(CONTROL_CHARS, "");
@@ -1,4 +1,4 @@
1
- import { createLogger } from './createLogger-B40gPzQo.mjs';
1
+ import { createLogger } from './createLogger-CIWSHrTL.mjs';
2
2
  import { P as PromptCancelledError, a as PROMPT_CANCEL_EXIT_CODE } from './prompt-cancelled-APzX1Im-.mjs';
3
3
 
4
4
  const defineHandler = (body) => async (toolbox) => {
@@ -3,13 +3,14 @@ import { dirname, join } from '@visulima/path';
3
3
  import { DEV_VARS_FILE, parseDevVariableEntries } from '@lunora/config';
4
4
  import { modify, applyEdits, parse } from 'jsonc-parser';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { LunoraError } from '@lunora/errors';
6
7
  import { b as tuiConfirm } from './tui-prompts-BjEN8XgP.mjs';
7
- import { collectCatalog, buildRegistryIndex } from './buildRegistryIndex-BcYe607_.mjs';
8
+ import { collectCatalog, buildRegistryIndex } from './buildRegistryIndex-CqM5FwGI.mjs';
8
9
  import { insertSchemaExtension } from './insertSchemaExtension-DAqbfr9Z.mjs';
9
10
  import { createHash } from 'node:crypto';
10
11
  import { tmpdir } from 'node:os';
11
12
  import { downloadTemplate } from 'giget';
12
- import parseManifest from './parseManifest--vZf2FY1.mjs';
13
+ import parseManifest from './parseManifest-BBrXCg9V.mjs';
13
14
 
14
15
  const DEFAULT_SOURCE_REF_FALLBACK = "alpha";
15
16
  const STABLE_BRANCH = "main";
@@ -54,7 +55,7 @@ const resolveVersionRef = (version) => {
54
55
  const resolveSourceRef = (ref) => {
55
56
  if (ref !== void 0 && ref.length > 0) {
56
57
  if (!isSafeRef(ref)) {
57
- throw new Error(`invalid --ref "${ref}" — a ref may contain letters, digits, ".", "_", "-", "/", "@" and must not contain "..".`);
58
+ throw new LunoraError("INTERNAL", `invalid --ref "${ref}" — a ref may contain letters, digits, ".", "_", "-", "/", "@" and must not contain "..".`);
58
59
  }
59
60
  return ref;
60
61
  }
@@ -457,11 +458,12 @@ export const schema = defineSchema({});
457
458
  return { kind: "skipped", path: schemaPath };
458
459
  }
459
460
  if (result.reason === "invalid-identifier") {
460
- throw new Error(
461
+ throw new LunoraError(
462
+ "INTERNAL",
461
463
  `schema-extension item "${itemKey}" is not a valid JS identifier — it is spliced into lunora/schema.ts as \`import { ${itemKey} }\` / \`.extend(${itemKey}.extension)\`. Rename the item to a valid identifier (no leading digit, no "-").`
462
464
  );
463
465
  }
464
- throw new Error(`schema-extension merge failed for "${itemKey}": ${result.reason}`);
466
+ throw new LunoraError("INTERNAL", `schema-extension merge failed for "${itemKey}": ${result.reason}`);
465
467
  };
466
468
  const previewWholeFile = (file, current, incoming, exists, logger) => {
467
469
  const lines = renderDiff(current, incoming);
@@ -549,7 +551,8 @@ const DEFAULT_SOURCE_BASE = "gh:anolilab/lunora/registry";
549
551
  const VALID_ITEM_NAME = /^[A-Za-z0-9][\w-]*$/u;
550
552
  const assertSafeItemName = (name) => {
551
553
  if (!VALID_ITEM_NAME.test(name)) {
552
- throw new Error(
554
+ throw new LunoraError(
555
+ "INTERNAL",
553
556
  `invalid registry item name "${name}" — names must match ${VALID_ITEM_NAME.source} (letters, digits, "-", "_"; no path separators or "..")`
554
557
  );
555
558
  }
@@ -601,7 +604,7 @@ const resolveItemDirectory = async (name, options) => {
601
604
  if (options.from !== void 0) {
602
605
  const directory = join(options.from, name);
603
606
  if (!existsSync(directory)) {
604
- throw new Error(`registry item not found in local source: ${directory}`);
607
+ throw new LunoraError("INTERNAL", `registry item not found in local source: ${directory}`);
605
608
  }
606
609
  return { cleanup: () => {
607
610
  }, directory };
@@ -612,7 +615,7 @@ const resolveItemDirectory = async (name, options) => {
612
615
  const resolveRegistryRoot = async (options) => {
613
616
  if (options.from !== void 0) {
614
617
  if (!existsSync(options.from)) {
615
- throw new Error(`registry root not found: ${options.from}`);
618
+ throw new LunoraError("INTERNAL", `registry root not found: ${options.from}`);
616
619
  }
617
620
  return { cleanup: () => {
618
621
  }, root: options.from };
@@ -635,7 +638,7 @@ const resolvePlan = async (names, options) => {
635
638
  return;
636
639
  }
637
640
  if (inProgress.has(name)) {
638
- throw new Error(`cyclic registry dependency detected at "${name}"`);
641
+ throw new LunoraError("INTERNAL", `cyclic registry dependency detected at "${name}"`);
639
642
  }
640
643
  inProgress.add(name);
641
644
  const { cleanup, directory } = await resolveItemDirectory(name, options);
@@ -2,19 +2,46 @@ import { STEP_BADGE_NAMES, LunoraReporter } from '@lunora/config';
2
2
  import { JsonReporter } from '@visulima/pail/reporter/json';
3
3
  import { createPail } from '@visulima/pail/server';
4
4
 
5
+ let sharedPail;
6
+ let jsonForced = false;
7
+ let configuredReporters;
5
8
  const wantJson = () => {
9
+ if (jsonForced) {
10
+ return true;
11
+ }
6
12
  const flag = process.env.LUNORA_LOG_JSON;
7
13
  return flag === "1" || flag === "true";
8
14
  };
9
- const buildReporter = () => {
10
- const Reporter = wantJson() ? JsonReporter : LunoraReporter;
11
- return new Reporter();
15
+ const constructReporter = (Reporter) => new Reporter();
16
+ const buildReporters = () => {
17
+ if (jsonForced) {
18
+ return [constructReporter(JsonReporter)];
19
+ }
20
+ if (configuredReporters !== void 0 && configuredReporters.length > 0) {
21
+ return configuredReporters;
22
+ }
23
+ return [constructReporter(wantJson() ? JsonReporter : LunoraReporter)];
24
+ };
25
+ const forceJsonLogging = () => {
26
+ if (jsonForced) {
27
+ return;
28
+ }
29
+ jsonForced = true;
30
+ sharedPail = void 0;
31
+ };
32
+ const logHandlers = {
33
+ compose: (...reporters) => reporters,
34
+ console: () => constructReporter(LunoraReporter),
35
+ json: () => constructReporter(JsonReporter)
36
+ };
37
+ const configureLogHandlers = (reporters) => {
38
+ configuredReporters = Array.isArray(reporters) ? reporters : [reporters];
39
+ sharedPail = void 0;
12
40
  };
13
41
  const STEP_LOG_TYPES = Object.fromEntries(STEP_BADGE_NAMES.map((name) => [name, { label: name, logLevel: "informational" }]));
14
- let sharedPail;
15
42
  const getPail = () => {
16
43
  sharedPail ??= createPail({
17
- reporters: [buildReporter()],
44
+ reporters: buildReporters(),
18
45
  scope: ["lunora"],
19
46
  stderr: process.stderr,
20
47
  stdout: process.stdout,
@@ -75,4 +102,4 @@ const logStep = (type, message) => {
75
102
  getPail()[type](message);
76
103
  };
77
104
 
78
- export { createLogger, createStderrLogger, getPail, logStep, pail };
105
+ export { configureLogHandlers, createLogger, createStderrLogger, forceJsonLogging, getPail, logHandlers, logStep, pail };
@@ -1,5 +1,13 @@
1
1
  import { spawn } from 'node:child_process';
2
2
 
3
+ const NEEDS_CMD_QUOTING = /\s/;
4
+ const spawnShellCompat = (command, args, platform = process.platform) => {
5
+ if (platform !== "win32" || command === process.execPath) {
6
+ return { args: [...args], command, shell: false };
7
+ }
8
+ const quote = (value) => NEEDS_CMD_QUOTING.test(value) ? `"${value}"` : value;
9
+ return { args: args.map((argument) => quote(argument)), command: quote(command), shell: true };
10
+ };
3
11
  const defaultSpawner = (descriptor) => new Promise((resolve, reject) => {
4
12
  const hasInput = typeof descriptor.input === "string";
5
13
  const wantCapture = descriptor.captureStdout === true;
@@ -9,9 +17,11 @@ const defaultSpawner = (descriptor) => new Promise((resolve, reject) => {
9
17
  } else if (descriptor.stdoutToStderr) {
10
18
  stdout = 2;
11
19
  }
12
- const child = spawn(descriptor.command, [...descriptor.args], {
20
+ const exec = spawnShellCompat(descriptor.command, descriptor.args);
21
+ const child = spawn(exec.command, exec.args, {
13
22
  cwd: descriptor.cwd ?? process.cwd(),
14
23
  env: descriptor.env ? { ...process.env, ...descriptor.env } : process.env,
24
+ shell: exec.shell,
15
25
  stdio: [hasInput ? "pipe" : "inherit", stdout, "inherit"]
16
26
  });
17
27
  let captured = "";
@@ -40,4 +50,4 @@ const createRecordingSpawner = (exitCode = 0) => {
40
50
  return { calls, spawner };
41
51
  };
42
52
 
43
- export { createRecordingSpawner, defaultSpawner };
53
+ export { createRecordingSpawner, defaultSpawner, spawnShellCompat };
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { platform } from 'node:os';
3
+ import { LunoraError } from '@lunora/errors';
3
4
 
4
5
  const platformCommand = () => {
5
6
  const os = platform();
@@ -29,10 +30,10 @@ const openUrl = async (url, options = {}) => {
29
30
  try {
30
31
  parsed = new URL(url);
31
32
  } catch {
32
- throw new Error(`Invalid URL: ${url}`);
33
+ throw new LunoraError("INTERNAL", `Invalid URL: ${url}`);
33
34
  }
34
35
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
35
- throw new Error(`Refusing to open non-http(s) URL: ${url}`);
36
+ throw new LunoraError("INTERNAL", `Refusing to open non-http(s) URL: ${url}`);
36
37
  }
37
38
  const opener = options.opener ?? platformOpener;
38
39
  await opener(url);
@@ -1,4 +1,4 @@
1
- import { createStderrLogger } from './createLogger-B40gPzQo.mjs';
1
+ import { createStderrLogger } from './createLogger-CIWSHrTL.mjs';
2
2
 
3
3
  const OUTPUT_FORMATS = /* @__PURE__ */ new Set(["json", "pretty"]);
4
4
  const validateOutputFormat = (command, format) => {
@@ -1,17 +1,20 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const NEWLINE_PRESENT = /[\r\n]/u;
2
4
  const VALID_ENV_NAME = /^[A-Za-z_]\w*$/u;
3
5
  const VALID_ITEM_NAME = /^[A-Za-z0-9][\w-]*$/u;
4
6
  const parseManifest = (raw, itemName) => {
5
7
  if (typeof raw !== "object" || raw === null) {
6
- throw new Error(`registry.json for "${itemName}" is not an object`);
8
+ throw new LunoraError("INTERNAL", `registry.json for "${itemName}" is not an object`);
7
9
  }
8
10
  const record = raw;
9
11
  const { name } = record;
10
12
  if (typeof name !== "string" || name.length === 0) {
11
- throw new Error(`registry.json for "${itemName}" is missing a string "name"`);
13
+ throw new LunoraError("INTERNAL", `registry.json for "${itemName}" is missing a string "name"`);
12
14
  }
13
15
  if (!VALID_ITEM_NAME.test(name)) {
14
- throw new Error(
16
+ throw new LunoraError(
17
+ "INTERNAL",
15
18
  `registry.json for "${itemName}": name "${name}" must match ${VALID_ITEM_NAME.source} (letters, digits, "-", "_"; no path separators, "..", or code)`
16
19
  );
17
20
  }
@@ -21,7 +24,7 @@ const parseManifest = (raw, itemName) => {
21
24
  }
22
25
  const files = filesRaw.map((entry, index) => {
23
26
  if (typeof entry !== "object" || entry === null) {
24
- throw new Error(`registry.json "${itemName}": files[${String(index)}] is not an object`);
27
+ throw new LunoraError("INTERNAL", `registry.json "${itemName}": files[${String(index)}] is not an object`);
25
28
  }
26
29
  const fileRecord = entry;
27
30
  const { from } = fileRecord;
@@ -31,14 +34,17 @@ const parseManifest = (raw, itemName) => {
31
34
  throw new TypeError(`registry.json "${itemName}": files[${String(index)}] needs string "from" and "to"`);
32
35
  }
33
36
  if (merge !== "create-or-skip" && merge !== "schema-extension") {
34
- throw new Error(`registry.json "${itemName}": files[${String(index)}].merge must be "create-or-skip" or "schema-extension"`);
37
+ throw new LunoraError("INTERNAL", `registry.json "${itemName}": files[${String(index)}].merge must be "create-or-skip" or "schema-extension"`);
35
38
  }
36
39
  for (const [field, value] of [
37
40
  ["from", from],
38
41
  ["to", to]
39
42
  ]) {
40
43
  if (value.includes("..") || value.startsWith("/")) {
41
- throw new Error(`registry.json "${itemName}": files[${String(index)}].${field} "${value}" must be a relative path without ".."`);
44
+ throw new LunoraError(
45
+ "INTERNAL",
46
+ `registry.json "${itemName}": files[${String(index)}].${field} "${value}" must be a relative path without ".."`
47
+ );
42
48
  }
43
49
  }
44
50
  return { from, merge, to };
@@ -59,15 +65,16 @@ const parseManifest = (raw, itemName) => {
59
65
  ).map((entry) => {
60
66
  const hasValue = typeof entry.value === "string";
61
67
  if (!VALID_ENV_NAME.test(entry.name)) {
62
- throw new Error(
68
+ throw new LunoraError(
69
+ "INTERNAL",
63
70
  `registry.json "${itemName}": envVars["${entry.name}"].name must match ${VALID_ENV_NAME.source} (letters, digits, underscore; no "=" or newline)`
64
71
  );
65
72
  }
66
73
  if (hasValue && NEWLINE_PRESENT.test(entry.value)) {
67
- throw new Error(`registry.json "${itemName}": envVars["${entry.name}"].value must not contain a newline`);
74
+ throw new LunoraError("INTERNAL", `registry.json "${itemName}": envVars["${entry.name}"].value must not contain a newline`);
68
75
  }
69
76
  if (typeof entry.description === "string" && NEWLINE_PRESENT.test(entry.description)) {
70
- throw new Error(`registry.json "${itemName}": envVars["${entry.name}"].description must not contain a newline`);
77
+ throw new LunoraError("INTERNAL", `registry.json "${itemName}": envVars["${entry.name}"].description must not contain a newline`);
71
78
  }
72
79
  return {
73
80
  ...typeof entry.description === "string" ? { description: entry.description } : {},
@@ -0,0 +1,4 @@
1
+ import 'node:fs';
2
+ import '@visulima/path';
3
+ export { r as runAddCommand, a as runBuildIndexCommand, g as runListCommand, b as runRegistryViewCommand } from './commands-vnRRkhNE.mjs';
4
+ import './buildRegistryIndex-CqM5FwGI.mjs';
@@ -1,4 +1,4 @@
1
- import { s as setBindingField } from './commands-BK6Pt3Ni.mjs';
1
+ import { s as setBindingField } from './commands-vnRRkhNE.mjs';
2
2
 
3
3
  const INVALID_SLUG_CHARS = /[^a-z0-9]+/u;
4
4
  const toKebabSlug = (input, min, max) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/cli",
3
- "version": "1.0.0-alpha.57",
3
+ "version": "1.0.0-alpha.59",
4
4
  "description": "The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -52,11 +52,12 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@bomb.sh/tab": "0.0.17",
55
- "@lunora/codegen": "1.0.0-alpha.28",
56
- "@lunora/config": "1.0.0-alpha.45",
57
- "@lunora/container": "1.0.0-alpha.5",
58
- "@lunora/d1": "1.0.0-alpha.21",
59
- "@lunora/seed": "1.0.0-alpha.13",
55
+ "@lunora/codegen": "1.0.0-alpha.29",
56
+ "@lunora/config": "1.0.0-alpha.47",
57
+ "@lunora/container": "1.0.0-alpha.6",
58
+ "@lunora/d1": "1.0.0-alpha.22",
59
+ "@lunora/errors": "1.0.0-alpha.1",
60
+ "@lunora/seed": "1.0.0-alpha.14",
60
61
  "@visulima/cerebro": "3.0.0-alpha.32",
61
62
  "@visulima/error": "6.0.0-alpha.34",
62
63
  "@visulima/fs": "5.0.0-alpha.32",
@@ -31,9 +31,12 @@ Set up a working Lunora project as fast as possible.
31
31
  Lunora into the current project.
32
32
  4. Run `lunora codegen` to generate `lunora/_generated/` and typecheck the
33
33
  schema + functions. This is the agent's feedback loop.
34
- 5. Start the dev loop with `lunora dev` (ask the user to run it locally, or
35
- start it in the background for cloud/headless agents it is long-running and
36
- does not exit).
34
+ 5. Start the dev loop. As an agent, run `lunora dev --background` it starts
35
+ the server as a managed detached process, blocks until it accepts requests,
36
+ prints the URL + PID, and returns (under a detected AI agent, plain
37
+ `lunora dev` does this automatically, with JSON logs). Never leave a bare
38
+ `lunora dev` running in your own shell — it is long-running and does not
39
+ exit.
37
40
  6. Verify a query/mutation round-trip works end to end.
38
41
 
39
42
  ## Path 1: New Project (Recommended)
@@ -87,7 +90,23 @@ not exit, so:
87
90
 
88
91
  - **Local development (user at the keyboard):** ask the user to run `lunora dev`
89
92
  in a terminal.
90
- - **Cloud or headless agents:** start `lunora dev` in the background.
93
+ - **Agents:** run `lunora dev --background`. It detaches the server, waits until
94
+ it answers HTTP, prints `Dev server running at <url> (pid <n>)`, and exits —
95
+ no orphaned shell, no PID bookkeeping. When Lunora detects an AI agent
96
+ (Claude Code, Cursor, Codex, …), plain `lunora dev` flips into this mode
97
+ automatically with JSON logs; `LUNORA_AGENT_MODE=0` opts out.
98
+
99
+ Manage the running server afterwards:
100
+
101
+ ```bash
102
+ lunora dev status --json # machine-readable: url, pid, uptime, logFile
103
+ lunora dev logs --lines 50 # tail the captured output (.lunora/dev.log)
104
+ lunora dev stop # idempotent — succeeds even if nothing runs
105
+ ```
106
+
107
+ A second `lunora dev` never double-starts: it reports the existing instance
108
+ (`.lunora/dev.json` is the lockfile). Probe readiness or liveness at
109
+ `GET /_lunora/status` (`{"ok":true}`).
91
110
 
92
111
  Vite serves on `http://localhost:5173` by default; the Worker is served on the
93
112
  same origin via `@cloudflare/vite-plugin`.
@@ -236,5 +255,6 @@ ids, `.dev.vars` secrets, and container exports.
236
255
  - [ ] New project: scaffolded with `lunora init --template <t>`.
237
256
  - [ ] Existing app: ran `lunora init --here` and wired `LunoraProvider`.
238
257
  - [ ] Ran `lunora codegen`: `lunora/_generated/` exists and typecheck is clean.
239
- - [ ] `lunora dev` is running — user terminal, or background for cloud agents.
258
+ - [ ] Dev server is running — user terminal, or `lunora dev --background`
259
+ (check with `lunora dev status`).
240
260
  - [ ] Verified a query/mutation round-trip re-renders the client live.
@@ -1,4 +0,0 @@
1
- import 'node:fs';
2
- import '@visulima/path';
3
- export { r as runAddCommand, a as runBuildIndexCommand, g as runListCommand, b as runRegistryViewCommand } from './commands-BK6Pt3Ni.mjs';
4
- import './buildRegistryIndex-BcYe607_.mjs';