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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE.md +6 -0
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/bin.mjs +1 -1
  4. package/dist/index.d.mts +358 -300
  5. package/dist/index.d.ts +358 -300
  6. package/dist/index.mjs +8 -9
  7. package/dist/packem_chunks/handler.mjs +15 -5
  8. package/dist/packem_chunks/handler10.mjs +8 -14
  9. package/dist/packem_chunks/handler11.mjs +20 -189
  10. package/dist/packem_chunks/handler12.mjs +176 -115
  11. package/dist/packem_chunks/handler13.mjs +118 -52
  12. package/dist/packem_chunks/handler14.mjs +50 -43
  13. package/dist/packem_chunks/handler15.mjs +48 -67
  14. package/dist/packem_chunks/handler16.mjs +94 -37
  15. package/dist/packem_chunks/handler17.mjs +39 -100
  16. package/dist/packem_chunks/handler18.mjs +87 -152
  17. package/dist/packem_chunks/handler19.mjs +148 -67
  18. package/dist/packem_chunks/handler2.mjs +6 -4
  19. package/dist/packem_chunks/handler20.mjs +77 -75
  20. package/dist/packem_chunks/handler21.mjs +71 -288
  21. package/dist/packem_chunks/handler3.mjs +8 -4
  22. package/dist/packem_chunks/handler4.mjs +2 -2
  23. package/dist/packem_chunks/handler5.mjs +7 -4
  24. package/dist/packem_chunks/handler6.mjs +10 -7
  25. package/dist/packem_chunks/handler7.mjs +2 -2
  26. package/dist/packem_chunks/handler8.mjs +1 -1
  27. package/dist/packem_chunks/handler9.mjs +330 -12
  28. package/dist/packem_chunks/planDevCommand.mjs +762 -81
  29. package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
  30. package/dist/packem_chunks/runDeployCommand.mjs +137 -22
  31. package/dist/packem_chunks/runInitCommand.mjs +1648 -188
  32. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +94 -47
  33. package/dist/packem_chunks/runResetCommand.mjs +2 -2
  34. package/dist/packem_chunks/runRpcCommand.mjs +3 -2
  35. package/dist/packem_shared/{COMMANDS-Dh0bSERi.mjs → COMMANDS-DZ0NnuqI.mjs} +103 -27
  36. package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-D0VOTerB.mjs} +7 -2
  37. package/dist/packem_shared/{api-spec-CtA6ilu4.mjs → api-spec-Bx0iKbxA.mjs} +3 -1
  38. package/dist/packem_shared/{buildRegistryIndex-BcYe607_.mjs → buildRegistryIndex-BS5ig822.mjs} +1 -1
  39. package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +31 -0
  40. package/dist/packem_shared/{command-BDXcJCCJ.mjs → command-lYnl4QyF.mjs} +6 -1
  41. package/dist/packem_shared/{commands-CkkATMMx.mjs → commands-D5Yxt9VY.mjs} +199 -20
  42. package/dist/packem_shared/{createLogger-CHPNjFw2.mjs → createLogger-CIWSHrTL.mjs} +40 -8
  43. package/dist/packem_shared/{createRecordingSpawner-DxI3mebw.mjs → createRecordingSpawner-WuSn20kb.mjs} +23 -2
  44. package/dist/packem_shared/detect-package-manager-v4hHpQd0.mjs +62 -0
  45. package/dist/packem_shared/{insertSchemaExtension-BuzF6-t2.mjs → insertSchemaExtension-DAqbfr9Z.mjs} +15 -10
  46. package/dist/packem_shared/{open-url-Dfq6fAyT.mjs → open-url-4PBLY9X0.mjs} +3 -2
  47. package/dist/packem_shared/{output-format-7gyGR3h8.mjs → output-format-B4642rjE.mjs} +1 -1
  48. package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-Dbp-Q2q3.mjs} +36 -9
  49. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
  50. package/dist/packem_shared/runAddCommand-D1hgfqFQ.mjs +4 -0
  51. package/dist/packem_shared/{storage-Bjo35hPa.mjs → storage-BXU4ax4O.mjs} +1 -1
  52. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +658 -0
  53. package/dist/packem_shared/wrangler-secrets-Coni-mER.mjs +49 -0
  54. package/package.json +17 -14
  55. package/skills/lunora-quickstart/SKILL.md +26 -5
  56. package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
  57. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
  58. package/dist/packem_shared/runAddCommand-DepS9W9W.mjs +0 -4
  59. package/dist/packem_shared/tui-prompts-CA9lngSS.mjs +0 -269
@@ -1,14 +1,16 @@
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 { isInteractive, promptSelect, promptText } from '@lunora/config';
5
+ import { LunoraError } from '@lunora/errors';
4
6
  import { join } from '@visulima/path';
5
7
  import { Project } from 'ts-morph';
6
8
  import { r as resolveAdminBaseUrl } from '../packem_shared/admin-url-4UzT-CI4.mjs';
7
- import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
9
+ import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
8
10
  import { diffSnapshots, renderMigrationFile } from '../packem_shared/diffSnapshots-BeDvvNiF.mjs';
9
11
  import { a as resolveProductionWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs';
10
12
  import schemaIrToSnapshot from '../packem_shared/schemaIrToSnapshot-DdsljJT-.mjs';
11
- import { runExportCommand, runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs';
13
+ import { runExportCommand, runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-D0VOTerB.mjs';
12
14
 
13
15
  const SNAPSHOT_FILENAME = ".snapshot.json";
14
16
  const NON_ALPHANUMERIC = /[^\da-z]+/gu;
@@ -41,12 +43,12 @@ const loadSnapshot = (path) => {
41
43
  const raw = readFileSync(path, "utf8");
42
44
  const parsed = JSON.parse(raw);
43
45
  if (parsed.version !== 1) {
44
- throw new Error(`unsupported snapshot version: ${parsed.version}`);
46
+ throw new LunoraError("INTERNAL", `unsupported snapshot version: ${parsed.version}`);
45
47
  }
46
48
  return parsed;
47
49
  } catch (error) {
48
50
  const message = error instanceof Error ? error.message : String(error);
49
- throw new Error(`failed to read ${path}: ${message}`, { cause: error });
51
+ throw new LunoraError("INTERNAL", `failed to read ${path}: ${message}`, { cause: error });
50
52
  }
51
53
  };
52
54
  const runMigrateGenerateCommand = (options) => {
@@ -151,7 +153,47 @@ const camelCase = (slug) => slug.split("-").filter((part) => part.length > 0).ma
151
153
  }
152
154
  return part.charAt(0).toUpperCase() + part.slice(1);
153
155
  }).join("");
154
- const runMigrateCreateCommand = (options) => {
156
+ const discoverKnownTables = (cwd) => {
157
+ const schemaPath = join(cwd, "lunora", "schema.ts");
158
+ if (!existsSync(schemaPath)) {
159
+ return [];
160
+ }
161
+ try {
162
+ const project = new Project({ skipAddingFilesFromTsConfig: true });
163
+ return discoverSchema(project, schemaPath).tables.map((table) => table.name);
164
+ } catch {
165
+ return [];
166
+ }
167
+ };
168
+ const promptForTable = async (tables) => {
169
+ if (tables.length > 0) {
170
+ return promptSelect(
171
+ "Which table does this migration iterate?",
172
+ tables.map((name) => {
173
+ return { label: name, value: name };
174
+ })
175
+ );
176
+ }
177
+ return promptText("Target table for the migration: ");
178
+ };
179
+ const resolveCreateTable = async (cwd, options) => {
180
+ if (options.table !== void 0) {
181
+ return options.table;
182
+ }
183
+ const prompt = options.promptTable ?? (isInteractive() ? promptForTable : void 0);
184
+ if (prompt === void 0) {
185
+ options.logger.error("migrate create requires a target table when not running interactively — re-run with --table <table>");
186
+ return void 0;
187
+ }
188
+ const answer = await prompt(discoverKnownTables(cwd));
189
+ const table = answer?.trim();
190
+ if (table === void 0 || table === "") {
191
+ options.logger.error("no table selected — re-run with --table <table>");
192
+ return void 0;
193
+ }
194
+ return table;
195
+ };
196
+ const runMigrateCreateCommand = async (options) => {
155
197
  const cwd = options.cwd ?? process.cwd();
156
198
  const slug = kebabCase(options.name);
157
199
  if (slug === "") {
@@ -165,9 +207,12 @@ const runMigrateCreateCommand = (options) => {
165
207
  );
166
208
  return { code: 1, file: "" };
167
209
  }
168
- const table = options.table ?? "TODO_table";
210
+ const table = await resolveCreateTable(cwd, options);
211
+ if (table === void 0) {
212
+ return { code: 1, file: "" };
213
+ }
169
214
  if (!IDENTIFIER_PATTERN.test(table)) {
170
- options.logger.error(`invalid --table: "${table}" — must be a valid identifier ([A-Za-z_][A-Za-z0-9_]*)`);
215
+ options.logger.error(`invalid table: "${table}" — must be a valid identifier ([A-Za-z_][A-Za-z0-9_]*)`);
171
216
  return { code: 1, file: "" };
172
217
  }
173
218
  const lunoraDirectory = join(cwd, "lunora");
@@ -195,9 +240,6 @@ ${content}`;
195
240
  ${block}
196
241
  `, "utf8");
197
242
  options.logger.success(`scaffolded migration "${slug}" in ${file}`);
198
- if (options.table === void 0) {
199
- options.logger.warn(`set the \`table\` field on "${slug}" — it defaults to "${table}"`);
200
- }
201
243
  return { code: 0, file };
202
244
  };
203
245
  const resolveMigrationTable = (cwd, id) => {
@@ -305,44 +347,49 @@ const runMigrateToHyperdriveCommand = async (options) => {
305
347
  }
306
348
  const temporaryDirectory = options.out === void 0 ? mkdtempSync(join(tmpdir(), "lunora-d1ps-")) : void 0;
307
349
  const dumpPath = options.out ?? join(temporaryDirectory, "dump.ndjson");
308
- logger.info(`Exporting .global() data from the D1 source (${fromUrl ?? "http://localhost:8787"}) …`);
309
- const exportResult = await runExportCommand({
310
- fetchImpl: options.fetchImpl,
311
- logger,
312
- out: dumpPath,
313
- prod: options.prod,
314
- tables: options.tables,
315
- token: options.fromToken,
316
- url: fromUrl
317
- });
318
- if (exportResult.code !== 0) {
319
- return { code: exportResult.code };
320
- }
321
- logger.info(`Exported ${String(exportResult.rows)} row(s) (${String(exportResult.bytes)} bytes).`);
322
- logger.info(`Importing into the Hyperdrive target (${toUrl ?? "http://localhost:8787"}) …`);
323
- const importResult = await runImportCommand({
324
- batchSize: options.batchSize,
325
- fetchImpl: options.fetchImpl,
326
- file: dumpPath,
327
- logger,
328
- prod: options.prod,
329
- token: options.toToken,
330
- url: toUrl
331
- });
332
- if (temporaryDirectory !== void 0) {
333
- rmSync(temporaryDirectory, { force: true, recursive: true });
334
- }
335
- if (importResult.code !== 0) {
336
- return { code: importResult.code };
337
- }
338
- if (importResult.inserted === exportResult.rows) {
339
- logger.info(`✓ Migrated ${String(exportResult.rows)} row(s) — counts match. Verify your app reads from Hyperdrive, then decommission the D1 binding.`);
340
- } else {
341
- logger.warn(
342
- `Imported ${String(importResult.inserted)} of ${String(exportResult.rows)} exported row(s) — the remainder likely already existed in the target (see conflicts above). Re-run after resolving, or inspect the dump with --out.`
343
- );
350
+ try {
351
+ logger.info(`Exporting .global() data from the D1 source (${fromUrl ?? "http://localhost:8787"}) …`);
352
+ const exportResult = await runExportCommand({
353
+ fetchImpl: options.fetchImpl,
354
+ logger,
355
+ out: dumpPath,
356
+ prod: options.prod,
357
+ tables: options.tables,
358
+ token: options.fromToken,
359
+ url: fromUrl
360
+ });
361
+ if (exportResult.code !== 0) {
362
+ return { code: exportResult.code };
363
+ }
364
+ logger.info(`Exported ${String(exportResult.rows)} row(s) (${String(exportResult.bytes)} bytes).`);
365
+ logger.info(`Importing into the Hyperdrive target (${toUrl ?? "http://localhost:8787"}) …`);
366
+ const importResult = await runImportCommand({
367
+ batchSize: options.batchSize,
368
+ fetchImpl: options.fetchImpl,
369
+ file: dumpPath,
370
+ logger,
371
+ prod: options.prod,
372
+ token: options.toToken,
373
+ url: toUrl
374
+ });
375
+ if (importResult.code !== 0) {
376
+ return { code: importResult.code };
377
+ }
378
+ if (importResult.inserted === exportResult.rows) {
379
+ logger.info(
380
+ `✓ Migrated ${String(exportResult.rows)} row(s) — counts match. Verify your app reads from Hyperdrive, then decommission the D1 binding.`
381
+ );
382
+ } else {
383
+ logger.warn(
384
+ `Imported ${String(importResult.inserted)} of ${String(exportResult.rows)} exported row(s) — the remainder likely already existed in the target (see conflicts above). Re-run after resolving, or inspect the dump with --out.`
385
+ );
386
+ }
387
+ return { code: 0 };
388
+ } finally {
389
+ if (temporaryDirectory !== void 0) {
390
+ rmSync(temporaryDirectory, { force: true, recursive: true });
391
+ }
344
392
  }
345
- return { code: 0 };
346
393
  };
347
394
  const execute = defineHandler(({ argument, cwd, logger, options }) => {
348
395
  const sub = argument[0];
@@ -1,7 +1,7 @@
1
1
  import { existsSync, rmSync } from 'node:fs';
2
2
  import { join } from '@visulima/path';
3
- import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
4
- import { b as tuiConfirm } from '../packem_shared/tui-prompts-CA9lngSS.mjs';
3
+ import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
4
+ import { b as tuiConfirm } from '../packem_shared/tui-prompts-BjEN8XgP.mjs';
5
5
 
6
6
  const runResetCommand = async (options) => {
7
7
  const cwd = options.cwd ?? process.cwd();
@@ -1,4 +1,5 @@
1
- import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.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,15 +1,21 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, readFileSync, lstatSync, 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-CHPNjFw2.mjs';
7
- import { tmpdir } from 'node:os';
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';
9
+ import { homedir } from 'node:os';
8
10
  import { join } from 'node:path';
9
11
 
10
12
  const addCommand = {
11
- argument: { description: "Feature or registry item: auth | email | storage | crons | presence | ratelimit | backup | …", name: "feature", type: String },
12
- description: "Add a feature or registry item (auth, email, storage, crons, …) to the current Lunora project",
13
+ argument: {
14
+ description: "Feature or registry item: ai | auth | email | storage | crons | presence | queue | workflow | flags | backup | …",
15
+ name: "feature",
16
+ type: String
17
+ },
18
+ description: "Add a feature or registry item (ai, auth, email, storage, crons, …) to the current Lunora project",
13
19
  examples: [
14
20
  ["lunora add auth", "Add authentication (asks which provider)"],
15
21
  ["lunora add auth --provider clerk", "Add Clerk auth without prompting"],
@@ -37,7 +43,8 @@ const addCommand = {
37
43
  name: "ref",
38
44
  type: String
39
45
  },
40
- { description: "Permit --source values outside gh:/github:/https://", name: "allow-unsafe-source", type: Boolean }
46
+ { description: "Permit --source values outside gh:/github:/https://", name: "allow-unsafe-source", type: Boolean },
47
+ { description: "Output format: pretty (default) or json", name: "format", type: String }
41
48
  ]
42
49
  };
43
50
 
@@ -212,9 +219,19 @@ const deploymentsCommand = {
212
219
  };
213
220
 
214
221
  const devCommand = {
222
+ argument: {
223
+ description: "Optional subcommand: stop (shut the running dev server down) | status (report it) | logs (print its captured output)",
224
+ name: "args",
225
+ type: String
226
+ },
215
227
  description: "Run the dev stack: wrangler worker + studio + codegen watch",
216
228
  examples: [
217
229
  ["lunora dev", "Run the worker + studio + codegen watch"],
230
+ ["lunora dev --background", "Run detached: blocks until ready, prints URL + PID, then returns"],
231
+ ["lunora dev stop", "Stop the background/tracked dev server (idempotent)"],
232
+ ["lunora dev status", "Report the running dev server (URL, PID, uptime)"],
233
+ ["lunora dev logs", "Print the captured dev-server log (background runs)"],
234
+ ["lunora dev --json", "Machine-readable JSON log lines (also LUNORA_LOG_JSON=1)"],
218
235
  ["lunora dev --no-studio", "Skip the embedded studio server"],
219
236
  ["lunora dev --worker-port 8080", "Use a custom wrangler dev port"],
220
237
  ["lunora dev --remote", "Proxy D1/KV/R2 to the deployed worker (also LUNORA_REMOTE=1)"]
@@ -224,10 +241,19 @@ const devCommand = {
224
241
  return { default: m.execute };
225
242
  }),
226
243
  name: "dev",
244
+ // KEEP IN SYNC with `daemonArguments` in `./lifecycle.ts`: a new flag that
245
+ // must reach a `--background` daemon has to be forwarded there explicitly.
227
246
  options: [
228
247
  { description: `Which API spec(s) codegen emits: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
229
248
  { description: "Studio server port (default 6173)", name: "port", type: Number },
230
249
  { description: "wrangler dev port (default 8787)", name: "worker-port", type: Number },
250
+ {
251
+ description: "Run the dev server as a managed background process (auto-enabled when an AI agent is detected; LUNORA_AGENT_MODE=0 disables)",
252
+ name: "background",
253
+ type: Boolean
254
+ },
255
+ { description: "Emit machine-readable JSON log lines (also LUNORA_LOG_JSON=1; auto-enabled for AI agents)", name: "json", type: Boolean },
256
+ { description: "How many trailing lines `lunora dev logs` prints (default 100, 0 = all)", name: "lines", type: Number },
231
257
  { description: "Don't start the embedded studio server", name: "no-studio", type: Boolean },
232
258
  { description: "Don't watch + regenerate codegen", name: "no-codegen", type: Boolean },
233
259
  { description: "Proxy D1/KV/R2 bindings to the deployed worker (or set LUNORA_REMOTE=1)", name: "remote", type: Boolean }
@@ -260,21 +286,24 @@ const doctorCommand = {
260
286
  };
261
287
 
262
288
  const envCommand = {
263
- argument: { description: "list | get <KEY> | set <KEY> <VALUE> | unset <KEY> | push | diff | doctor", name: "subcommand", type: String },
264
- description: "Manage .dev.vars and sync secrets via wrangler (list | get | set | unset | push | diff | doctor)",
289
+ argument: { description: "list | get <KEY> | set <KEY> <VALUE> | unset <KEY> | generate [KEY] | push | diff | doctor", name: "subcommand", type: String },
290
+ description: "Manage .dev.vars and sync secrets via wrangler (list | get | set | unset | generate | push | diff | doctor)",
265
291
  examples: [
266
292
  ["lunora env list", "List .dev.vars keys"],
267
293
  ["lunora env set API_KEY secret", "Set a local variable"],
294
+ ["lunora env generate", "Generate strong values for the project's secrets (print KEY=value)"],
295
+ ["lunora env generate AUTH_SECRET --set", "Generate one secret and write it to .dev.vars"],
268
296
  ["lunora env push --yes", "Upload secrets to Cloudflare"],
269
297
  ["lunora env diff", "Compare local .dev.vars keys against Cloudflare"]
270
298
  ],
271
299
  group: "Data",
272
- loader: () => import('../packem_chunks/handler21.mjs').then((m) => {
300
+ loader: () => import('../packem_chunks/handler9.mjs').then((m) => {
273
301
  return { default: m.execute };
274
302
  }),
275
303
  name: "env",
276
304
  options: [
277
305
  { description: "Target production for `push` (passes --env production to wrangler)", name: "prod", type: Boolean },
306
+ { description: "For `generate` — write the generated secrets into .dev.vars instead of printing them", name: "set", type: Boolean },
278
307
  {
279
308
  description: "Push secrets to a temporary-account deployment when unauthenticated (wrangler secret put --temporary). Errors if you're already authenticated.",
280
309
  name: "temporary",
@@ -292,7 +321,7 @@ const exportCommand = {
292
321
  ["lunora export --tables messages,users", "Export only specific tables"]
293
322
  ],
294
323
  group: "Data",
295
- loader: () => import('../packem_chunks/handler9.mjs').then((m) => {
324
+ loader: () => import('../packem_chunks/handler10.mjs').then((m) => {
296
325
  return { default: m.execute };
297
326
  }),
298
327
  name: "export",
@@ -314,7 +343,7 @@ const importCommand = {
314
343
  description: "Bulk-insert rows from an NDJSON file via the worker's admin endpoint",
315
344
  examples: [["lunora import backup.ndjson", "Bulk-insert rows from an NDJSON file"]],
316
345
  group: "Data",
317
- loader: () => import('../packem_chunks/handler10.mjs').then((m) => {
346
+ loader: () => import('../packem_chunks/handler11.mjs').then((m) => {
318
347
  return { default: m.execute };
319
348
  }),
320
349
  name: "import",
@@ -322,6 +351,7 @@ const importCommand = {
322
351
  { description: "Wrap each bare doc as `{table:<name>,doc:...}`", name: "table", type: String },
323
352
  { description: "Rows per HTTP request (default 500)", name: "batch-size", type: Number },
324
353
  { description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
354
+ { description: "Confirm bulk-writing production (required with --prod)", name: "yes", type: Boolean },
325
355
  { description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
326
356
  {
327
357
  description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
@@ -338,7 +368,7 @@ const infoCommand = {
338
368
  ["lunora info --json", "Emit a JSON snapshot"]
339
369
  ],
340
370
  group: "Project",
341
- loader: () => import('../packem_chunks/handler11.mjs').then((m) => {
371
+ loader: () => import('../packem_chunks/handler12.mjs').then((m) => {
342
372
  return { default: m.execute };
343
373
  }),
344
374
  name: "info",
@@ -350,6 +380,7 @@ const initCommand = {
350
380
  description: "Scaffold a new Lunora project",
351
381
  examples: [
352
382
  ["lunora init my-app", "Scaffold with the default (vite) template"],
383
+ ["lunora init my-app -t next", "Scaffold a Next.js app"],
353
384
  ["lunora init my-app -t tanstack-start-react", "Scaffold a TanStack Start (React) app"],
354
385
  ["lunora init my-app -t tanstack-start-solid", "Scaffold a TanStack Start (Solid) app"],
355
386
  ["lunora init my-app --ref alpha", "Scaffold from the alpha branch's templates"],
@@ -369,7 +400,7 @@ const initCommand = {
369
400
  // picker (default React overlay) and a non-interactive run errors.
370
401
  // For React/Vue/Solid/Svelte SPAs use `--vite <framework>` (overlay);
371
402
  // `-t` selects a bespoke template.
372
- description: "Bespoke template (standalone | astro | nuxt | sveltekit | tanstack-start-react | tanstack-start-solid). For an SPA use --vite react|vue|solid|svelte.",
403
+ description: "Bespoke template (standalone | astro | next | nuxt | sveltekit | tanstack-start-react | tanstack-start-solid). For an SPA use --vite react|vue|solid|svelte.",
373
404
  name: "template",
374
405
  type: String
375
406
  },
@@ -419,6 +450,16 @@ const initCommand = {
419
450
  description: "Also scaffold a CI deploy pipeline: github (.github/workflows/deploy.yml) or gitlab (.gitlab-ci.yml)",
420
451
  name: "ci",
421
452
  type: String
453
+ },
454
+ {
455
+ description: "Add features non-interactively after scaffolding (comma-separated): ai | auth | backup | browser | cloudflare-access | crons | email | flags | hyperdrive | payment | presence | queue | storage | workflow",
456
+ name: "add",
457
+ type: String
458
+ },
459
+ {
460
+ description: "Walk through every step (prompts + output) without writing files, installing, or running git",
461
+ name: "dry-run",
462
+ type: Boolean
422
463
  }
423
464
  ]
424
465
  };
@@ -432,7 +473,7 @@ const insightsCommand = {
432
473
  ["lunora insights --prod --url https://app.example.com --token $LUNORA_ADMIN_TOKEN", "Report against production"]
433
474
  ],
434
475
  group: "Develop",
435
- loader: () => import('../packem_chunks/handler12.mjs').then((m) => {
476
+ loader: () => import('../packem_chunks/handler13.mjs').then((m) => {
436
477
  return { default: m.execute };
437
478
  }),
438
479
  name: "insights",
@@ -454,7 +495,7 @@ const linkCommand = {
454
495
  ["lunora link --remove", "Remove the link"]
455
496
  ],
456
497
  group: "Deploy",
457
- loader: () => import('../packem_chunks/handler13.mjs').then((m) => {
498
+ loader: () => import('../packem_chunks/handler14.mjs').then((m) => {
458
499
  return { default: m.execute };
459
500
  }),
460
501
  name: "link",
@@ -470,7 +511,7 @@ const logsCommand = {
470
511
  argument: { description: "Worker name (defaults to the name in wrangler config)", name: "worker", type: String },
471
512
  description: "Stream live logs from a deployed Lunora Worker",
472
513
  group: "Deploy",
473
- loader: () => import('../packem_chunks/handler14.mjs').then((m) => {
514
+ loader: () => import('../packem_chunks/handler15.mjs').then((m) => {
474
515
  return { default: m.execute };
475
516
  }),
476
517
  name: "logs",
@@ -504,7 +545,7 @@ const migrateCommand = {
504
545
  name: "migrate",
505
546
  options: [
506
547
  { description: "Migration name slug (e.g. add_users_email)", name: "name", type: String },
507
- { description: "Target table for `create`", name: "table", type: String },
548
+ { description: "Target table for `create` (prompted for interactively when omitted)", name: "table", type: String },
508
549
  { description: "Preview a data migration without rewriting rows", name: "dry-run", type: Boolean },
509
550
  { description: "Rows per batch for a data migration", name: "batch-size", type: Number },
510
551
  { description: "Cap batches processed this run (maps to the runner's maxBatches)", name: "steps", type: Number },
@@ -530,7 +571,7 @@ const prepareCommand = {
530
571
  description: "Run codegen + binding reconcile + wrangler validation (no Vite) — for CI",
531
572
  examples: [["lunora prepare", "Codegen + binding reconcile + validate (CI, before deploy)"]],
532
573
  group: "Deploy",
533
- loader: () => import('../packem_chunks/handler15.mjs').then((m) => {
574
+ loader: () => import('../packem_chunks/handler16.mjs').then((m) => {
534
575
  return { default: m.execute };
535
576
  }),
536
577
  name: "prepare",
@@ -554,7 +595,7 @@ const registryCommand = {
554
595
  ["lunora registry build --check", "Verify the committed catalog is current"]
555
596
  ],
556
597
  group: "Project",
557
- loader: () => import('../packem_chunks/handler16.mjs').then((m) => {
598
+ loader: () => import('../packem_chunks/handler17.mjs').then((m) => {
558
599
  return { default: m.execute };
559
600
  }),
560
601
  name: "registry",
@@ -604,7 +645,7 @@ const rulesCommand = {
604
645
  ["lunora rules check --strict", "Exit non-zero when rules are missing (CI gate)"]
605
646
  ],
606
647
  group: "Project",
607
- loader: () => import('../packem_chunks/handler17.mjs').then((m) => {
648
+ loader: () => import('../packem_chunks/handler18.mjs').then((m) => {
608
649
  return { default: m.execute };
609
650
  }),
610
651
  name: "rules",
@@ -642,7 +683,7 @@ const seedCommand = {
642
683
  ["lunora seed --seed 7 --dry-run", "Print the NDJSON for seed 7 without inserting"]
643
684
  ],
644
685
  group: "Data",
645
- loader: () => import('../packem_chunks/handler18.mjs').then((m) => {
686
+ loader: () => import('../packem_chunks/handler19.mjs').then((m) => {
646
687
  return { default: m.execute };
647
688
  }),
648
689
  name: "seed",
@@ -671,7 +712,7 @@ const verifyCommand = {
671
712
  ["lunora verify --no-typecheck", "Skip the TypeScript type-check"]
672
713
  ],
673
714
  group: "Deploy",
674
- loader: () => import('../packem_chunks/handler19.mjs').then((m) => {
715
+ loader: () => import('../packem_chunks/handler20.mjs').then((m) => {
675
716
  return { default: m.execute };
676
717
  }),
677
718
  name: "verify",
@@ -690,13 +731,28 @@ const viewCommand = {
690
731
  ["lunora view --remote", "Open the deployed studio"]
691
732
  ],
692
733
  group: "Project",
693
- loader: () => import('../packem_chunks/handler20.mjs').then((m) => {
734
+ loader: () => import('../packem_chunks/handler21.mjs').then((m) => {
694
735
  return { default: m.execute };
695
736
  }),
696
737
  name: "view",
697
738
  options: [{ description: "Open the deployed worker URL instead of localhost", name: "remote", type: Boolean }]
698
739
  };
699
740
 
741
+ const NO_STACK = { filterStacktrace: () => false, hideErrorCodeView: true };
742
+ const renderLunoraError = (error, options = {}) => {
743
+ const message = error instanceof Error ? error.message : String(error);
744
+ const hint = resolveHint(isLunoraError(error) ? { code: error.code, hint: error.hint, message } : message);
745
+ const rendered = new VisulimaError({
746
+ // `renderError` iterates `hint` as lines; the shared flattener returns one
747
+ // string, so split it back to lines for the terminal renderer.
748
+ hint: hint === void 0 ? void 0 : flattenHint(hint).split("\n"),
749
+ message: options.reason === void 0 ? message : `${options.reason}: ${message}`,
750
+ name: error instanceof Error && error.name.length > 0 ? error.name : "Error"
751
+ });
752
+ rendered.stack = "";
753
+ return renderError(rendered, NO_STACK);
754
+ };
755
+
700
756
  const editDistance = (a, b) => {
701
757
  const distances = Array.from({ length: b.length + 1 }, (_, index) => index);
702
758
  for (let row = 1; row <= a.length; row += 1) {
@@ -759,6 +815,15 @@ const isNewer = (current, latest) => compareVersions(latest, current) > 0;
759
815
  const isCacheFresh = (checkedAt, nowMs, ttlMs) => nowMs - checkedAt < ttlMs;
760
816
  const formatUpdateNotice = (current, latest) => `Update available for @lunora/cli: ${current} → ${latest} — run \`pnpm add -D @lunora/cli@latest\``;
761
817
  const cacheFilePath = (cacheDirectory) => join(cacheDirectory, "lunora-cli-update.json");
818
+ const defaultCacheDirectory = (env) => {
819
+ const base = env.XDG_CACHE_HOME && env.XDG_CACHE_HOME.length > 0 ? env.XDG_CACHE_HOME : join(homedir(), ".cache");
820
+ const directory = join(base, "lunora");
821
+ try {
822
+ mkdirSync(directory, { mode: 448, recursive: true });
823
+ } catch {
824
+ }
825
+ return directory;
826
+ };
762
827
  const readCache = (cacheDirectory) => {
763
828
  try {
764
829
  const parsed = JSON.parse(readFileSync(cacheFilePath(cacheDirectory), "utf8"));
@@ -774,7 +839,14 @@ const readCache = (cacheDirectory) => {
774
839
  };
775
840
  const writeCache = (cacheDirectory, cache) => {
776
841
  try {
777
- writeFileSync(cacheFilePath(cacheDirectory), `${JSON.stringify(cache)}
842
+ const path = cacheFilePath(cacheDirectory);
843
+ try {
844
+ if (lstatSync(path).isSymbolicLink()) {
845
+ return;
846
+ }
847
+ } catch {
848
+ }
849
+ writeFileSync(path, `${JSON.stringify(cache)}
778
850
  `, "utf8");
779
851
  } catch {
780
852
  }
@@ -799,7 +871,7 @@ const maybeNotifyUpdate = async (deps) => {
799
871
  if (isNotifierDisabled(deps.current, env, isTty)) {
800
872
  return;
801
873
  }
802
- const cacheDirectory = deps.cacheDir ?? tmpdir();
874
+ const cacheDirectory = deps.cacheDir ?? defaultCacheDirectory(env);
803
875
  const nowMs = (deps.now ?? Date.now)();
804
876
  const ttlMs = deps.ttlMs ?? CACHE_TTL_MS;
805
877
  const cache = readCache(cacheDirectory);
@@ -911,7 +983,11 @@ const reportRunError = (error) => {
911
983
  const message = error instanceof Error ? error.message : String(error);
912
984
  const unknown = UNKNOWN_COMMAND.exec(message);
913
985
  if (!unknown?.groups) {
914
- logger.error(message);
986
+ if (isLunoraError(error) || findSolutionByMessage(message) !== void 0) {
987
+ logger.error(renderLunoraError(error));
988
+ } else {
989
+ logger.error(message);
990
+ }
915
991
  return;
916
992
  }
917
993
  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";
@@ -129,6 +130,10 @@ const resolveImportRequest = async (options) => {
129
130
  options.logger.error("--prod requires an explicit --url (refusing to import to the implicit localhost worker)");
130
131
  return void 0;
131
132
  }
133
+ if (options.prod && options.yes !== true) {
134
+ options.logger.error("import --prod bulk-writes production. Re-run with --yes to confirm.");
135
+ return void 0;
136
+ }
132
137
  const token = options.token ?? process.env["LUNORA_ADMIN_TOKEN"];
133
138
  if (!token) {
134
139
  options.logger.error("admin token required — pass --token or set LUNORA_ADMIN_TOKEN");
@@ -183,7 +188,7 @@ const runImportCommand = async (options) => {
183
188
  });
184
189
  if (!response.ok) {
185
190
  const text = await response.text().catch(() => "<no body>");
186
- throw new Error(`import batch failed (HTTP ${String(response.status)}): ${text}`);
191
+ throw new LunoraError("INTERNAL", `import batch failed (HTTP ${String(response.status)}): ${text}`);
187
192
  }
188
193
  const json = await response.json();
189
194
  if (json.inserted) {
@@ -213,7 +218,7 @@ const runImportCommand = async (options) => {
213
218
  parsedDocument = JSON.parse(trimmed);
214
219
  } catch (error) {
215
220
  const message = error instanceof Error ? error.message : String(error);
216
- throw new Error(`invalid JSON on line ${String(lineNumber)}: ${message}`, { cause: error });
221
+ throw new LunoraError("INTERNAL", `invalid JSON on line ${String(lineNumber)}: ${message}`, { cause: error });
217
222
  }
218
223
  batch.push(JSON.stringify({ doc: parsedDocument, table: options.table }));
219
224
  };
@@ -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-Dbp-Q2q3.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, "");
@@ -0,0 +1,31 @@
1
+ import { findLunoraSolution } from '@lunora/codegen';
2
+ import { VisulimaError, renderError } from '@visulima/error';
3
+
4
+ const flattenSolutionBody = (solution) => solution.body.split("\n").filter((line) => !line.startsWith("```")).join("\n").replaceAll(/\*\*(.+?)\*\*/gu, "$1").replaceAll(/`([^`]+)`/gu, "$1");
5
+ const NO_STACK = { filterStacktrace: () => false, hideErrorCodeView: true };
6
+ const renderCodegenFailure = (error, reason) => {
7
+ const message = error instanceof Error ? error.message : String(error);
8
+ const solution = findLunoraSolution(message);
9
+ const rendered = new VisulimaError({
10
+ hint: solution ? [solution.header, "", flattenSolutionBody(solution)] : void 0,
11
+ message: reason === void 0 ? `codegen failed: ${message}` : `codegen failed (${reason}): ${message}`,
12
+ name: "CodegenError"
13
+ });
14
+ rendered.stack = "";
15
+ return renderError(rendered, NO_STACK);
16
+ };
17
+ const renderCodegenHint = (message) => {
18
+ const solution = findLunoraSolution(message);
19
+ if (solution === void 0) {
20
+ return void 0;
21
+ }
22
+ const rendered = new VisulimaError({
23
+ hint: [flattenSolutionBody(solution)],
24
+ message: solution.header,
25
+ name: "Hint"
26
+ });
27
+ rendered.stack = "";
28
+ return renderError(rendered, NO_STACK);
29
+ };
30
+
31
+ export { renderCodegenHint as a, renderCodegenFailure as r };