@lunora/config 1.0.0-alpha.2 → 1.0.0-alpha.20

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 (23) hide show
  1. package/__assets__/package-og.svg +1 -1
  2. package/dist/index.d.mts +240 -6
  3. package/dist/index.d.ts +240 -6
  4. package/dist/index.mjs +13 -10
  5. package/dist/packem_shared/ACCENT-DW1XJn8i.mjs +40 -0
  6. package/dist/packem_shared/LunoraReporter-Ci-bDCK9.mjs +70 -0
  7. package/dist/packem_shared/{PACKAGE_SECRETS_REGISTRY-CySy5vR_.mjs → PACKAGE_SECRETS_REGISTRY-B8t_SdoZ.mjs} +9 -1
  8. package/dist/packem_shared/{injectRemoteFlags-C-WZAKLY.mjs → REMOTE_ELIGIBLE_KEYS-BC7_e9Bz.mjs} +1 -1
  9. package/dist/packem_shared/{REQUIRED_COMPATIBILITY_DATE-Dd1suoit.mjs → REQUIRED_COMPATIBILITY_DATE-USQQ7NfE.mjs} +80 -2
  10. package/dist/packem_shared/{buildPackageSecretsBlock-S74dgmwy.mjs → buildPackageSecretsBlock-DWDKHViT.mjs} +60 -4
  11. package/dist/packem_shared/{discoverSchemaInfo-DWtypqpP.mjs → discoverSchemaInfo-BB-CKlTK.mjs} +1 -1
  12. package/dist/packem_shared/{inferLunoraBindings-0W3eRdIP.mjs → inferLunoraBindings-b9SJwb2s.mjs} +45 -18
  13. package/dist/packem_shared/{reconcileWranglerBindings-ByJk3yLU.mjs → reconcileWranglerBindings-27af-Qvt.mjs} +56 -4
  14. package/dist/packem_shared/streamContainerLogs-BZ4cOZwH.mjs +157 -0
  15. package/dist/studio-host/index.mjs +4 -4
  16. package/package.json +8 -6
  17. /package/dist/packem_shared/{interpretRemote-CtcIcB5-.mjs → LUNORA_CONFIG_FILE-CtcIcB5-.mjs} +0 -0
  18. /package/dist/packem_shared/{formatLunoraEvent-D2fDeGB6.mjs → LUNORA_EVENT_SOURCE-D2fDeGB6.mjs} +0 -0
  19. /package/dist/packem_shared/{handlePolicyScaffoldRequest-CiC2IGKx.mjs → POLICY_SCAFFOLD_ENDPOINT-CiC2IGKx.mjs} +0 -0
  20. /package/dist/packem_shared/{handleSchemaEditRequest-Df-Wrix-.mjs → SCHEMA_EDIT_ENDPOINT-Df-Wrix-.mjs} +0 -0
  21. /package/dist/packem_shared/{handleSeedRequest-DVCjaGO-.mjs → SEED_ENDPOINT-DVCjaGO-.mjs} +0 -0
  22. /package/dist/packem_shared/{findWranglerFile-DwSuC-Kn.mjs → WRANGLER_FILES-DwSuC-Kn.mjs} +0 -0
  23. /package/dist/packem_shared/{studioAssetsStamp-Csk5RS4E.mjs → loadStudioAssets-Csk5RS4E.mjs} +0 -0
@@ -2,8 +2,9 @@ import { randomBytes } from 'node:crypto';
2
2
  import { existsSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, parseDevVariableEntries, DEV_VARS_NEWLINE, splitDevVariableLine, unquoteDevVariable } from './DEV_VARS_EXAMPLE_FILE-dJPNTEnK.mjs';
5
- import { secretsForPackages } from './PACKAGE_SECRETS_REGISTRY-CySy5vR_.mjs';
5
+ import { CORE_SECRETS, PACKAGE_SECRETS_REGISTRY, secretsForPackages } from './PACKAGE_SECRETS_REGISTRY-B8t_SdoZ.mjs';
6
6
 
7
+ const requiredSecrets = (packageNames) => [...CORE_SECRETS, ...secretsForPackages(packageNames)];
7
8
  const SECRET_BYTES = 32;
8
9
  const SECRET_KEY = /(?:KEY|PASSWORD|SECRET|TOKEN)$/u;
9
10
  const PLACEHOLDER_MARKERS = [
@@ -45,6 +46,11 @@ const isPlaceholderValue = (value) => {
45
46
  const isPlaceholder = (rawValue) => isPlaceholderValue(unquoteDevVariable(rawValue.trim()));
46
47
  const defaultRandomHex = (bytes) => randomBytes(bytes).toString("hex");
47
48
  const generatedSecretFor = (key, rawValue, randomHex) => SECRET_KEY.test(key) && isPlaceholder(rawValue) ? randomHex(SECRET_BYTES) : void 0;
49
+ const PROVIDER_SECRET_KEYS = new Set(
50
+ [...CORE_SECRETS, ...Object.values(PACKAGE_SECRETS_REGISTRY).flat()].filter((entry) => SECRET_KEY.test(entry.key) && entry.placeholderValue.startsWith("<")).map((entry) => entry.key)
51
+ );
52
+ const isMintableSecretKey = (key) => SECRET_KEY.test(key) && !PROVIDER_SECRET_KEYS.has(key);
53
+ const generateSecretValue = (randomHex = defaultRandomHex) => randomHex(SECRET_BYTES);
48
54
  const planDevVariablesScaffold = (input) => {
49
55
  if (input.devVarsExists) {
50
56
  return { status: "exists" };
@@ -155,7 +161,7 @@ const secretEntryBlock = (entry) => {
155
161
  return lines.join("\n");
156
162
  };
157
163
  const buildPackageSecretsBlock = (packageNames, existingKeys) => {
158
- const entries = secretsForPackages(packageNames).filter((entry) => !existingKeys.has(entry.key));
164
+ const entries = requiredSecrets(packageNames).filter((entry) => !existingKeys.has(entry.key));
159
165
  if (entries.length === 0) {
160
166
  return "";
161
167
  }
@@ -181,7 +187,57 @@ ${block}
181
187
  rmSync(temporaryPath, { force: true });
182
188
  throw error;
183
189
  }
184
- return secretsForPackages(packageNames).filter((entry) => !existingKeys.has(entry.key)).map((entry) => entry.key);
190
+ return requiredSecrets(packageNames).filter((entry) => !existingKeys.has(entry.key)).map((entry) => entry.key);
191
+ };
192
+ const planDevSecretsFill = (input) => {
193
+ const randomHex = input.randomHex ?? defaultRandomHex;
194
+ const filledKeys = [];
195
+ const lines = input.existingContent.split(DEV_VARS_NEWLINE).map((line) => {
196
+ const parsed = splitDevVariableLine(line);
197
+ const secret = parsed ? generatedSecretFor(parsed.key, parsed.value, randomHex) : void 0;
198
+ if (!parsed || secret === void 0) {
199
+ return line;
200
+ }
201
+ filledKeys.push(parsed.key);
202
+ return `${parsed.key}="${secret}"`;
203
+ });
204
+ const present = new Set(parseDevVariableEntries(input.existingContent).map((entry) => entry.key));
205
+ const addedKeys = [];
206
+ const additions = [];
207
+ for (const entry of CORE_SECRETS) {
208
+ if (present.has(entry.key)) {
209
+ continue;
210
+ }
211
+ addedKeys.push(entry.key);
212
+ additions.push(`# ${entry.description}`, `${entry.key}="${randomHex(SECRET_BYTES)}"`);
213
+ }
214
+ const body = lines.join("\n");
215
+ if (additions.length === 0) {
216
+ return { addedKeys, content: body, filledKeys };
217
+ }
218
+ const separator = body === "" || body.endsWith("\n") ? "" : "\n";
219
+ return { addedKeys, content: `${body}${separator}${additions.join("\n")}
220
+ `, filledKeys };
221
+ };
222
+ const fillDevSecrets = (deps) => {
223
+ const devVariablesPath = join(deps.cwd, DEV_VARS_FILE);
224
+ const exists = existsSync(devVariablesPath);
225
+ const existingContent = exists ? readFileSync(devVariablesPath, "utf8") : "";
226
+ const plan = planDevSecretsFill({ existingContent, randomHex: deps.randomHex });
227
+ if (plan.filledKeys.length === 0 && plan.addedKeys.length === 0) {
228
+ return { addedKeys: [], filledKeys: [], status: "unchanged" };
229
+ }
230
+ const temporaryPath = `${devVariablesPath}.tmp-${String(process.pid)}`;
231
+ try {
232
+ writeFileSync(temporaryPath, plan.content, { encoding: "utf8", mode: 384 });
233
+ renameSync(temporaryPath, devVariablesPath);
234
+ } catch (error) {
235
+ rmSync(temporaryPath, { force: true });
236
+ throw error;
237
+ }
238
+ const generated = [...plan.filledKeys, ...plan.addedKeys];
239
+ deps.info?.(`Generated ${String(generated.length)} dev secret(s) in ${DEV_VARS_FILE}: ${generated.join(", ")}`);
240
+ return { addedKeys: plan.addedKeys, filledKeys: plan.filledKeys, status: exists ? "filled" : "created" };
185
241
  };
186
242
 
187
- export { buildPackageSecretsBlock, ensureDevVariables, ensureDevVariablesExample as ensureDevVarsExample, isPlaceholderValue, planDevVariablesAugment, planDevVariablesScaffold };
243
+ export { buildPackageSecretsBlock, ensureDevVariables, ensureDevVariablesExample as ensureDevVarsExample, fillDevSecrets, generateSecretValue, isMintableSecretKey, isPlaceholderValue, planDevSecretsFill, planDevVariablesAugment, planDevVariablesScaffold, requiredSecrets };
@@ -10,7 +10,7 @@ const discoverSchemaInfo = (projectRoot, schemaDirectory) => {
10
10
  }
11
11
  try {
12
12
  const project = new Project({ skipAddingFilesFromTsConfig: true, useInMemoryFileSystem: false });
13
- const schema = discoverSchema(project, schemaPath);
13
+ const schema = discoverSchema(project, schemaPath, projectRoot);
14
14
  return {
15
15
  info: {
16
16
  hasGlobalTable: schema.tables.some((table) => table.shardMode === "global"),
@@ -1,10 +1,25 @@
1
1
  import { existsSync, statSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { init, parse } from 'es-module-lexer';
3
3
  import { discoverContainerInfo } from './discoverContainerInfo-BXFs6Wav.mjs';
4
- import { discoverSchemaInfo } from './discoverSchemaInfo-DWtypqpP.mjs';
5
- import { discoverWorkflowInfo } from './discoverWorkflowInfo-CedvR0mn.mjs';
6
- import { WRANGLER_FILES, readWranglerJsonc } from './findWranglerFile-DwSuC-Kn.mjs';
4
+ import { QUEUES_FILENAME, discoverQueues } from '@lunora/codegen';
5
+ import { Project } from 'ts-morph';
7
6
  import { join } from 'node:path';
7
+ import { discoverSchemaInfo } from './discoverSchemaInfo-BB-CKlTK.mjs';
8
+ import { discoverWorkflowInfo } from './discoverWorkflowInfo-CedvR0mn.mjs';
9
+ import { WRANGLER_FILES, readWranglerJsonc } from './WRANGLER_FILES-DwSuC-Kn.mjs';
10
+
11
+ const discoverQueueInfo = (projectRoot, schemaDirectory) => {
12
+ const queuesPath = join(projectRoot, schemaDirectory, QUEUES_FILENAME);
13
+ if (!existsSync(queuesPath)) {
14
+ return { queues: [] };
15
+ }
16
+ try {
17
+ const project = new Project({ skipAddingFilesFromTsConfig: true, useInMemoryFileSystem: false });
18
+ return { queues: discoverQueues(project, join(projectRoot, schemaDirectory)) };
19
+ } catch (error) {
20
+ return { error: error instanceof Error ? error.message : String(error), queues: [] };
21
+ }
22
+ };
8
23
 
9
24
  const SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"]);
10
25
  const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".lunora-cache", ".wrangler", "_generated", "dist", "node_modules"]);
@@ -23,18 +38,23 @@ const TYPE_ONLY_EXPORT_PATTERNS = {
23
38
  };
24
39
  const ENV_DB_PATTERN = /\benv\s*\.\s*DB\b/;
25
40
  const ENV_AI_PATTERN = /\benv\s*\.\s*AI\b/;
41
+ const CTX_PIPELINES_PATTERN = /\bctx\s*\.\s*pipelines\b/;
26
42
  const TYPE_ONLY_IMPORT_PATTERN = /^\s*import\s+type\b/;
27
43
  const CAPABILITY_SOURCES = {
28
44
  usesAi: { pattern: /\bfrom\s+["']@lunora\/ai["']/, source: "@lunora/ai" },
29
- usesAnalytics: { pattern: /\bfrom\s+["']@lunora\/analytics["']/, source: "@lunora/analytics" },
45
+ usesAnalytics: { pattern: /\bfrom\s+["']@lunora\/bindings\/analytics["']/, source: "@lunora/bindings/analytics" },
30
46
  usesAuth: { pattern: /\bfrom\s+["']@lunora\/auth["']/, source: "@lunora/auth" },
31
47
  usesBrowser: { pattern: /\bfrom\s+["']@lunora\/browser["']/, source: "@lunora/browser" },
32
48
  usesHyperdrive: { pattern: /\bfrom\s+["']@lunora\/hyperdrive["']/, source: "@lunora/hyperdrive" },
33
- usesImages: { pattern: /\bfrom\s+["']@lunora\/images["']/, source: "@lunora/images" },
34
- usesKv: { pattern: /\bfrom\s+["']@lunora\/kv["']/, source: "@lunora/kv" },
49
+ usesImages: { pattern: /\bfrom\s+["']@lunora\/bindings\/images["']/, source: "@lunora/bindings/images" },
50
+ usesKv: { pattern: /\bfrom\s+["']@lunora\/bindings\/kv["']/, source: "@lunora/bindings/kv" },
35
51
  usesMail: { pattern: /\bfrom\s+["']@lunora\/mail["']/, source: "@lunora/mail" },
36
52
  usesPayment: { pattern: /\bfrom\s+["']@lunora\/payment["']/, source: "@lunora/payment" },
37
- usesPipelines: { pattern: /\bfrom\s+["']@lunora\/pipelines["']/, source: "@lunora/pipelines" },
53
+ // Keyed off the `ctx.pipelines` access (not an import) — see CTX_PIPELINES_PATTERN.
54
+ // Pipelines is codegen-wired onto ActionCtx, so apps reach it via `ctx.pipelines`
55
+ // rather than importing `@lunora/bindings/pipelines`; `source` names that subpath
56
+ // for the hint message.
57
+ usesPipelines: { pattern: CTX_PIPELINES_PATTERN, source: "@lunora/bindings/pipelines" },
38
58
  usesScheduler: { pattern: /\bfrom\s+["']@lunora\/scheduler["']/, source: "@lunora/scheduler" },
39
59
  usesStorage: { pattern: /\bfrom\s+["']@lunora\/storage["']/, source: "@lunora/storage" }
40
60
  };
@@ -90,7 +110,12 @@ const capabilitiesFromSource = (code) => {
90
110
  } catch {
91
111
  capabilities = regexCapabilities(code);
92
112
  }
93
- return mergeCapabilities(capabilities, { ...NO_CAPABILITIES, needsD1: ENV_DB_PATTERN.test(code), usesAi: ENV_AI_PATTERN.test(code) });
113
+ return mergeCapabilities(capabilities, {
114
+ ...NO_CAPABILITIES,
115
+ needsD1: ENV_DB_PATTERN.test(code),
116
+ usesAi: ENV_AI_PATTERN.test(code),
117
+ usesPipelines: CTX_PIPELINES_PATTERN.test(code)
118
+ });
94
119
  };
95
120
  const collectSourceFiles = (directory, accumulator) => {
96
121
  let entries;
@@ -137,8 +162,8 @@ const detectExportedDurableObjects = (entryPath) => {
137
162
  const code = readFileSync(entryPath, "utf8");
138
163
  let exportedNames;
139
164
  try {
140
- const [, exports$1] = parse(code);
141
- exportedNames = new Set(exports$1.map((entry) => entry.n));
165
+ const [, exports] = parse(code);
166
+ exportedNames = new Set(exports.map((entry) => entry.n));
142
167
  } catch {
143
168
  exportedNames = new Set(DURABLE_OBJECT_CLASSES.filter((className) => new RegExp(String.raw`\bexport\b[^\n;]*\b${className}\b`).test(code)));
144
169
  }
@@ -163,8 +188,8 @@ const detectContainerExports = (entryPath, containers) => {
163
188
  const starReexport = CONTAINERS_STAR_REEXPORT_PATTERN.test(code);
164
189
  let exportedNames;
165
190
  try {
166
- const [, exports$1] = parse(code);
167
- exportedNames = new Set(exports$1.map((entry) => entry.n));
191
+ const [, exports] = parse(code);
192
+ exportedNames = new Set(exports.map((entry) => entry.n));
168
193
  } catch {
169
194
  exportedNames = new Set(
170
195
  containers.map((container) => container.className).filter((className) => new RegExp(String.raw`\bexport\b[^\n;]*\b${className}\b`).test(code))
@@ -188,8 +213,8 @@ const detectWorkflowExports = (entryPath, workflows) => {
188
213
  const starReexport = WORKFLOWS_STAR_REEXPORT_PATTERN.test(code);
189
214
  let exportedNames;
190
215
  try {
191
- const [, exports$1] = parse(code);
192
- exportedNames = new Set(exports$1.map((entry) => entry.n));
216
+ const [, exports] = parse(code);
217
+ exportedNames = new Set(exports.map((entry) => entry.n));
193
218
  } catch {
194
219
  exportedNames = new Set(
195
220
  workflows.map((workflow) => workflow.className).filter((className) => new RegExp(String.raw`\bexport\b[^\n;]*\b${className}\b`).test(code))
@@ -237,14 +262,14 @@ const describeCapabilitySignals = (capabilities, exported) => {
237
262
  // Self-describing bindings: the binding name is the whole config (no remote
238
263
  // id to mint), so reconcile auto-writes them like the DO/D1 bindings.
239
264
  [capabilities.usesBrowser, "browser (@lunora/browser imported) — self-describing { binding: BROWSER }"],
240
- [capabilities.usesImages, "images (@lunora/images imported) — self-describing { binding: IMAGES }"],
241
- [capabilities.usesAnalytics, "analytics_engine_datasets (@lunora/analytics imported) — self-describing { binding: ANALYTICS, dataset }"],
265
+ [capabilities.usesImages, "images (@lunora/bindings/images imported) — self-describing { binding: IMAGES }"],
266
+ [capabilities.usesAnalytics, "analytics_engine_datasets (@lunora/bindings/analytics imported) — self-describing { binding: ANALYTICS, dataset }"],
242
267
  // Hint bindings: each needs a remote resource Lunora can't fabricate (a KV
243
268
  // namespace id, a Hyperdrive id, a Pipelines pipeline name), so they surface
244
269
  // as hints — never an auto-write — exactly like R2's user-defined bucket name.
245
270
  [
246
271
  capabilities.usesKv,
247
- "hint: @lunora/kv is imported; add a kv_namespaces binding ({ binding, id }) and pass env.<BINDING> to createKv() — the namespace id can't be auto-provisioned"
272
+ "hint: @lunora/bindings/kv is imported; add a kv_namespaces binding ({ binding, id }) and pass env.<BINDING> to createKv() — the namespace id can't be auto-provisioned"
248
273
  ],
249
274
  [
250
275
  capabilities.usesHyperdrive,
@@ -252,7 +277,7 @@ const describeCapabilitySignals = (capabilities, exported) => {
252
277
  ],
253
278
  [
254
279
  capabilities.usesPipelines,
255
- "hint: @lunora/pipelines is imported; run 'wrangler pipelines create <name>' and add a 'pipelines' binding ({ binding, pipeline }) — the pipeline resource can't be auto-provisioned"
280
+ "hint: ctx.pipelines is used; run 'wrangler pipelines create <name>' and add a 'pipelines' binding ({ binding, pipeline }) — the pipeline resource can't be auto-provisioned"
256
281
  ]
257
282
  ];
258
283
  return rules.filter(([active]) => active).map(([, signal]) => signal);
@@ -276,6 +301,7 @@ const inferLunoraBindings = async (options) => {
276
301
  const needsD1 = capabilities.needsD1 || schemaNeedsD1(options.projectRoot, schemaDirectory);
277
302
  const containers = detectContainerExports(entryPath, discoverContainerInfo(options.projectRoot, schemaDirectory).containers);
278
303
  const workflows = detectWorkflowExports(entryPath, discoverWorkflowInfo(options.projectRoot, schemaDirectory).workflows);
304
+ const queues = [...discoverQueueInfo(options.projectRoot, schemaDirectory).queues];
279
305
  const capabilityFlags = {};
280
306
  for (const flag of CAPABILITY_FLAGS) {
281
307
  capabilityFlags[flag] = capabilities[flag];
@@ -284,6 +310,7 @@ const inferLunoraBindings = async (options) => {
284
310
  containers,
285
311
  durableObjects,
286
312
  needsD1,
313
+ queues,
287
314
  signals: describeSignals(durableObjects, needsD1, capabilities, containers, workflows),
288
315
  workflows,
289
316
  ...capabilityFlags
@@ -1,7 +1,7 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { containerBuildTag } from '@lunora/container';
3
3
  import { modify, applyEdits } from 'jsonc-parser';
4
- import { findWranglerFile, readWranglerJsonc } from './findWranglerFile-DwSuC-Kn.mjs';
4
+ import { findWranglerFile, readWranglerJsonc } from './WRANGLER_FILES-DwSuC-Kn.mjs';
5
5
 
6
6
  const FORMATTING = { formattingOptions: { insertSpaces: true, tabSize: 4 } };
7
7
  const D1_PLACEHOLDER_ID = "<replace-with-d1-create-id>";
@@ -23,7 +23,7 @@ const collectHintBindingWarnings = (inferred, parsed) => {
23
23
  const rules = [
24
24
  [
25
25
  inferred.usesKv && (parsed?.kv_namespaces?.length ?? 0) === 0,
26
- "@lunora/kv is used but no kv_namespaces binding exists; add a kv_namespaces entry ({ binding, id }) and pass env.<BINDING> to createKv() — the namespace id can't be auto-provisioned."
26
+ "@lunora/bindings/kv is used but no kv_namespaces binding exists; add a kv_namespaces entry ({ binding, id }) and pass env.<BINDING> to createKv() — the namespace id can't be auto-provisioned."
27
27
  ],
28
28
  [
29
29
  inferred.usesHyperdrive && (parsed?.hyperdrive?.length ?? 0) === 0,
@@ -31,7 +31,7 @@ const collectHintBindingWarnings = (inferred, parsed) => {
31
31
  ],
32
32
  [
33
33
  inferred.usesPipelines && (parsed?.pipelines?.length ?? 0) === 0,
34
- "@lunora/pipelines is used but no pipelines binding exists; run 'wrangler pipelines create <name>' and add a 'pipelines' binding ({ binding, pipeline }) — the pipeline resource can't be auto-provisioned."
34
+ "ctx.pipelines is used but no pipelines binding exists; run 'wrangler pipelines create <name>' and add a 'pipelines' binding ({ binding, pipeline }) — the pipeline resource can't be auto-provisioned."
35
35
  ]
36
36
  ];
37
37
  return rules.filter(([active]) => active).map(([, warning]) => warning);
@@ -222,6 +222,57 @@ const reconcileWorkflows = (text, parsed, workflows) => {
222
222
  const nextText = applyModify(text, ["workflows"], [...existing, ...missing.map((workflow) => workflowEntryFor(workflow))]);
223
223
  return { added: missing.map((workflow) => `workflows/${workflow.className}`), text: nextText };
224
224
  };
225
+ const reconcileQueues = (text, parsed, queues) => {
226
+ const existing = parsed.queues ?? {};
227
+ const existingProducers = existing.producers ?? [];
228
+ const existingConsumers = existing.consumers ?? [];
229
+ const haveProducer = new Set(existingProducers.map((entry) => entry.binding));
230
+ const haveConsumer = new Set(existingConsumers.map((entry) => entry.queue));
231
+ const missingProducers = queues.filter((queue) => !haveProducer.has(queue.bindingName));
232
+ const missingConsumers = queues.filter((queue) => !haveConsumer.has(queue.name));
233
+ if (missingProducers.length === 0 && missingConsumers.length === 0) {
234
+ return { added: [], text };
235
+ }
236
+ const nextProducers = [
237
+ ...existingProducers,
238
+ ...missingProducers.map((queue) => {
239
+ return { binding: queue.bindingName, queue: queue.name };
240
+ })
241
+ ];
242
+ const nextConsumers = [
243
+ ...existingConsumers,
244
+ ...missingConsumers.map((queue) => {
245
+ const consumer = { queue: queue.name };
246
+ if (queue.mode === "pull") {
247
+ consumer.type = "http_pull";
248
+ }
249
+ if (queue.tuning.maxBatchSize !== void 0) {
250
+ consumer.max_batch_size = queue.tuning.maxBatchSize;
251
+ }
252
+ if (queue.tuning.maxBatchTimeout !== void 0) {
253
+ consumer.max_batch_timeout = queue.tuning.maxBatchTimeout;
254
+ }
255
+ if (queue.tuning.maxRetries !== void 0) {
256
+ consumer.max_retries = queue.tuning.maxRetries;
257
+ }
258
+ if (queue.tuning.deadLetterQueue !== void 0) {
259
+ consumer.dead_letter_queue = queue.tuning.deadLetterQueue;
260
+ }
261
+ if (queue.tuning.retryDelay !== void 0) {
262
+ consumer.retry_delay = queue.tuning.retryDelay;
263
+ }
264
+ return consumer;
265
+ })
266
+ ];
267
+ const nextText = applyModify(text, ["queues"], { consumers: nextConsumers, producers: nextProducers });
268
+ return {
269
+ added: [
270
+ ...missingProducers.map((queue) => `queues.producers/${queue.bindingName}`),
271
+ ...missingConsumers.map((queue) => `queues.consumers/${queue.name}`)
272
+ ],
273
+ text: nextText
274
+ };
275
+ };
225
276
  const reconcileWranglerBindings = (projectRoot, inferred) => {
226
277
  const wranglerPath = findWranglerFile(projectRoot);
227
278
  const exportGaps = collectExportGaps(inferred);
@@ -250,7 +301,8 @@ const reconcileWranglerBindings = (projectRoot, inferred) => {
250
301
  { enabled: inferred.usesAnalytics, run: (text2) => reconcileAnalytics(text2, parsed) },
251
302
  { enabled: true, run: (text2) => reconcileObservability(text2, parsed) },
252
303
  { enabled: exportedContainers.length > 0, run: (text2) => reconcileContainers(text2, parsed, exportedContainers) },
253
- { enabled: exportedWorkflows.length > 0, run: (text2) => reconcileWorkflows(text2, parsed, exportedWorkflows) }
304
+ { enabled: exportedWorkflows.length > 0, run: (text2) => reconcileWorkflows(text2, parsed, exportedWorkflows) },
305
+ { enabled: inferred.queues.length > 0, run: (text2) => reconcileQueues(text2, parsed, inferred.queues) }
254
306
  ];
255
307
  let text = original;
256
308
  const added = [];
@@ -0,0 +1,157 @@
1
+ import { Writable } from 'node:stream';
2
+ import { StringDecoder } from 'node:string_decoder';
3
+
4
+ const DEV_CONTAINER_IMAGE_PREFIX = "cloudflare-dev/";
5
+ const DEFAULT_POLL_INTERVAL_MS = 1500;
6
+ const createDefaultDocker = async () => {
7
+ const { default: Dockerode } = await import('dockerode');
8
+ return new Dockerode();
9
+ };
10
+ const classFromImage = (image) => {
11
+ if (!image.startsWith(DEV_CONTAINER_IMAGE_PREFIX)) {
12
+ return void 0;
13
+ }
14
+ const [segment] = image.slice(DEV_CONTAINER_IMAGE_PREFIX.length).split(":");
15
+ return segment !== void 0 && segment.length > 0 ? segment : void 0;
16
+ };
17
+ const lineBufferWritable = (emit) => {
18
+ const decoder = new StringDecoder("utf8");
19
+ let buffer = "";
20
+ const flushLine = (line) => {
21
+ emit(line.endsWith("\r") ? line.slice(0, -1) : line);
22
+ };
23
+ return new Writable({
24
+ final(callback) {
25
+ buffer += decoder.end();
26
+ if (buffer.length > 0) {
27
+ flushLine(buffer);
28
+ buffer = "";
29
+ }
30
+ callback();
31
+ },
32
+ write(chunk, _encoding, callback) {
33
+ buffer += decoder.write(chunk);
34
+ const lines = buffer.split("\n");
35
+ buffer = lines.pop() ?? "";
36
+ for (const line of lines) {
37
+ flushLine(line);
38
+ }
39
+ callback();
40
+ }
41
+ });
42
+ };
43
+ const streamContainerLogs = (options) => {
44
+ const classToExport = new Map(options.containers.map((source) => [source.className.toLowerCase(), source.exportName]));
45
+ if (classToExport.size === 0) {
46
+ return { close: () => {
47
+ } };
48
+ }
49
+ const interval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
50
+ const attached = /* @__PURE__ */ new Map();
51
+ let closed = false;
52
+ let unavailableNotified = false;
53
+ let timer;
54
+ let dockerPromise;
55
+ const getDocker = async () => {
56
+ dockerPromise ??= options.docker ? Promise.resolve(options.docker) : createDefaultDocker();
57
+ return dockerPromise;
58
+ };
59
+ const attach = async (docker, id, exportName) => {
60
+ try {
61
+ const stream = await docker.getContainer(id).logs({ follow: true, stderr: true, stdout: true, tail: "all", timestamps: false });
62
+ if (closed) {
63
+ stream.destroy();
64
+ return;
65
+ }
66
+ const stdout = lineBufferWritable((text) => {
67
+ options.onLine({ level: "info", name: exportName, text });
68
+ });
69
+ const stderr = lineBufferWritable((text) => {
70
+ options.onLine({ level: "error", name: exportName, text });
71
+ });
72
+ docker.modem.demuxStream(stream, stdout, stderr);
73
+ stream.on("end", () => {
74
+ stdout.end();
75
+ stderr.end();
76
+ attached.delete(id);
77
+ });
78
+ stream.on("error", () => {
79
+ attached.delete(id);
80
+ });
81
+ attached.set(id, stream);
82
+ } catch {
83
+ }
84
+ };
85
+ const listRunning = async () => {
86
+ try {
87
+ const docker = await getDocker();
88
+ const running = await docker.listContainers({ filters: { status: ["running"] } });
89
+ unavailableNotified = false;
90
+ return running;
91
+ } catch (error) {
92
+ if (!unavailableNotified) {
93
+ unavailableNotified = true;
94
+ options.onUnavailable?.(error instanceof Error ? error.message : String(error));
95
+ }
96
+ return void 0;
97
+ }
98
+ };
99
+ const reconcile = async (docker, running) => {
100
+ const live = /* @__PURE__ */ new Set();
101
+ for (const summary of running) {
102
+ const className = classFromImage(summary.Image);
103
+ const exportName = className === void 0 ? void 0 : classToExport.get(className);
104
+ if (exportName === void 0) {
105
+ continue;
106
+ }
107
+ live.add(summary.Id);
108
+ if (!attached.has(summary.Id)) {
109
+ await attach(docker, summary.Id, exportName);
110
+ }
111
+ }
112
+ for (const [id, stream] of attached) {
113
+ if (!live.has(id)) {
114
+ stream.destroy();
115
+ attached.delete(id);
116
+ }
117
+ }
118
+ };
119
+ const poll = async () => {
120
+ if (closed) {
121
+ return;
122
+ }
123
+ const running = await listRunning();
124
+ if (running === void 0) {
125
+ return;
126
+ }
127
+ await reconcile(await getDocker(), running);
128
+ };
129
+ let polling = false;
130
+ const onTick = () => {
131
+ if (polling || closed) {
132
+ return;
133
+ }
134
+ polling = true;
135
+ poll().finally(() => {
136
+ polling = false;
137
+ }).catch(() => void 0);
138
+ };
139
+ timer = setInterval(onTick, interval);
140
+ timer.unref();
141
+ onTick();
142
+ return {
143
+ close: () => {
144
+ closed = true;
145
+ if (timer) {
146
+ clearInterval(timer);
147
+ timer = void 0;
148
+ }
149
+ for (const [id, stream] of attached) {
150
+ stream.destroy();
151
+ attached.delete(id);
152
+ }
153
+ }
154
+ };
155
+ };
156
+
157
+ export { streamContainerLogs };
@@ -1,7 +1,7 @@
1
1
  export { parseDevVariable, resolveAdminToken } from '../packem_shared/parseDevVariable-CJiq2IwE.mjs';
2
- export { default as loadStudioAssets, studioAssetsStamp } from '../packem_shared/studioAssetsStamp-Csk5RS4E.mjs';
3
- export { POLICY_SCAFFOLD_ENDPOINT, handlePolicyScaffoldRequest } from '../packem_shared/handlePolicyScaffoldRequest-CiC2IGKx.mjs';
2
+ export { default as loadStudioAssets, studioAssetsStamp } from '../packem_shared/loadStudioAssets-Csk5RS4E.mjs';
3
+ export { POLICY_SCAFFOLD_ENDPOINT, handlePolicyScaffoldRequest } from '../packem_shared/POLICY_SCAFFOLD_ENDPOINT-CiC2IGKx.mjs';
4
4
  export { default as renderStudioHtml } from '../packem_shared/renderStudioHtml-449Ysn75.mjs';
5
- export { SCHEMA_EDIT_ENDPOINT, handleSchemaEditRequest } from '../packem_shared/handleSchemaEditRequest-Df-Wrix-.mjs';
6
- export { SEED_ENDPOINT, handleSeedRequest } from '../packem_shared/handleSeedRequest-DVCjaGO-.mjs';
5
+ export { SCHEMA_EDIT_ENDPOINT, handleSchemaEditRequest } from '../packem_shared/SCHEMA_EDIT_ENDPOINT-Df-Wrix-.mjs';
6
+ export { SEED_ENDPOINT, handleSeedRequest } from '../packem_shared/SEED_ENDPOINT-DVCjaGO-.mjs';
7
7
  export { serveJsonHandler } from '../packem_shared/serveJsonHandler-B4OLTGLS.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/config",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.20",
4
4
  "description": "Internal shared CLI + Vite config layer for Lunora: wrangler.jsonc validation, binding inference, and .dev.vars scaffolding",
5
5
  "keywords": [
6
6
  "bindings",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/config"
26
26
  },
27
27
  "files": [
28
- "dist",
28
+ "./dist",
29
29
  "__assets__",
30
30
  "README.md",
31
31
  "LICENSE.md"
@@ -50,15 +50,17 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@lunora/codegen": "1.0.0-alpha.2",
54
- "@lunora/container": "1.0.0-alpha.1",
55
- "@lunora/seed": "1.0.0-alpha.1",
53
+ "@lunora/codegen": "1.0.0-alpha.13",
54
+ "@lunora/container": "1.0.0-alpha.4",
55
+ "@lunora/seed": "1.0.0-alpha.5",
56
+ "@visulima/colorize": "2.0.0-alpha.14",
57
+ "dockerode": "^4.0.12",
56
58
  "es-module-lexer": "^2.1.0",
57
59
  "jsonc-parser": "^3.3.1",
58
60
  "ts-morph": "^28.0.0"
59
61
  },
60
62
  "peerDependencies": {
61
- "@lunora/studio": "1.0.0-alpha.1"
63
+ "@lunora/studio": "1.0.0-alpha.8"
62
64
  },
63
65
  "peerDependenciesMeta": {
64
66
  "@lunora/studio": {