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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE.md +6 -0
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/bin.mjs +1 -1
  4. package/dist/index.d.mts +248 -115
  5. package/dist/index.d.ts +248 -115
  6. package/dist/index.mjs +8 -8
  7. package/dist/packem_chunks/handler.mjs +87 -6
  8. package/dist/packem_chunks/handler10.mjs +8 -14
  9. package/dist/packem_chunks/handler11.mjs +19 -189
  10. package/dist/packem_chunks/handler12.mjs +176 -115
  11. package/dist/packem_chunks/handler13.mjs +118 -52
  12. package/dist/packem_chunks/handler14.mjs +50 -43
  13. package/dist/packem_chunks/handler15.mjs +48 -67
  14. package/dist/packem_chunks/handler16.mjs +74 -37
  15. package/dist/packem_chunks/handler17.mjs +39 -100
  16. package/dist/packem_chunks/handler18.mjs +87 -152
  17. package/dist/packem_chunks/handler19.mjs +148 -67
  18. package/dist/packem_chunks/handler2.mjs +6 -4
  19. package/dist/packem_chunks/handler20.mjs +77 -75
  20. package/dist/packem_chunks/handler21.mjs +71 -288
  21. package/dist/packem_chunks/handler3.mjs +6 -3
  22. package/dist/packem_chunks/handler4.mjs +2 -2
  23. package/dist/packem_chunks/handler5.mjs +7 -4
  24. package/dist/packem_chunks/handler6.mjs +10 -7
  25. package/dist/packem_chunks/handler7.mjs +2 -2
  26. package/dist/packem_chunks/handler8.mjs +1 -1
  27. package/dist/packem_chunks/handler9.mjs +315 -12
  28. package/dist/packem_chunks/planDevCommand.mjs +653 -37
  29. package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
  30. package/dist/packem_chunks/runDeployCommand.mjs +115 -20
  31. package/dist/packem_chunks/runInitCommand.mjs +1602 -172
  32. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +5 -4
  33. package/dist/packem_chunks/runResetCommand.mjs +2 -2
  34. package/dist/packem_chunks/runRpcCommand.mjs +3 -2
  35. package/dist/packem_shared/{COMMANDS-Bn8luojF.mjs → COMMANDS-0BD3K1pb.mjs} +79 -21
  36. package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-CRQmmBZM.mjs} +3 -2
  37. package/dist/packem_shared/{api-spec-CtA6ilu4.mjs → api-spec-Bx0iKbxA.mjs} +3 -1
  38. package/dist/packem_shared/{buildRegistryIndex-BcYe607_.mjs → buildRegistryIndex-CqM5FwGI.mjs} +1 -1
  39. package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +31 -0
  40. package/dist/packem_shared/{command-BDXcJCCJ.mjs → command-lYnl4QyF.mjs} +6 -1
  41. package/dist/packem_shared/{commands-DqsEzojt.mjs → commands-vnRRkhNE.mjs} +123 -15
  42. package/dist/packem_shared/{createLogger-CHPNjFw2.mjs → createLogger-CIWSHrTL.mjs} +40 -8
  43. package/dist/packem_shared/{createRecordingSpawner-DxI3mebw.mjs → createRecordingSpawner-Cw5Iu73G.mjs} +12 -2
  44. package/dist/packem_shared/{detect-package-manager-DYp7n3mJ.mjs → detect-package-manager-DvEthdCw.mjs} +26 -30
  45. package/dist/packem_shared/{insertSchemaExtension-BuzF6-t2.mjs → insertSchemaExtension-DAqbfr9Z.mjs} +15 -10
  46. package/dist/packem_shared/{open-url-Dfq6fAyT.mjs → open-url-4PBLY9X0.mjs} +3 -2
  47. package/dist/packem_shared/{output-format-7gyGR3h8.mjs → output-format-B4642rjE.mjs} +1 -1
  48. package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-BBrXCg9V.mjs} +16 -9
  49. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
  50. package/dist/packem_shared/runAddCommand-BCcJzs_k.mjs +4 -0
  51. package/dist/packem_shared/storage-bGfgwOgF.mjs +84 -0
  52. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +658 -0
  53. package/dist/packem_shared/wrangler-secrets-Dq_Fkbm-.mjs +49 -0
  54. package/package.json +10 -7
  55. package/skills/lunora-quickstart/SKILL.md +25 -5
  56. package/skills/lunora-setup-storage/SKILL.md +7 -3
  57. package/dist/packem_shared/features-ocSSpZtS.mjs +0 -24
  58. package/dist/packem_shared/runAddCommand-G544_v6e.mjs +0 -4
  59. package/dist/packem_shared/tui-prompts-XHFxlOg5.mjs +0 -269
@@ -1,22 +1,51 @@
1
+ import { STEP_BADGE_NAMES, LunoraReporter } from '@lunora/config';
1
2
  import { JsonReporter } from '@visulima/pail/reporter/json';
2
- import { PrettyReporter } from '@visulima/pail/reporter/pretty';
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 : PrettyReporter;
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)];
12
24
  };
13
- let sharedPail;
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;
40
+ };
41
+ const STEP_LOG_TYPES = Object.fromEntries(STEP_BADGE_NAMES.map((name) => [name, { label: name, logLevel: "informational" }]));
14
42
  const getPail = () => {
15
43
  sharedPail ??= createPail({
16
- reporters: [buildReporter()],
44
+ reporters: buildReporters(),
17
45
  scope: ["lunora"],
18
46
  stderr: process.stderr,
19
- stdout: process.stdout
47
+ stdout: process.stdout,
48
+ types: STEP_LOG_TYPES
20
49
  });
21
50
  return sharedPail;
22
51
  };
@@ -69,5 +98,8 @@ const pail = /* @__PURE__ */ new Proxy({}, {
69
98
  return typeof value === "function" ? value.bind(instance) : value;
70
99
  }
71
100
  });
101
+ const logStep = (type, message) => {
102
+ getPail()[type](message);
103
+ };
72
104
 
73
- export { createLogger, createStderrLogger, getPail, 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,9 +1,7 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { dirname, join } from '@visulima/path';
2
+ import { LunoraError } from '@lunora/errors';
3
+ import { findPackageManagerSync, identifyInitiatingPackageManager } from '@visulima/package/package-manager';
4
4
 
5
- const FALLBACK = "pnpm";
6
- const KNOWN_MANAGERS = ["pnpm", "yarn", "npm", "bun"];
7
5
  const INSTALL_PREFERENCE = ["pnpm", "bun", "yarn", "npm"];
8
6
  const isManagerInstalled = (manager) => {
9
7
  try {
@@ -16,34 +14,23 @@ const detectInstalledManagers = (probe = isManagerInstalled) => INSTALL_PREFEREN
16
14
  const installArgsFor = (manager) => {
17
15
  return { args: ["install"], command: manager };
18
16
  };
19
- const parseDeclaredManager = (declared) => {
20
- if (typeof declared !== "string") {
21
- return void 0;
22
- }
23
- return KNOWN_MANAGERS.find((manager) => declared.startsWith(`${manager}@`));
24
- };
25
- const readDeclaredManager = (directory) => {
26
- const candidate = join(directory, "package.json");
27
- if (!existsSync(candidate)) {
28
- return void 0;
29
- }
17
+ const detectPackageManager = (startDirectory) => {
30
18
  try {
31
- const parsed = JSON.parse(readFileSync(candidate, "utf8"));
32
- return parseDeclaredManager(parsed.packageManager);
19
+ return findPackageManagerSync(startDirectory).packageManager;
33
20
  } catch {
34
- return void 0;
35
21
  }
36
- };
37
- const detectPackageManager = (startDirectory) => {
38
- let directory = startDirectory;
39
- while (directory && directory !== dirname(directory)) {
40
- const declared = readDeclaredManager(directory);
41
- if (declared !== void 0) {
42
- return declared;
43
- }
44
- directory = dirname(directory);
45
- }
46
- return FALLBACK;
22
+ const initiating = identifyInitiatingPackageManager();
23
+ if (initiating !== void 0) {
24
+ return initiating.name === "cnpm" ? "npm" : initiating.name;
25
+ }
26
+ const [installed] = detectInstalledManagers();
27
+ if (installed !== void 0) {
28
+ return installed;
29
+ }
30
+ throw new LunoraError(
31
+ "INTERNAL",
32
+ "Could not detect a package manager: no lock file or `packageManager` field was found, and none (pnpm, bun, yarn, npm) is installed on PATH."
33
+ );
47
34
  };
48
35
  const execArgsFor = (manager, command, args) => {
49
36
  if (manager === "yarn") {
@@ -57,5 +44,14 @@ const execArgsFor = (manager, command, args) => {
57
44
  }
58
45
  return { args: ["exec", command, ...args], command: "pnpm" };
59
46
  };
47
+ const runScriptCommand = (manager, script) => {
48
+ if (manager === "npm") {
49
+ return `npm run ${script}`;
50
+ }
51
+ if (manager === "bun") {
52
+ return `bun run ${script}`;
53
+ }
54
+ return `${manager} ${script}`;
55
+ };
60
56
 
61
- export { detectInstalledManagers as a, detectPackageManager as d, execArgsFor as e, installArgsFor as i };
57
+ export { detectInstalledManagers as a, detectPackageManager as d, execArgsFor as e, installArgsFor as i, runScriptCommand as r };
@@ -1,4 +1,4 @@
1
- import { Project, SyntaxKind } from 'ts-morph';
1
+ import { Project, SyntaxKind, Node } from 'ts-morph';
2
2
 
3
3
  const VALID_JS_IDENTIFIER = /^[A-Za-z_$][\w$]*$/u;
4
4
  const startMarker = (key) => `// lunora:add:${key}:start`;
@@ -12,6 +12,19 @@ const findDefineSchemaCall = (callExpressions) => {
12
12
  }
13
13
  return void 0;
14
14
  };
15
+ const outermostChainExpression = (defineSchemaCall) => {
16
+ let node = defineSchemaCall;
17
+ for (let parent = node.getParent(); parent !== void 0; parent = node.getParent()) {
18
+ if (Node.isPropertyAccessExpression(parent) && parent.getExpression() === node) {
19
+ node = parent;
20
+ } else if (Node.isCallExpression(parent) && parent.getExpression() === node) {
21
+ node = parent;
22
+ } else {
23
+ break;
24
+ }
25
+ }
26
+ return node;
27
+ };
15
28
  const insertSchemaExtension = (source, key) => {
16
29
  if (!VALID_JS_IDENTIFIER.test(key)) {
17
30
  return { ok: false, reason: "invalid-identifier" };
@@ -33,15 +46,7 @@ const insertSchemaExtension = (source, key) => {
33
46
  if (tablesArgument?.getKind() !== SyntaxKind.ObjectLiteralExpression) {
34
47
  return { ok: false, reason: "non-object-argument" };
35
48
  }
36
- const variableDeclaration = defineSchemaCall.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
37
- if (!variableDeclaration) {
38
- return { ok: false, reason: "no-define-schema" };
39
- }
40
- const initializer = variableDeclaration.getInitializer();
41
- if (!initializer) {
42
- return { ok: false, reason: "no-define-schema" };
43
- }
44
- const insertAt = initializer.getEnd();
49
+ const insertAt = outermostChainExpression(defineSchemaCall).getEnd();
45
50
  const chainText = `
46
51
  ${startMarker(key)}
47
52
  .extend(${key}.extension)
@@ -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-CHPNjFw2.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,9 @@
1
+ class PromptCancelledError extends Error {
2
+ constructor() {
3
+ super("cancelled");
4
+ this.name = "PromptCancelledError";
5
+ }
6
+ }
7
+ const PROMPT_CANCEL_EXIT_CODE = 130;
8
+
9
+ export { PromptCancelledError as P, PROMPT_CANCEL_EXIT_CODE as a };
@@ -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';
@@ -0,0 +1,84 @@
1
+ import { s as setBindingField } from './commands-vnRRkhNE.mjs';
2
+
3
+ const INVALID_SLUG_CHARS = /[^a-z0-9]+/u;
4
+ const toKebabSlug = (input, min, max) => {
5
+ let slug = input.toLowerCase().split(INVALID_SLUG_CHARS).filter(Boolean).join("-").slice(0, max);
6
+ if (slug.endsWith("-")) {
7
+ slug = slug.slice(0, -1);
8
+ }
9
+ return slug.length >= min ? slug : void 0;
10
+ };
11
+
12
+ const DB_BINDING = "DB";
13
+ const AUTH_DB_PROMPT = "Name your D1 database (run `wrangler d1 create` to get its id, then put it in wrangler.jsonc)";
14
+ const sanitizeDatabaseName = (input) => toKebabSlug(input, 1, 64);
15
+ const FALLBACK_DATABASE_NAME = "lunora-db";
16
+ const deriveDatabaseName = (projectName) => sanitizeDatabaseName(`${projectName}-db`) ?? FALLBACK_DATABASE_NAME;
17
+ const promptDatabaseName = async (text, projectName) => {
18
+ const fallback = deriveDatabaseName(projectName);
19
+ return sanitizeDatabaseName(await text(AUTH_DB_PROMPT, { default: fallback, placeholder: fallback })) ?? fallback;
20
+ };
21
+ const withAuthDatabaseName = (manifest, name) => setBindingField(manifest, "d1_databases", { key: "binding", value: DB_BINDING }, "database_name", name);
22
+
23
+ const AUTH_PROVIDER_OPTIONS = [
24
+ { description: "Email + password on better-auth (default)", label: "Email & password", value: "auth" },
25
+ { description: "Clerk-hosted auth", label: "Clerk", value: "auth-clerk" },
26
+ { description: "Auth0 (OIDC)", label: "Auth0", value: "auth-auth0" }
27
+ ];
28
+ const DEFAULT_AUTH_ITEM = "auth";
29
+ const promptAuthProvider = async (select) => await select("Which auth provider?", AUTH_PROVIDER_OPTIONS, { default: DEFAULT_AUTH_ITEM }) ?? DEFAULT_AUTH_ITEM;
30
+ const EMAIL_ITEM = "mail";
31
+ const normalizeFeature = (raw) => {
32
+ const value = raw.trim();
33
+ if (value === "") {
34
+ return void 0;
35
+ }
36
+ const lower = value.toLowerCase();
37
+ if (lower === "auth") {
38
+ return { kind: "auth" };
39
+ }
40
+ if (lower === "email" || lower === "mail") {
41
+ return { kind: "email" };
42
+ }
43
+ return { item: lower, kind: "item" };
44
+ };
45
+
46
+ const SEND_EMAIL_BINDING = "SEND_EMAIL";
47
+ const MAIL_DESTINATION_PROMPT = "Verified destination email for production delivery (blank = set it later in wrangler.jsonc)";
48
+ const isValidEmail = (value) => {
49
+ const trimmed = value.trim();
50
+ if (trimmed.length === 0 || trimmed.includes(" ")) {
51
+ return false;
52
+ }
53
+ const at = trimmed.indexOf("@");
54
+ if (at <= 0 || at !== trimmed.lastIndexOf("@")) {
55
+ return false;
56
+ }
57
+ const domain = trimmed.slice(at + 1);
58
+ return domain.length >= 3 && domain.includes(".") && !domain.startsWith(".") && !domain.endsWith(".");
59
+ };
60
+ const resolveTypedDestination = (entered, warn) => {
61
+ const trimmed = entered.trim();
62
+ if (trimmed === "") {
63
+ return void 0;
64
+ }
65
+ if (!isValidEmail(trimmed)) {
66
+ warn(`"${trimmed}" doesn't look like an email — leaving the placeholder; set destination_address in wrangler.jsonc.`);
67
+ return void 0;
68
+ }
69
+ return trimmed;
70
+ };
71
+ const withMailDestination = (manifest, address) => setBindingField(manifest, "send_email", { key: "name", value: SEND_EMAIL_BINDING }, "destination_address", address);
72
+
73
+ const UPLOADS_BINDING = "UPLOADS";
74
+ const STORAGE_BUCKET_PROMPT = "Name your R2 bucket (you can rename it in wrangler.jsonc later)";
75
+ const sanitizeBucketName = (input) => toKebabSlug(input, 3, 63);
76
+ const FALLBACK_BUCKET_NAME = "lunora-uploads";
77
+ const deriveBucketName = (projectName) => sanitizeBucketName(`${projectName}-uploads`) ?? FALLBACK_BUCKET_NAME;
78
+ const promptBucketName = async (text, projectName) => {
79
+ const fallback = deriveBucketName(projectName);
80
+ return sanitizeBucketName(await text(STORAGE_BUCKET_PROMPT, { default: fallback, placeholder: fallback })) ?? fallback;
81
+ };
82
+ const withStorageBucketName = (manifest, bucketName) => setBindingField(manifest, "r2_buckets", { key: "binding", value: UPLOADS_BINDING }, "bucket_name", bucketName);
83
+
84
+ export { AUTH_PROVIDER_OPTIONS as A, DEFAULT_AUTH_ITEM as D, EMAIL_ITEM as E, MAIL_DESTINATION_PROMPT as M, sanitizeDatabaseName as a, deriveDatabaseName as b, promptDatabaseName as c, deriveBucketName as d, promptAuthProvider as e, withMailDestination as f, withAuthDatabaseName as g, normalizeFeature as n, promptBucketName as p, resolveTypedDestination as r, sanitizeBucketName as s, withStorageBucketName as w };