@lunora/cli 1.0.0-alpha.1 → 1.0.0-alpha.100

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 (60) 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 +473 -344
  5. package/dist/index.d.ts +473 -344
  6. package/dist/index.mjs +10 -11
  7. package/dist/packem_chunks/handler.mjs +91 -7
  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 +27 -6
  14. package/dist/packem_chunks/handler16.mjs +6 -3
  15. package/dist/packem_chunks/handler17.mjs +1 -1
  16. package/dist/packem_chunks/handler18.mjs +5 -7
  17. package/dist/packem_chunks/handler19.mjs +44 -5
  18. package/dist/packem_chunks/handler2.mjs +6 -4
  19. package/dist/packem_chunks/handler20.mjs +2 -2
  20. package/dist/packem_chunks/handler21.mjs +158 -267
  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 +759 -121
  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 +2053 -112
  32. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +98 -51
  33. package/dist/packem_chunks/runResetCommand.mjs +4 -4
  34. package/dist/packem_chunks/runRpcCommand.mjs +3 -2
  35. package/dist/packem_shared/{COMMANDS-1V_KEx35.mjs → COMMANDS-BOJIDoVY.mjs} +154 -28
  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/{runAddCommand-BZGkRnBs.mjs → commands-D5Yxt9VY.mjs} +331 -33
  42. package/dist/packem_shared/{createLogger-CHPNjFw2.mjs → createLogger-CIWSHrTL.mjs} +40 -8
  43. package/dist/packem_shared/{defaultSpawner-DxI3mebw.mjs → createRecordingSpawner-WuSn20kb.mjs} +23 -2
  44. package/dist/packem_shared/detect-package-manager-v4hHpQd0.mjs +62 -0
  45. package/dist/packem_shared/{diffSnapshots-RR2ZE8Ya.mjs → diffSnapshots-BeDvvNiF.mjs} +1 -1
  46. package/dist/packem_shared/{insertSchemaExtension-BuzF6-t2.mjs → insertSchemaExtension-DAqbfr9Z.mjs} +15 -10
  47. package/dist/packem_shared/{open-url-Dfq6fAyT.mjs → open-url-4PBLY9X0.mjs} +3 -2
  48. package/dist/packem_shared/{output-format-7gyGR3h8.mjs → output-format-B4642rjE.mjs} +1 -1
  49. package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-Dbp-Q2q3.mjs} +36 -9
  50. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
  51. package/dist/packem_shared/runAddCommand-D1hgfqFQ.mjs +4 -0
  52. package/dist/packem_shared/{schemaIrToSnapshot-aBTo7TM5.mjs → schemaIrToSnapshot-DdsljJT-.mjs} +1 -1
  53. package/dist/packem_shared/storage-BXU4ax4O.mjs +84 -0
  54. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +658 -0
  55. package/dist/packem_shared/wrangler-secrets-Coni-mER.mjs +49 -0
  56. package/package.json +23 -17
  57. package/skills/lunora-quickstart/SKILL.md +26 -5
  58. package/skills/lunora-setup-storage/SKILL.md +7 -3
  59. package/dist/packem_shared/features-ocSSpZtS.mjs +0 -24
  60. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
@@ -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 };
@@ -1,4 +1,5 @@
1
- import { createLogger } from './createLogger-CHPNjFw2.mjs';
1
+ import { createLogger } from './createLogger-CIWSHrTL.mjs';
2
+ import { P as PromptCancelledError, a as PROMPT_CANCEL_EXIT_CODE } from './prompt-cancelled-APzX1Im-.mjs';
2
3
 
3
4
  const defineHandler = (body) => async (toolbox) => {
4
5
  const logger = createLogger();
@@ -6,6 +7,10 @@ const defineHandler = (body) => async (toolbox) => {
6
7
  const { code } = await body({ argument: toolbox.argument, cwd: toolbox.process.cwd, logger, options: toolbox.options });
7
8
  toolbox.process.exit(code);
8
9
  } catch (error) {
10
+ if (error instanceof PromptCancelledError) {
11
+ toolbox.process.exit(PROMPT_CANCEL_EXIT_CODE);
12
+ return;
13
+ }
9
14
  logger.error(error instanceof Error ? error.message : String(error));
10
15
  toolbox.process.exit(1);
11
16
  }
@@ -1,15 +1,169 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
2
- import { join, dirname } from '@visulima/path';
3
- import { DEV_VARS_FILE, parseDevVariableEntries, promptYesNo } from '@lunora/config';
2
+ import { dirname, join, relative } from '@visulima/path';
3
+ import { DEV_VARS_FILE, parseDevVariableEntries, findWranglerFile, readWranglerJsonc } from '@lunora/config';
4
4
  import { modify, applyEdits, parse } from 'jsonc-parser';
5
- import { collectCatalog, buildRegistryIndex } from './buildRegistryIndex-BcYe607_.mjs';
6
- import { insertSchemaExtension } from './insertSchemaExtension-BuzF6-t2.mjs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { LunoraError } from '@lunora/errors';
7
+ import { b as tuiConfirm } from './tui-prompts-BjEN8XgP.mjs';
8
+ import { collectCatalog, buildRegistryIndex } from './buildRegistryIndex-BS5ig822.mjs';
9
+ import { insertSchemaExtension } from './insertSchemaExtension-DAqbfr9Z.mjs';
7
10
  import { createHash } from 'node:crypto';
8
11
  import { tmpdir } from 'node:os';
9
12
  import { downloadTemplate } from 'giget';
10
- import parseManifest from './parseManifest--vZf2FY1.mjs';
13
+ import parseManifest from './parseManifest-Dbp-Q2q3.mjs';
11
14
 
12
- const applyDeps = (deps, projectRoot, logger, section = "dependencies") => {
15
+ const DEFAULT_SOURCE_REF_FALLBACK = "alpha";
16
+ const STABLE_BRANCH = "main";
17
+ const PRERELEASE_CHANNEL_BRANCHES = /* @__PURE__ */ new Set(["alpha", "beta", "next"]);
18
+ const SAFE_REF = /^[\w./@-]+$/;
19
+ const isSafeRef = (ref) => !ref.includes("..") && SAFE_REF.test(ref);
20
+ const resolveCliVersion = () => {
21
+ try {
22
+ let directory = dirname(fileURLToPath(import.meta.url));
23
+ for (let index = 0; index < 6; index += 1) {
24
+ const candidate = join(directory, "package.json");
25
+ if (existsSync(candidate)) {
26
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
27
+ if (parsed.name === "@lunora/cli" && typeof parsed.version === "string") {
28
+ return parsed.version;
29
+ }
30
+ }
31
+ const parent = dirname(directory);
32
+ if (parent === directory) {
33
+ break;
34
+ }
35
+ directory = parent;
36
+ }
37
+ } catch {
38
+ }
39
+ return "0.0.0";
40
+ };
41
+ const resolveVersionRef = (version) => {
42
+ if (version === "0.0.0") {
43
+ return DEFAULT_SOURCE_REF_FALLBACK;
44
+ }
45
+ const core = version.split("+")[0] ?? version;
46
+ const dashIndex = core.indexOf("-");
47
+ if (dashIndex !== -1) {
48
+ const [channel] = core.slice(dashIndex + 1).split(".");
49
+ if (channel !== void 0 && PRERELEASE_CHANNEL_BRANCHES.has(channel)) {
50
+ return channel;
51
+ }
52
+ }
53
+ return STABLE_BRANCH;
54
+ };
55
+ const resolveSourceRef = (ref) => {
56
+ if (ref !== void 0 && ref.length > 0) {
57
+ if (!isSafeRef(ref)) {
58
+ throw new LunoraError("INTERNAL", `invalid --ref "${ref}" — a ref may contain letters, digits, ".", "_", "-", "/", "@" and must not contain "..".`);
59
+ }
60
+ return ref;
61
+ }
62
+ return resolveVersionRef(resolveCliVersion());
63
+ };
64
+ const SOURCE_REPO = "anolilab/lunora";
65
+ const COMMIT_SHA = /^[0-9a-f]{40}$/iu;
66
+ const SEMVER_BODY = String.raw`\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)+)?(?:\+[0-9A-Za-z.-]+)?`;
67
+ const LEADING_VERSION_TAG = new RegExp(String.raw`^v?${SEMVER_BODY}$`, "u");
68
+ const PACKAGE_VERSION_TAG = new RegExp(String.raw`^(?:@[\w.-]+\/)?[\w.-]+@${SEMVER_BODY}$`, "u");
69
+ const isImmutableRef = (ref) => COMMIT_SHA.test(ref) || LEADING_VERSION_TAG.test(ref) || PACKAGE_VERSION_TAG.test(ref);
70
+ const githubAuthHeaders = () => {
71
+ const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
72
+ return token !== void 0 && token.length > 0 ? { authorization: `Bearer ${token}` } : {};
73
+ };
74
+ const fetchBranchSha = async (branch) => {
75
+ try {
76
+ const response = await fetch(`https://api.github.com/repos/${SOURCE_REPO}/commits/${encodeURIComponent(branch)}`, {
77
+ headers: { accept: "application/vnd.github+json", "user-agent": "lunora-cli", ...githubAuthHeaders() },
78
+ signal: AbortSignal.timeout(1e4)
79
+ });
80
+ if (!response.ok) {
81
+ return void 0;
82
+ }
83
+ const body = await response.json();
84
+ return typeof body.sha === "string" && COMMIT_SHA.test(body.sha) ? body.sha : void 0;
85
+ } catch {
86
+ return void 0;
87
+ }
88
+ };
89
+ const resolvePinnedSourceRef = async (ref, logger) => {
90
+ const resolved = resolveSourceRef(ref);
91
+ if (isImmutableRef(resolved)) {
92
+ return resolved;
93
+ }
94
+ const sha = await fetchBranchSha(resolved);
95
+ if (sha === void 0) {
96
+ logger.warn(`could not pin ${SOURCE_REPO}#${resolved} to a commit — fetching the UNPINNED branch (set GITHUB_TOKEN if rate-limited).`);
97
+ return resolved;
98
+ }
99
+ logger.info(`pinned ${SOURCE_REPO}#${resolved} → ${sha}`);
100
+ return sha;
101
+ };
102
+ const STABLE_DIST_TAG = "latest";
103
+ const resolveDistTag = (version = resolveCliVersion()) => {
104
+ const ref = resolveVersionRef(version);
105
+ return ref === STABLE_BRANCH ? STABLE_DIST_TAG : ref;
106
+ };
107
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org";
108
+ const registryBase = () => {
109
+ const configured = process.env["npm_config_registry"];
110
+ const base = configured !== void 0 && configured.length > 0 ? configured : DEFAULT_REGISTRY;
111
+ return base.endsWith("/") ? base.slice(0, -1) : base;
112
+ };
113
+ const resolveTagVersion = async (packageName, tag) => {
114
+ try {
115
+ const response = await fetch(`${registryBase()}/${packageName.replaceAll("/", "%2F")}`, {
116
+ headers: { accept: "application/vnd.npm.install-v1+json" },
117
+ signal: AbortSignal.timeout(1e4)
118
+ });
119
+ if (!response.ok) {
120
+ return void 0;
121
+ }
122
+ const packument = await response.json();
123
+ return packument["dist-tags"]?.[tag];
124
+ } catch {
125
+ return void 0;
126
+ }
127
+ };
128
+ const resolveTagVersions = async (names, tag) => {
129
+ const resolved = /* @__PURE__ */ new Map();
130
+ await Promise.all(
131
+ [...new Set(names)].map(async (name) => {
132
+ const version = await resolveTagVersion(name, tag);
133
+ if (version !== void 0) {
134
+ resolved.set(name, version);
135
+ }
136
+ })
137
+ );
138
+ return resolved;
139
+ };
140
+
141
+ const resolveDepRange = (range) => {
142
+ if (!range.startsWith("workspace:")) {
143
+ return range;
144
+ }
145
+ const rest = range.slice("workspace:".length);
146
+ if (rest === "" || rest === "*" || rest === "^" || rest === "~") {
147
+ return resolveDistTag();
148
+ }
149
+ return rest;
150
+ };
151
+ const UMBRELLA_REEXPORTED_DEPS = /* @__PURE__ */ new Set(["@lunora/client", "@lunora/do", "@lunora/ratelimit", "@lunora/runtime", "@lunora/server", "@lunora/values"]);
152
+ const UMBRELLA_IMPORT_RE = /(['"])@lunora\/(client|do|ratelimit|runtime|server|values)(\/[^'"]*)?\1/gu;
153
+ const projectUsesUmbrella = (projectRoot) => {
154
+ const packageJsonPath = join(projectRoot, "package.json");
155
+ if (!existsSync(packageJsonPath)) {
156
+ return false;
157
+ }
158
+ try {
159
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
160
+ return parsed.dependencies?.lunorash !== void 0 || parsed.devDependencies?.lunorash !== void 0;
161
+ } catch {
162
+ return false;
163
+ }
164
+ };
165
+ const rewriteUmbrellaImports = (source) => source.replaceAll(UMBRELLA_IMPORT_RE, (_match, quote, base, subpath) => `${quote}lunorash/${base}${subpath ?? ""}${quote}`);
166
+ const applyDeps = (deps, projectRoot, logger, section = "dependencies", useUmbrella = false) => {
13
167
  const entries = Object.entries(deps);
14
168
  if (entries.length === 0) {
15
169
  return [];
@@ -23,11 +177,15 @@ const applyDeps = (deps, projectRoot, logger, section = "dependencies") => {
23
177
  const parsed = JSON.parse(text);
24
178
  const added = [];
25
179
  for (const [name, range] of entries) {
180
+ if (useUmbrella && UMBRELLA_REEXPORTED_DEPS.has(name)) {
181
+ logger.info(`dep provided by the lunorash umbrella, skipping: ${name}`);
182
+ continue;
183
+ }
26
184
  if (parsed.dependencies?.[name] !== void 0 || parsed.devDependencies?.[name] !== void 0) {
27
185
  logger.info(`dep already present: ${name}`);
28
186
  continue;
29
187
  }
30
- const edits = modify(text, [section, name], range, {
188
+ const edits = modify(text, [section, name], resolveDepRange(range), {
31
189
  formattingOptions: { insertSpaces: true, tabSize: 4 }
32
190
  });
33
191
  text = applyEdits(text, edits);
@@ -148,14 +306,14 @@ const applyBindings = (bindings, projectRoot, logger) => {
148
306
  }
149
307
  return applied;
150
308
  };
151
- const applyItemResources = (manifest, cwd, logger) => {
309
+ const applyItemResources = (manifest, cwd, logger, useUmbrella = false) => {
152
310
  const deps = [];
153
311
  const bindings = [];
154
312
  if (manifest.deps) {
155
- deps.push(...applyDeps(manifest.deps, cwd, logger));
313
+ deps.push(...applyDeps(manifest.deps, cwd, logger, "dependencies", useUmbrella));
156
314
  }
157
315
  if (manifest.devDependencies) {
158
- deps.push(...applyDeps(manifest.devDependencies, cwd, logger, "devDependencies"));
316
+ deps.push(...applyDeps(manifest.devDependencies, cwd, logger, "devDependencies", useUmbrella));
159
317
  }
160
318
  if (manifest.bindings) {
161
319
  bindings.push(...applyBindings(manifest.bindings, cwd, logger));
@@ -187,8 +345,8 @@ const confirmDepMutation = async (items, options) => {
187
345
  options.logger.error(`add: stdin is not a TTY and the requested items ${reasonText} — re-run with --yes to confirm`);
188
346
  return false;
189
347
  }
190
- const confirmer = options.confirm ?? promptYesNo;
191
- const confirmed = await confirmer(`The requested items ${reasonText}. Continue? [y/N] `);
348
+ const confirmer = options.confirm ?? tuiConfirm;
349
+ const confirmed = await confirmer(`The requested items ${reasonText}. Continue?`);
192
350
  if (!confirmed) {
193
351
  options.logger.info("add: aborted");
194
352
  }
@@ -267,7 +425,12 @@ const renderDiff = (oldText, newText) => {
267
425
  return out;
268
426
  };
269
427
 
270
- const reconcileSchemaExtension = (file, itemKey, itemDirectory, projectRoot, logger, diff) => {
428
+ const CODE_FILE_RE = /\.[cm]?[jt]sx?$/u;
429
+ const readItemFile = (itemDirectory, file, useUmbrella) => {
430
+ const source = readFileSync(join(itemDirectory, file.from), "utf8");
431
+ return useUmbrella && CODE_FILE_RE.test(file.to) ? rewriteUmbrellaImports(source) : source;
432
+ };
433
+ const reconcileSchemaExtension = (file, itemKey, itemDirectory, projectRoot, logger, diff, useUmbrella) => {
271
434
  const schemaPath = join(projectRoot, "lunora", "schema.ts");
272
435
  if (diff) {
273
436
  logger.info(`~ would merge .extend(${itemKey}.extension) into lunora/schema.ts (and create ${file.to} if absent)`);
@@ -276,9 +439,13 @@ const reconcileSchemaExtension = (file, itemKey, itemDirectory, projectRoot, log
276
439
  const destinationPath = join(projectRoot, file.to);
277
440
  if (!existsSync(destinationPath)) {
278
441
  mkdirSync(dirname(destinationPath), { recursive: true });
279
- writeFileSync(destinationPath, readFileSync(join(itemDirectory, file.from), "utf8"), "utf8");
442
+ writeFileSync(destinationPath, readItemFile(itemDirectory, file, useUmbrella), "utf8");
280
443
  }
281
- const existingSchema = existsSync(schemaPath) ? readFileSync(schemaPath, "utf8") : 'import { defineSchema } from "@lunora/server";\n\nexport const schema = defineSchema({});\n';
444
+ const baseModule = useUmbrella ? "lunorash/server" : "@lunora/server";
445
+ const existingSchema = existsSync(schemaPath) ? readFileSync(schemaPath, "utf8") : `import { defineSchema } from "${baseModule}";
446
+
447
+ export const schema = defineSchema({});
448
+ `;
282
449
  const result = insertSchemaExtension(existingSchema, itemKey);
283
450
  if (result.ok) {
284
451
  mkdirSync(dirname(schemaPath), { recursive: true });
@@ -291,11 +458,12 @@ const reconcileSchemaExtension = (file, itemKey, itemDirectory, projectRoot, log
291
458
  return { kind: "skipped", path: schemaPath };
292
459
  }
293
460
  if (result.reason === "invalid-identifier") {
294
- throw new Error(
461
+ throw new LunoraError(
462
+ "INTERNAL",
295
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 "-").`
296
464
  );
297
465
  }
298
- throw new Error(`schema-extension merge failed for "${itemKey}": ${result.reason}`);
466
+ throw new LunoraError("INTERNAL", `schema-extension merge failed for "${itemKey}": ${result.reason}`);
299
467
  };
300
468
  const previewWholeFile = (file, current, incoming, exists, logger) => {
301
469
  const lines = renderDiff(current, incoming);
@@ -308,9 +476,9 @@ const previewWholeFile = (file, current, incoming, exists, logger) => {
308
476
  logger.info(` ${line}`);
309
477
  }
310
478
  };
311
- const reconcileWholeFile = (file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions) => {
479
+ const reconcileWholeFile = (file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions, useUmbrella) => {
312
480
  const destinationPath = join(projectRoot, file.to);
313
- const incoming = readFileSync(join(itemDirectory, file.from), "utf8");
481
+ const incoming = readItemFile(itemDirectory, file, useUmbrella);
314
482
  const exists = existsSync(destinationPath);
315
483
  const current = exists ? readFileSync(destinationPath, "utf8") : "";
316
484
  const write = (message) => {
@@ -348,11 +516,95 @@ const reconcileWholeFile = (file, itemKey, itemDirectory, projectRoot, logger, l
348
516
  logger.warn(`conflict: ${file.to} has local edits and an upstream update — wrote ${file.to}.new (use --overwrite to take theirs)`);
349
517
  return { kind: "skipped", path: destinationPath };
350
518
  };
351
- const reconcileFile = (file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions = {}) => {
519
+ const reconcileFile = (file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions = {}, useUmbrella = false) => {
352
520
  if (file.merge === "schema-extension") {
353
- return reconcileSchemaExtension(file, itemKey, itemDirectory, projectRoot, logger, reconcileOptions.diff === true);
521
+ return reconcileSchemaExtension(file, itemKey, itemDirectory, projectRoot, logger, reconcileOptions.diff === true, useUmbrella);
354
522
  }
355
- return reconcileWholeFile(file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions);
523
+ return reconcileWholeFile(file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions, useUmbrella);
524
+ };
525
+ const WORKER_ENTRY_FALLBACKS = ["src/server.ts", "src/server/index.ts", "src/server/index.tsx", "src/index.ts", "src/worker.ts"];
526
+ const readWranglerMain = (projectRoot) => {
527
+ const wranglerPath = findWranglerFile(projectRoot);
528
+ if (wranglerPath === void 0) {
529
+ return void 0;
530
+ }
531
+ const { parsed } = readWranglerJsonc(wranglerPath);
532
+ return typeof parsed?.main === "string" ? parsed.main : void 0;
533
+ };
534
+ const findWorkerEntry = (projectRoot) => {
535
+ const main = readWranglerMain(projectRoot);
536
+ const candidates = main === void 0 ? WORKER_ENTRY_FALLBACKS : [main, ...WORKER_ENTRY_FALLBACKS];
537
+ for (const candidate of candidates) {
538
+ const absolute = join(projectRoot, candidate);
539
+ if (!existsSync(absolute)) {
540
+ continue;
541
+ }
542
+ const content = readFileSync(absolute, "utf8");
543
+ if (!content.includes("createShardDO(")) {
544
+ if (candidate === main) {
545
+ break;
546
+ }
547
+ continue;
548
+ }
549
+ return { entryPath: absolute, main: candidate, source: content };
550
+ }
551
+ return void 0;
552
+ };
553
+ const computeRelativeSpecifier = (entryPath, projectRoot, moduleName) => {
554
+ const importPath = relative(dirname(entryPath), join(projectRoot, "lunora", moduleName)).replaceAll("\\", "/");
555
+ return importPath.startsWith(".") ? importPath : `./${importPath}`;
556
+ };
557
+ const logClassAFallback = (entrypointReexports, logger) => {
558
+ for (const reexport of entrypointReexports) {
559
+ const specifier = `./lunora/${reexport.module}.js`;
560
+ const instruction = `Add \`export * from "${specifier}"\` to your worker entry`;
561
+ const suffix = reexport.comment ? ` (${reexport.comment})` : "";
562
+ logger.warn(`${instruction}${suffix}`);
563
+ }
564
+ return 0;
565
+ };
566
+ const buildReexportLines = (entrypointReexports, entryPath, projectRoot, source) => {
567
+ const lines = [];
568
+ for (const reexport of entrypointReexports) {
569
+ const specifier = computeRelativeSpecifier(entryPath, projectRoot, reexport.module);
570
+ const escapedSpecifier = specifier.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
571
+ const existingRe = new RegExp(String.raw`export\s+\*\s+from\s+["']${escapedSpecifier}\.js["']`, "u");
572
+ if (existingRe.test(source)) {
573
+ continue;
574
+ }
575
+ if (reexport.comment) {
576
+ lines.push(`
577
+ // ${reexport.comment}`);
578
+ }
579
+ lines.push(`export * from "${specifier}.js";`);
580
+ }
581
+ return lines;
582
+ };
583
+ const applyEntrypointReexports = (entrypointReexports, projectRoot, logger, diff) => {
584
+ if (entrypointReexports.length === 0) {
585
+ return 0;
586
+ }
587
+ const entry = findWorkerEntry(projectRoot);
588
+ if (entry === void 0) {
589
+ return logClassAFallback(entrypointReexports, logger);
590
+ }
591
+ const linesToAppend = buildReexportLines(entrypointReexports, entry.entryPath, projectRoot, entry.source);
592
+ if (linesToAppend.length === 0) {
593
+ return 0;
594
+ }
595
+ if (diff) {
596
+ for (const line of linesToAppend) {
597
+ if (line !== "") {
598
+ logger.info(`~ entrypoint: ${line}`);
599
+ }
600
+ }
601
+ return linesToAppend.length;
602
+ }
603
+ const separator = entry.source.endsWith("\n") ? "" : "\n";
604
+ writeFileSync(entry.entryPath, `${entry.source}${separator}${linesToAppend.join("\n")}
605
+ `, "utf8");
606
+ logger.success(`wrote ${String(linesToAppend.length)} entrypoint re-export(s) to ${relative(projectRoot, entry.entryPath)}`);
607
+ return linesToAppend.length;
356
608
  };
357
609
  const reconcileItems = (items, cwd, logger, reconcileOptions = {}) => {
358
610
  const written = [];
@@ -360,15 +612,19 @@ const reconcileItems = (items, cwd, logger, reconcileOptions = {}) => {
360
612
  const depsAdded = [];
361
613
  const bindingsApplied = [];
362
614
  const lock = readLock(cwd);
615
+ const useUmbrella = projectUsesUmbrella(cwd);
363
616
  for (const { directory, manifest } of items) {
364
617
  for (const file of manifest.files) {
365
- const outcome = reconcileFile(file, manifest.name, directory, cwd, logger, lock, reconcileOptions);
618
+ const outcome = reconcileFile(file, manifest.name, directory, cwd, logger, lock, reconcileOptions, useUmbrella);
366
619
  (outcome.kind === "written" ? written : skipped).push(outcome.path);
367
620
  }
621
+ if (manifest.entrypointReexports !== void 0) {
622
+ applyEntrypointReexports(manifest.entrypointReexports, cwd, logger, reconcileOptions.diff === true);
623
+ }
368
624
  if (reconcileOptions.diff) {
369
625
  continue;
370
626
  }
371
- const applied = applyItemResources(manifest, cwd, logger);
627
+ const applied = applyItemResources(manifest, cwd, logger, useUmbrella);
372
628
  depsAdded.push(...applied.deps);
373
629
  bindingsApplied.push(...applied.bindings);
374
630
  }
@@ -379,11 +635,11 @@ const reconcileItems = (items, cwd, logger, reconcileOptions = {}) => {
379
635
  };
380
636
 
381
637
  const DEFAULT_SOURCE_BASE = "gh:anolilab/lunora/registry";
382
- const DEFAULT_SOURCE_REF = "alpha";
383
638
  const VALID_ITEM_NAME = /^[A-Za-z0-9][\w-]*$/u;
384
639
  const assertSafeItemName = (name) => {
385
640
  if (!VALID_ITEM_NAME.test(name)) {
386
- throw new Error(
641
+ throw new LunoraError(
642
+ "INTERNAL",
387
643
  `invalid registry item name "${name}" — names must match ${VALID_ITEM_NAME.source} (letters, digits, "-", "_"; no path separators or "..")`
388
644
  );
389
645
  }
@@ -420,29 +676,39 @@ const fetchToStaging = async (remote, label, logger) => {
420
676
  throw error;
421
677
  }
422
678
  };
679
+ const remoteRefCache = /* @__PURE__ */ new WeakMap();
680
+ const resolveRemoteRef = async (options) => {
681
+ const cached = remoteRefCache.get(options);
682
+ if (cached !== void 0) {
683
+ return cached;
684
+ }
685
+ const pending = options.source !== void 0 && options.source.length > 0 ? Promise.resolve(resolveSourceRef(options.ref)) : resolvePinnedSourceRef(options.ref, options.logger);
686
+ remoteRefCache.set(options, pending);
687
+ return pending;
688
+ };
423
689
  const resolveItemDirectory = async (name, options) => {
424
690
  assertSafeItemName(name);
425
691
  if (options.from !== void 0) {
426
692
  const directory = join(options.from, name);
427
693
  if (!existsSync(directory)) {
428
- throw new Error(`registry item not found in local source: ${directory}`);
694
+ throw new LunoraError("INTERNAL", `registry item not found in local source: ${directory}`);
429
695
  }
430
696
  return { cleanup: () => {
431
697
  }, directory };
432
698
  }
433
699
  const base = options.source ?? DEFAULT_SOURCE_BASE;
434
- return fetchToStaging(`${base}/${name}#${DEFAULT_SOURCE_REF}`, "item", options.logger);
700
+ return fetchToStaging(`${base}/${name}#${await resolveRemoteRef(options)}`, "item", options.logger);
435
701
  };
436
702
  const resolveRegistryRoot = async (options) => {
437
703
  if (options.from !== void 0) {
438
704
  if (!existsSync(options.from)) {
439
- throw new Error(`registry root not found: ${options.from}`);
705
+ throw new LunoraError("INTERNAL", `registry root not found: ${options.from}`);
440
706
  }
441
707
  return { cleanup: () => {
442
708
  }, root: options.from };
443
709
  }
444
710
  const base = options.source ?? DEFAULT_SOURCE_BASE;
445
- const { cleanup, directory } = await fetchToStaging(`${base}#${DEFAULT_SOURCE_REF}`, "registry", options.logger);
711
+ const { cleanup, directory } = await fetchToStaging(`${base}#${await resolveRemoteRef(options)}`, "registry", options.logger);
446
712
  return { cleanup, root: directory };
447
713
  };
448
714
  const readManifest = (itemDirectory, name) => {
@@ -459,7 +725,7 @@ const resolvePlan = async (names, options) => {
459
725
  return;
460
726
  }
461
727
  if (inProgress.has(name)) {
462
- throw new Error(`cyclic registry dependency detected at "${name}"`);
728
+ throw new LunoraError("INTERNAL", `cyclic registry dependency detected at "${name}"`);
463
729
  }
464
730
  inProgress.add(name);
465
731
  const { cleanup, directory } = await resolveItemDirectory(name, options);
@@ -488,6 +754,26 @@ const resolvePlan = async (names, options) => {
488
754
  const emptyResult = () => {
489
755
  return { bindings: [], code: 0, deps: [], skipped: [], written: [] };
490
756
  };
757
+ const setBindingField = (manifest, section, match, field, fieldValue) => {
758
+ if (!manifest.bindings) {
759
+ return manifest;
760
+ }
761
+ return {
762
+ ...manifest,
763
+ bindings: manifest.bindings.map((binding) => {
764
+ if (binding.path[0] !== section || !Array.isArray(binding.value)) {
765
+ return binding;
766
+ }
767
+ const entries = binding.value;
768
+ return {
769
+ ...binding,
770
+ value: entries.map(
771
+ (entry) => typeof entry === "object" && entry !== null && entry[match.key] === match.value ? { ...entry, [field]: fieldValue } : entry
772
+ )
773
+ };
774
+ })
775
+ };
776
+ };
491
777
 
492
778
  const printPlan = (logger, manifest) => {
493
779
  const label = manifest.title ?? manifest.description;
@@ -508,6 +794,11 @@ const printPlan = (logger, manifest) => {
508
794
  const valueSuffix = variable.secret ? " (secret)" : ` = ${JSON.stringify(variable.value ?? "")}`;
509
795
  logger.info(` env ${variable.name}${valueSuffix}`);
510
796
  }
797
+ for (const reexport of manifest.entrypointReexports ?? []) {
798
+ const specifier = `./lunora/${reexport.module}`;
799
+ const suffix = reexport.comment ? ` // ${reexport.comment}` : "";
800
+ logger.info(` entry ${specifier}${suffix}`);
801
+ }
511
802
  };
512
803
  const printJsonPlan = (items) => {
513
804
  const planSnapshot = items.map(({ manifest }) => {
@@ -519,6 +810,9 @@ const printJsonPlan = (items) => {
519
810
  }),
520
811
  deps: Object.keys(manifest.deps ?? {}),
521
812
  devDependencies: Object.keys(manifest.devDependencies ?? {}),
813
+ entrypointReexports: (manifest.entrypointReexports ?? []).map((reexport) => {
814
+ return { module: reexport.module, ...reexport.comment ? { comment: reexport.comment } : {} };
815
+ }),
522
816
  envVars: (manifest.envVars ?? []).map((variable) => {
523
817
  return { name: variable.name, ...variable.secret ? { secret: true } : { value: variable.value ?? "" } };
524
818
  }),
@@ -593,8 +887,12 @@ const runAddCommand = async (options) => {
593
887
  }
594
888
  let cleanups = [];
595
889
  try {
596
- const { cleanups: planCleanups, items } = await resolvePlan(options.names, options);
890
+ const { cleanups: planCleanups, items: resolvedItems } = await resolvePlan(options.names, options);
597
891
  cleanups = planCleanups;
892
+ const { transformManifest } = options;
893
+ const items = transformManifest ? resolvedItems.map((item) => {
894
+ return { ...item, manifest: transformManifest(item.manifest) };
895
+ }) : resolvedItems;
598
896
  for (const { manifest } of items) {
599
897
  printPlan(options.logger, manifest);
600
898
  }
@@ -690,4 +988,4 @@ const runBuildIndexCommand = async (options) => {
690
988
  return empty;
691
989
  };
692
990
 
693
- export { runAddCommand, runBuildIndexCommand, runListCommand, runRegistryViewCommand };
991
+ export { runBuildIndexCommand as a, runRegistryViewCommand as b, resolveTagVersions as c, resolveSourceRef as d, resolvePinnedSourceRef as e, resolveDistTag as f, runListCommand as g, runAddCommand as r, setBindingField as s };