better-cf 0.1.0 → 0.2.2

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.
package/dist/cli/index.js CHANGED
@@ -1,13 +1,54 @@
1
1
  import { Command } from 'commander';
2
2
  import pc from 'picocolors';
3
3
  import { spawn } from 'child_process';
4
- import fs7 from 'fs';
4
+ import fs8 from 'fs';
5
5
  import path3 from 'path';
6
6
  import { Project, SyntaxKind, Node } from 'ts-morph';
7
7
  import { applyEdits, modify, parse } from 'jsonc-parser';
8
8
  import chokidar from 'chokidar';
9
+ import { createInterface } from 'readline/promises';
10
+ import { stdin, stdout } from 'process';
9
11
 
10
12
  // src/cli/index.ts
13
+
14
+ // src/cli/errors.ts
15
+ var CliError = class extends Error {
16
+ code;
17
+ summary;
18
+ file;
19
+ details;
20
+ hint;
21
+ docsUrl;
22
+ constructor(options) {
23
+ super(options.summary);
24
+ this.name = "CliError";
25
+ this.code = options.code;
26
+ this.summary = options.summary;
27
+ this.file = options.file;
28
+ this.details = options.details;
29
+ this.hint = options.hint;
30
+ this.docsUrl = options.docsUrl;
31
+ if (options.cause !== void 0) {
32
+ this.cause = options.cause;
33
+ }
34
+ }
35
+ };
36
+ function toCliError(error) {
37
+ if (error instanceof CliError) {
38
+ return error;
39
+ }
40
+ if (error instanceof Error) {
41
+ return new CliError({
42
+ code: "UNEXPECTED_ERROR",
43
+ summary: error.message,
44
+ details: error.stack
45
+ });
46
+ }
47
+ return new CliError({
48
+ code: "UNEXPECTED_ERROR",
49
+ summary: String(error)
50
+ });
51
+ }
11
52
  function printLine(message) {
12
53
  process.stdout.write(`${message}
13
54
  `);
@@ -38,6 +79,33 @@ var logger = {
38
79
  },
39
80
  item(label, value) {
40
81
  printLine(` -> ${pc.bold(label)}${value ? `: ${value}` : ""}`);
82
+ },
83
+ diagnostic(diag) {
84
+ const levelBadge = diag.level === "error" ? pc.red("[error]") : pc.yellow("[warn]");
85
+ printLine(`${levelBadge} ${pc.bold(diag.code)} ${diag.message}`);
86
+ if (diag.filePath) {
87
+ printLine(` file: ${diag.filePath}`);
88
+ }
89
+ if (diag.hint) {
90
+ printLine(` hint: ${diag.hint}`);
91
+ }
92
+ },
93
+ cliError(payload) {
94
+ printErrLine(pc.red(pc.bold(`
95
+ ERROR ${payload.code}`)));
96
+ printErrLine(pc.red(payload.summary));
97
+ if (payload.file) {
98
+ printErrLine(` file: ${payload.file}`);
99
+ }
100
+ if (payload.details) {
101
+ printErrLine(` details: ${payload.details}`);
102
+ }
103
+ if (payload.hint) {
104
+ printErrLine(` hint: ${payload.hint}`);
105
+ }
106
+ if (payload.docsUrl) {
107
+ printErrLine(` docs: ${payload.docsUrl}`);
108
+ }
41
109
  }
42
110
  };
43
111
  function runCommand(command, args, cwd, stdio = "inherit") {
@@ -51,6 +119,31 @@ function runCommand(command, args, cwd, stdio = "inherit") {
51
119
  child.once("close", (code) => resolve(code ?? 0));
52
120
  });
53
121
  }
122
+ function runCommandCapture(command, args, cwd) {
123
+ return new Promise((resolve, reject) => {
124
+ const child = spawn(command, args, {
125
+ cwd,
126
+ stdio: "pipe",
127
+ env: process.env
128
+ });
129
+ let stdout = "";
130
+ let stderr = "";
131
+ child.stdout?.on("data", (chunk) => {
132
+ stdout += chunk.toString();
133
+ });
134
+ child.stderr?.on("data", (chunk) => {
135
+ stderr += chunk.toString();
136
+ });
137
+ child.once("error", (error) => reject(error));
138
+ child.once("close", (code) => {
139
+ resolve({
140
+ code: code ?? 0,
141
+ stdout,
142
+ stderr
143
+ });
144
+ });
145
+ });
146
+ }
54
147
  function spawnCommand(command, args, cwd) {
55
148
  return spawn(command, args, {
56
149
  cwd,
@@ -58,16 +151,241 @@ function spawnCommand(command, args, cwd) {
58
151
  env: process.env
59
152
  });
60
153
  }
154
+
155
+ // src/cli/commands/admin.ts
156
+ async function runWranglerQueueSubcommand(args, rootDir = process.cwd(), summary = "Wrangler queue command failed.") {
157
+ logger.section(`wrangler ${args.join(" ")}`);
158
+ const code = await runCommand("npx", ["wrangler", ...args], rootDir, "inherit");
159
+ if (code !== 0) {
160
+ throw new CliError({
161
+ code: "WRANGLER_QUEUE_COMMAND_FAILED",
162
+ summary,
163
+ details: `Command "wrangler ${args.join(" ")}" exited with code ${code}.`,
164
+ hint: "Verify wrangler auth/project configuration and command arguments.",
165
+ docsUrl: "https://developers.cloudflare.com/queues/reference/wrangler-commands/"
166
+ });
167
+ }
168
+ }
169
+ async function queueListCommand(rootDir = process.cwd()) {
170
+ await runWranglerQueueSubcommand(["queues", "list"], rootDir, "Failed to list queues.");
171
+ }
172
+ async function queueCreateCommand(name, options = {}, rootDir = process.cwd()) {
173
+ assertWranglerToken("queue name", name);
174
+ const args = ["queues", "create", name];
175
+ pushNumberOption(args, "--delivery-delay-secs", options.deliveryDelaySecs);
176
+ pushNumberOption(args, "--message-retention-period-secs", options.messageRetentionPeriodSecs);
177
+ await runWranglerQueueSubcommand(
178
+ args,
179
+ rootDir,
180
+ `Failed to create queue "${name}".`
181
+ );
182
+ }
183
+ async function queueUpdateCommand(name, options = {}, rootDir = process.cwd()) {
184
+ assertWranglerToken("queue name", name);
185
+ const args = ["queues", "update", name];
186
+ pushNumberOption(args, "--delivery-delay-secs", options.deliveryDelaySecs);
187
+ pushNumberOption(args, "--message-retention-period-secs", options.messageRetentionPeriodSecs);
188
+ await runWranglerQueueSubcommand(
189
+ args,
190
+ rootDir,
191
+ `Failed to update queue "${name}".`
192
+ );
193
+ }
194
+ async function queueDeleteCommand(name, rootDir = process.cwd()) {
195
+ assertWranglerToken("queue name", name);
196
+ await runWranglerQueueSubcommand(
197
+ ["queues", "delete", name],
198
+ rootDir,
199
+ `Failed to delete queue "${name}".`
200
+ );
201
+ }
202
+ async function queueInfoCommand(name, rootDir = process.cwd()) {
203
+ assertWranglerToken("queue name", name);
204
+ await runWranglerQueueSubcommand(["queues", "info", name], rootDir, `Failed to get queue "${name}".`);
205
+ }
206
+ async function queuePauseCommand(name, rootDir = process.cwd()) {
207
+ assertWranglerToken("queue name", name);
208
+ await runWranglerQueueSubcommand(
209
+ ["queues", "pause-delivery", name],
210
+ rootDir,
211
+ `Failed to pause queue "${name}".`
212
+ );
213
+ }
214
+ async function queueResumeCommand(name, rootDir = process.cwd()) {
215
+ assertWranglerToken("queue name", name);
216
+ await runWranglerQueueSubcommand(
217
+ ["queues", "resume-delivery", name],
218
+ rootDir,
219
+ `Failed to resume queue "${name}".`
220
+ );
221
+ }
222
+ async function queuePurgeCommand(name, rootDir = process.cwd()) {
223
+ assertWranglerToken("queue name", name);
224
+ await runWranglerQueueSubcommand(
225
+ ["queues", "purge", name],
226
+ rootDir,
227
+ `Failed to purge queue "${name}".`
228
+ );
229
+ }
230
+ async function queueConsumerHttpAddCommand(queue, options = {}, rootDir = process.cwd()) {
231
+ assertWranglerToken("queue name", queue);
232
+ const args = ["queues", "consumer", "http", "add", queue];
233
+ pushNumberOption(args, "--batch-size", options.batchSize);
234
+ pushNumberOption(args, "--message-retries", options.messageRetries);
235
+ pushStringOption(args, "--dead-letter-queue", options.deadLetterQueue);
236
+ pushNumberOption(args, "--visibility-timeout-secs", options.visibilityTimeoutSecs);
237
+ pushNumberOption(args, "--retry-delay-secs", options.retryDelaySecs);
238
+ await runWranglerQueueSubcommand(
239
+ args,
240
+ rootDir,
241
+ `Failed to add HTTP consumer for queue "${queue}".`
242
+ );
243
+ }
244
+ async function queueConsumerHttpRemoveCommand(queue, rootDir = process.cwd()) {
245
+ assertWranglerToken("queue name", queue);
246
+ await runWranglerQueueSubcommand(
247
+ ["queues", "consumer", "http", "remove", queue],
248
+ rootDir,
249
+ `Failed to remove HTTP consumer for queue "${queue}".`
250
+ );
251
+ }
252
+ async function queueConsumerWorkerAddCommand(queue, script, options = {}, rootDir = process.cwd()) {
253
+ assertWranglerToken("queue name", queue);
254
+ assertWranglerToken("worker script name", script);
255
+ const args = ["queues", "consumer", "worker", "add", queue, script];
256
+ pushNumberOption(args, "--batch-size", options.batchSize);
257
+ pushNumberOption(args, "--batch-timeout", options.batchTimeout);
258
+ pushNumberOption(args, "--message-retries", options.messageRetries);
259
+ pushStringOption(args, "--dead-letter-queue", options.deadLetterQueue);
260
+ pushNumberOption(args, "--max-concurrency", options.maxConcurrency);
261
+ pushNumberOption(args, "--retry-delay-secs", options.retryDelaySecs);
262
+ await runWranglerQueueSubcommand(
263
+ args,
264
+ rootDir,
265
+ `Failed to add worker consumer for queue "${queue}".`
266
+ );
267
+ }
268
+ async function queueConsumerWorkerRemoveCommand(queue, script, rootDir = process.cwd()) {
269
+ assertWranglerToken("queue name", queue);
270
+ assertWranglerToken("worker script name", script);
271
+ await runWranglerQueueSubcommand(
272
+ ["queues", "consumer", "worker", "remove", queue, script],
273
+ rootDir,
274
+ `Failed to remove worker consumer for queue "${queue}".`
275
+ );
276
+ }
277
+ async function subscriptionListCommand(queue, options = {}, rootDir = process.cwd()) {
278
+ assertWranglerToken("queue name", queue);
279
+ const args = ["queues", "subscription", "list", queue];
280
+ pushNumberOption(args, "--page", options.page);
281
+ pushNumberOption(args, "--per-page", options.perPage);
282
+ pushBooleanFlag(args, "--json", options.json);
283
+ await runWranglerQueueSubcommand(
284
+ args,
285
+ rootDir,
286
+ "Failed to list queue subscriptions."
287
+ );
288
+ }
289
+ async function subscriptionCreateCommand(queue, options, rootDir = process.cwd()) {
290
+ assertWranglerToken("queue name", queue);
291
+ assertWranglerToken("subscription source", options.source);
292
+ const args = ["queues", "subscription", "create", queue, "--source", options.source, "--events", options.events];
293
+ pushStringOption(args, "--name", options.name);
294
+ pushBooleanOption(args, "--enabled", options.enabled);
295
+ pushStringOption(args, "--model-name", options.modelName);
296
+ pushStringOption(args, "--worker-name", options.workerName);
297
+ pushStringOption(args, "--workflow-name", options.workflowName);
298
+ await runWranglerQueueSubcommand(
299
+ args,
300
+ rootDir,
301
+ "Failed to create queue subscription."
302
+ );
303
+ }
304
+ async function subscriptionGetCommand(queue, id, options = {}, rootDir = process.cwd()) {
305
+ assertWranglerToken("queue name", queue);
306
+ assertWranglerToken("subscription id", id);
307
+ const args = ["queues", "subscription", "get", queue, "--id", id];
308
+ pushBooleanFlag(args, "--json", options.json);
309
+ await runWranglerQueueSubcommand(
310
+ args,
311
+ rootDir,
312
+ `Failed to get queue subscription "${id}" from "${queue}".`
313
+ );
314
+ }
315
+ async function subscriptionUpdateCommand(queue, id, options, rootDir = process.cwd()) {
316
+ assertWranglerToken("queue name", queue);
317
+ assertWranglerToken("subscription id", id);
318
+ const args = ["queues", "subscription", "update", queue, "--id", id];
319
+ pushStringOption(args, "--name", options.name);
320
+ pushStringOption(args, "--events", options.events);
321
+ pushBooleanOption(args, "--enabled", options.enabled);
322
+ pushBooleanFlag(args, "--json", options.json);
323
+ await runWranglerQueueSubcommand(
324
+ args,
325
+ rootDir,
326
+ `Failed to update queue subscription "${id}" on "${queue}".`
327
+ );
328
+ }
329
+ async function subscriptionDeleteCommand(queue, id, options = {}, rootDir = process.cwd()) {
330
+ assertWranglerToken("queue name", queue);
331
+ assertWranglerToken("subscription id", id);
332
+ const args = ["queues", "subscription", "delete", queue, "--id", id];
333
+ pushBooleanFlag(args, "--force", options.force);
334
+ await runWranglerQueueSubcommand(
335
+ args,
336
+ rootDir,
337
+ `Failed to delete queue subscription "${id}" from "${queue}".`
338
+ );
339
+ }
340
+ function pushStringOption(target, flag, value) {
341
+ if (value && value.length > 0) {
342
+ target.push(flag, value);
343
+ }
344
+ }
345
+ function pushNumberOption(target, flag, value) {
346
+ if (value !== void 0) {
347
+ target.push(flag, String(value));
348
+ }
349
+ }
350
+ function pushBooleanOption(target, flag, value) {
351
+ if (value !== void 0) {
352
+ target.push(flag, String(value));
353
+ }
354
+ }
355
+ function pushBooleanFlag(target, flag, value) {
356
+ if (value) {
357
+ target.push(flag);
358
+ }
359
+ }
360
+ function assertWranglerToken(label, value) {
361
+ if (!value || value.trim().length === 0) {
362
+ throw new CliError({
363
+ code: "INVALID_WRANGLER_ARGUMENT",
364
+ summary: `Invalid ${label}.`,
365
+ details: "Value cannot be empty.",
366
+ hint: `Provide a non-empty ${label}.`
367
+ });
368
+ }
369
+ if (value.startsWith("-")) {
370
+ throw new CliError({
371
+ code: "INVALID_WRANGLER_ARGUMENT",
372
+ summary: `Invalid ${label}.`,
373
+ details: `Value "${value}" starts with "-" and may be interpreted as a CLI flag.`,
374
+ hint: `Use a ${label} that does not start with "-".`
375
+ });
376
+ }
377
+ }
61
378
  var DEFAULT_IGNORE = ["node_modules", ".better-cf", "dist", ".wrangler"];
62
379
  function loadCliConfig(rootDir = process.cwd()) {
63
380
  const defaults = {
64
381
  rootDir,
65
382
  ignore: [...DEFAULT_IGNORE],
66
383
  workerEntry: void 0,
67
- legacyServiceWorker: false
384
+ legacyServiceWorker: false,
385
+ inferEnvTypes: true
68
386
  };
69
387
  const configPath = path3.join(rootDir, "better-cf.config.ts");
70
- if (!fs7.existsSync(configPath)) {
388
+ if (!fs8.existsSync(configPath)) {
71
389
  return defaults;
72
390
  }
73
391
  const project = new Project({
@@ -89,11 +407,13 @@ function loadCliConfig(rootDir = process.cwd()) {
89
407
  const configObject = initializer;
90
408
  const workerEntry = readString(configObject, "workerEntry");
91
409
  const legacyServiceWorker = readBoolean(configObject, "legacyServiceWorker");
410
+ const inferEnvTypes = readBoolean(configObject, "inferEnvTypes");
92
411
  const ignore = readStringArray(configObject, "ignore");
93
412
  return {
94
413
  rootDir,
95
414
  workerEntry,
96
415
  legacyServiceWorker: legacyServiceWorker ?? defaults.legacyServiceWorker,
416
+ inferEnvTypes: inferEnvTypes ?? defaults.inferEnvTypes,
97
417
  ignore: ignore ? [.../* @__PURE__ */ new Set([...defaults.ignore, ...ignore])] : defaults.ignore
98
418
  };
99
419
  }
@@ -153,7 +473,7 @@ function resolveWorkerEntry(config) {
153
473
  ].filter((value) => Boolean(value));
154
474
  for (const candidate of candidates) {
155
475
  const absolutePath = path3.isAbsolute(candidate) ? candidate : path3.join(config.rootDir, candidate);
156
- if (fs7.existsSync(absolutePath)) {
476
+ if (fs8.existsSync(absolutePath)) {
157
477
  return absolutePath;
158
478
  }
159
479
  }
@@ -165,14 +485,14 @@ function resolveWorkerEntry(config) {
165
485
  // src/cli/codegen.ts
166
486
  function generateCode(discovery, config) {
167
487
  const outputDir = path3.join(config.rootDir, ".better-cf");
168
- fs7.mkdirSync(outputDir, { recursive: true });
488
+ fs8.mkdirSync(outputDir, { recursive: true });
169
489
  const workerEntryAbsolute = resolveWorkerEntry(config);
170
490
  const entryContents = renderEntryFile(discovery, workerEntryAbsolute, outputDir, config);
171
491
  const typesContents = renderTypesFile(discovery);
172
492
  const entryPath = path3.join(outputDir, "entry.ts");
173
493
  const typesPath = path3.join(outputDir, "types.d.ts");
174
- fs7.writeFileSync(entryPath, entryContents, "utf8");
175
- fs7.writeFileSync(typesPath, typesContents, "utf8");
494
+ fs8.writeFileSync(entryPath, entryContents, "utf8");
495
+ fs8.writeFileSync(typesPath, typesContents, "utf8");
176
496
  return {
177
497
  entryPath,
178
498
  typesPath
@@ -182,14 +502,18 @@ function renderEntryFile(discovery, workerEntryAbsolute, outDir, config) {
182
502
  const imports = [];
183
503
  const bindings = [];
184
504
  const queueMap = [];
185
- imports.push(`import workerDefault, * as workerModule from '${toImportPath(outDir, workerEntryAbsolute)}';`);
505
+ imports.push(
506
+ `import workerDefault, * as workerModule from ${JSON.stringify(toImportPath(outDir, workerEntryAbsolute))};`
507
+ );
186
508
  imports.push(`import { getQueueInternals, resolveWorkerHandlers } from 'better-cf/queue/internal';`);
187
509
  for (const queue of discovery.queues) {
188
510
  const queueImportPath = toImportPath(outDir, queue.absoluteFilePath);
189
511
  if (queue.isDefaultExport) {
190
- imports.push(`import ${queue.importName} from '${queueImportPath}';`);
512
+ imports.push(`import ${queue.importName} from ${JSON.stringify(queueImportPath)};`);
191
513
  } else {
192
- imports.push(`import { ${queue.exportName} as ${queue.importName} } from '${queueImportPath}';`);
514
+ imports.push(
515
+ `import { ${queue.exportName} as ${queue.importName} } from ${JSON.stringify(queueImportPath)};`
516
+ );
193
517
  }
194
518
  bindings.push(`getQueueInternals(${queue.importName}).setBinding('${queue.bindingName}');`);
195
519
  queueMap.push(` '${queue.queueName}': ${queue.importName}`);
@@ -246,6 +570,8 @@ declare module 'better-cf/queue' {
246
570
  interface BetterCfGeneratedBindings {
247
571
  ${lines.join("\n")}
248
572
  }
573
+
574
+ interface BetterCfAutoEnv extends BetterCfGeneratedBindings {}
249
575
  }
250
576
 
251
577
  export {};
@@ -260,10 +586,10 @@ function toImportPath(fromDir, targetFile) {
260
586
  }
261
587
 
262
588
  // src/cli/discovery/naming.ts
263
- function deriveQueueName(input) {
264
- const withoutSuffix = input.replace(/Queue$/, "");
589
+ function deriveQueueName(input2) {
590
+ const withoutSuffix = input2.replace(/Queue$/, "");
265
591
  const kebab = withoutSuffix.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
266
- return kebab || input.toLowerCase();
592
+ return kebab || input2.toLowerCase();
267
593
  }
268
594
  function deriveBindingName(queueName) {
269
595
  return `QUEUE_${queueName.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").toUpperCase()}`;
@@ -280,8 +606,10 @@ var RESERVED_KEYS = /* @__PURE__ */ new Set([
280
606
  "retry",
281
607
  "retryDelay",
282
608
  "deadLetter",
609
+ "deliveryDelay",
283
610
  "visibilityTimeout",
284
611
  "batch",
612
+ "consumer",
285
613
  "message",
286
614
  "process",
287
615
  "processBatch",
@@ -370,7 +698,7 @@ async function scanQueues(config) {
370
698
  }
371
699
  function createProject(rootDir) {
372
700
  const tsConfigPath = path3.join(rootDir, "tsconfig.json");
373
- if (fs7.existsSync(tsConfigPath)) {
701
+ if (fs8.existsSync(tsConfigPath)) {
374
702
  return new Project({
375
703
  tsConfigFilePath: tsConfigPath,
376
704
  skipAddingFilesFromTsConfig: true
@@ -389,7 +717,7 @@ function collectSourceFiles(rootDir, ignore) {
389
717
  const files = [];
390
718
  const ignoreSet = new Set(ignore);
391
719
  function walk(currentPath) {
392
- const entries = fs7.readdirSync(currentPath, { withFileTypes: true });
720
+ const entries = fs8.readdirSync(currentPath, { withFileTypes: true });
393
721
  for (const entry of entries) {
394
722
  if (entry.name.startsWith(".git")) {
395
723
  continue;
@@ -403,7 +731,7 @@ function collectSourceFiles(rootDir, ignore) {
403
731
  continue;
404
732
  }
405
733
  if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
406
- const content = fs7.readFileSync(absolutePath, "utf8");
734
+ const content = fs8.readFileSync(absolutePath, "utf8");
407
735
  if (content.includes("defineQueue")) {
408
736
  files.push(absolutePath);
409
737
  }
@@ -467,7 +795,8 @@ function extractQueueConfig(call, absolutePath, diagnostics, rootDir) {
467
795
  level: "error",
468
796
  code: "INVALID_PROCESS_MODE",
469
797
  message: "Queue config cannot include both process and processBatch.",
470
- filePath: path3.relative(rootDir, absolutePath)
798
+ filePath: path3.relative(rootDir, absolutePath),
799
+ hint: "Pick exactly one processing mode for worker consumers."
471
800
  });
472
801
  }
473
802
  const retry = readNumberProperty(objectLiteral, "retry", diagnostics, absolutePath, rootDir);
@@ -479,13 +808,23 @@ function extractQueueConfig(call, absolutePath, diagnostics, rootDir) {
479
808
  rootDir
480
809
  );
481
810
  const deadLetter = readStringProperty(objectLiteral, "deadLetter", diagnostics, absolutePath, rootDir);
482
- const visibilityTimeout = readNumberOrStringProperty(
811
+ const deliveryDelay = readNumberOrStringProperty(
812
+ objectLiteral,
813
+ "deliveryDelay",
814
+ diagnostics,
815
+ absolutePath,
816
+ rootDir
817
+ );
818
+ const topLevelVisibilityTimeout = readNumberOrStringProperty(
483
819
  objectLiteral,
484
820
  "visibilityTimeout",
485
821
  diagnostics,
486
822
  absolutePath,
487
823
  rootDir
488
824
  );
825
+ const consumer = readConsumerConfig(objectLiteral, diagnostics, absolutePath, rootDir);
826
+ const consumerType = consumer?.type;
827
+ const visibilityTimeout = consumer?.visibilityTimeout ?? topLevelVisibilityTimeout;
489
828
  const batchObject = getObjectLiteralProperty(objectLiteral, "batch");
490
829
  const batchMaxSize = batchObject ? readNumberProperty(batchObject, "maxSize", diagnostics, absolutePath, rootDir) : void 0;
491
830
  const batchTimeout = batchObject ? readNumberOrStringProperty(batchObject, "timeout", diagnostics, absolutePath, rootDir) : void 0;
@@ -511,14 +850,35 @@ function extractQueueConfig(call, absolutePath, diagnostics, rootDir) {
511
850
  }
512
851
  }
513
852
  }
853
+ const relativeFile = path3.relative(rootDir, absolutePath);
854
+ if (consumerType === "http_pull" && (hasProcess || hasProcessBatch)) {
855
+ diagnostics.push({
856
+ level: "error",
857
+ code: "INVALID_PULL_MODE_HANDLER",
858
+ message: 'Queue with consumer.type="http_pull" cannot include process/processBatch.',
859
+ filePath: relativeFile,
860
+ hint: "Remove process handlers for pull consumers and consume via HTTP pull APIs."
861
+ });
862
+ }
863
+ if (consumerType === "http_pull" && isMultiJob) {
864
+ diagnostics.push({
865
+ level: "error",
866
+ code: "UNSUPPORTED_PULL_MULTIJOB",
867
+ message: 'Multi-job queue mode is not supported when consumer.type="http_pull".',
868
+ filePath: relativeFile,
869
+ hint: "Split jobs into separate queues when using http_pull."
870
+ });
871
+ }
514
872
  return {
515
873
  retry,
516
874
  retryDelay,
517
875
  deadLetter,
876
+ deliveryDelay,
518
877
  batchMaxSize,
519
878
  batchTimeout,
520
879
  maxConcurrency,
521
880
  visibilityTimeout,
881
+ consumerType,
522
882
  hasProcess,
523
883
  hasProcessBatch,
524
884
  isMultiJob
@@ -610,6 +970,36 @@ function readNumberOrStringProperty(objectLiteral, name, diagnostics, absolutePa
610
970
  });
611
971
  return void 0;
612
972
  }
973
+ function readConsumerConfig(objectLiteral, diagnostics, absolutePath, rootDir) {
974
+ const consumerObject = getObjectLiteralProperty(objectLiteral, "consumer");
975
+ if (!consumerObject) {
976
+ return void 0;
977
+ }
978
+ const typeValue = readStringProperty(consumerObject, "type", diagnostics, absolutePath, rootDir);
979
+ if (!typeValue) {
980
+ return void 0;
981
+ }
982
+ if (typeValue !== "worker" && typeValue !== "http_pull") {
983
+ diagnostics.push({
984
+ level: "warning",
985
+ code: "NON_STATIC_CONFIG",
986
+ message: `Unknown consumer.type "${typeValue}" in ${path3.relative(rootDir, absolutePath)}.`,
987
+ filePath: path3.relative(rootDir, absolutePath)
988
+ });
989
+ return void 0;
990
+ }
991
+ const visibilityTimeout = readNumberOrStringProperty(
992
+ consumerObject,
993
+ "visibilityTimeout",
994
+ diagnostics,
995
+ absolutePath,
996
+ rootDir
997
+ );
998
+ return {
999
+ type: typeValue,
1000
+ visibilityTimeout
1001
+ };
1002
+ }
613
1003
  function addConflictDiagnostics(queues, diagnostics) {
614
1004
  const queueNameMap = /* @__PURE__ */ new Map();
615
1005
  const bindingNameMap = /* @__PURE__ */ new Map();
@@ -647,8 +1037,105 @@ function toErrorMessage(value) {
647
1037
  }
648
1038
  return String(value);
649
1039
  }
1040
+ var WRANGLER_ENV_INTERFACE = "BetterCfWranglerEnv";
1041
+ async function generateEnvTypes(config) {
1042
+ const outDir = path3.join(config.rootDir, ".better-cf");
1043
+ fs8.mkdirSync(outDir, { recursive: true });
1044
+ const wranglerEnvPath = path3.join(outDir, "wrangler-env.d.ts");
1045
+ const autoEnvPath = path3.join(outDir, "auto-env.d.ts");
1046
+ const skipWrangler = process.env.BETTER_CF_SKIP_WRANGLER_TYPES === "1" || config.inferEnvTypes === false;
1047
+ if (skipWrangler) {
1048
+ fs8.writeFileSync(
1049
+ wranglerEnvPath,
1050
+ `// Auto-generated fallback when wrangler types is skipped.
1051
+ interface ${WRANGLER_ENV_INTERFACE} {
1052
+ [key: string]: unknown;
1053
+ }
1054
+ `,
1055
+ "utf8"
1056
+ );
1057
+ } else {
1058
+ const command = await runCommandCapture(
1059
+ "npx",
1060
+ ["wrangler", "types", "--path", wranglerEnvPath, "--env-interface", WRANGLER_ENV_INTERFACE],
1061
+ config.rootDir
1062
+ );
1063
+ if (command.code !== 0) {
1064
+ throw new CliError({
1065
+ code: "WRANGLER_TYPES_FAILED",
1066
+ summary: "Failed to infer env types with wrangler.",
1067
+ details: command.stderr || command.stdout || `Exit code ${command.code}`,
1068
+ hint: "Install wrangler >=3.91 and verify wrangler config is valid.",
1069
+ docsUrl: "https://developers.cloudflare.com/workers/wrangler/commands/#types"
1070
+ });
1071
+ }
1072
+ if (!fs8.existsSync(wranglerEnvPath)) {
1073
+ throw new CliError({
1074
+ code: "WRANGLER_TYPES_OUTPUT_MISSING",
1075
+ summary: "Wrangler types command succeeded but output file was not created.",
1076
+ file: wranglerEnvPath,
1077
+ hint: "Check wrangler CLI version and project permissions."
1078
+ });
1079
+ }
1080
+ }
1081
+ fs8.writeFileSync(
1082
+ autoEnvPath,
1083
+ `// Auto-generated by better-cf. Do not edit.
1084
+ /// <reference path="./wrangler-env.d.ts" />
1085
+
1086
+ declare module 'better-cf/queue' {
1087
+ interface BetterCfAutoEnv extends BetterCfGeneratedBindings, ${WRANGLER_ENV_INTERFACE} {}
1088
+ }
1089
+
1090
+ export {};
1091
+ `,
1092
+ "utf8"
1093
+ );
1094
+ return {
1095
+ wranglerEnvPath,
1096
+ autoEnvPath
1097
+ };
1098
+ }
1099
+
1100
+ // src/cli/wrangler/duration.ts
1101
+ function parseDurationSecondsStrict(value, context) {
1102
+ if (typeof value === "number") {
1103
+ if (!Number.isFinite(value) || value < 0) {
1104
+ throw new CliError({
1105
+ code: "INVALID_DURATION",
1106
+ summary: `Invalid duration for ${context}.`,
1107
+ details: `Expected non-negative finite number, received ${String(value)}.`,
1108
+ hint: 'Use values like 30 or "30s", "5m", "1h".'
1109
+ });
1110
+ }
1111
+ return value;
1112
+ }
1113
+ const match = value.match(/^(\d+)(s|m|h)$/);
1114
+ if (!match) {
1115
+ throw new CliError({
1116
+ code: "INVALID_DURATION",
1117
+ summary: `Invalid duration string for ${context}.`,
1118
+ details: `Received "${value}".`,
1119
+ hint: 'Use formats like "30s", "5m", "1h".'
1120
+ });
1121
+ }
1122
+ const amount = Number.parseInt(match[1], 10);
1123
+ const unit = match[2];
1124
+ if (unit === "s") {
1125
+ return amount;
1126
+ }
1127
+ if (unit === "m") {
1128
+ return amount * 60;
1129
+ }
1130
+ return amount * 3600;
1131
+ }
1132
+ function parseDurationMsStrict(value, context) {
1133
+ return parseDurationSecondsStrict(value, context) * 1e3;
1134
+ }
1135
+
1136
+ // src/cli/wrangler/jsonc.ts
650
1137
  function patchJsoncConfig(filePath, discovery) {
651
- let text = fs7.readFileSync(filePath, "utf8");
1138
+ let text = fs8.readFileSync(filePath, "utf8");
652
1139
  text = applyEdits(
653
1140
  text,
654
1141
  modify(text, ["main"], ".better-cf/entry.ts", {
@@ -657,28 +1144,46 @@ function patchJsoncConfig(filePath, discovery) {
657
1144
  );
658
1145
  const producers = discovery.queues.map((queue) => ({
659
1146
  queue: queue.queueName,
660
- binding: queue.bindingName
661
- }));
662
- const consumers = discovery.queues.map((queue) => ({
663
- queue: queue.queueName,
664
- ...queue.config.batchMaxSize !== void 0 ? { max_batch_size: queue.config.batchMaxSize } : {},
665
- ...queue.config.batchTimeout !== void 0 ? { max_batch_timeout: parseDurationSeconds(queue.config.batchTimeout) } : {},
666
- ...queue.config.retry !== void 0 ? { max_retries: queue.config.retry } : {},
667
- ...queue.config.deadLetter !== void 0 ? { dead_letter_queue: queue.config.deadLetter } : {},
668
- ...queue.config.maxConcurrency !== void 0 ? { max_concurrency: queue.config.maxConcurrency } : {},
669
- ...queue.config.retryDelay !== void 0 ? { retry_delay: parseDurationSeconds(queue.config.retryDelay) } : {}
1147
+ binding: queue.bindingName,
1148
+ ...queue.config.deliveryDelay !== void 0 ? { delivery_delay: parseDurationSecondsStrict(queue.config.deliveryDelay, `${queue.queueName}.deliveryDelay`) } : {}
670
1149
  }));
1150
+ const consumers = discovery.queues.map((queue) => {
1151
+ const base = {
1152
+ queue: queue.queueName,
1153
+ ...queue.config.retry !== void 0 ? { max_retries: queue.config.retry } : {},
1154
+ ...queue.config.deadLetter !== void 0 ? { dead_letter_queue: queue.config.deadLetter } : {},
1155
+ ...queue.config.retryDelay !== void 0 ? { retry_delay: parseDurationSecondsStrict(queue.config.retryDelay, `${queue.queueName}.retryDelay`) } : {}
1156
+ };
1157
+ if (queue.config.consumerType === "http_pull") {
1158
+ return {
1159
+ ...base,
1160
+ type: "http_pull",
1161
+ ...queue.config.visibilityTimeout !== void 0 ? {
1162
+ visibility_timeout_ms: parseDurationMsStrict(
1163
+ queue.config.visibilityTimeout,
1164
+ `${queue.queueName}.visibilityTimeout`
1165
+ )
1166
+ } : {}
1167
+ };
1168
+ }
1169
+ return {
1170
+ ...base,
1171
+ ...queue.config.batchMaxSize !== void 0 ? { max_batch_size: queue.config.batchMaxSize } : {},
1172
+ ...queue.config.batchTimeout !== void 0 ? { max_batch_timeout: parseDurationSecondsStrict(queue.config.batchTimeout, `${queue.queueName}.batch.timeout`) } : {},
1173
+ ...queue.config.maxConcurrency !== void 0 ? { max_concurrency: queue.config.maxConcurrency } : {}
1174
+ };
1175
+ });
671
1176
  text = applyEdits(
672
1177
  text,
673
1178
  modify(text, ["queues"], { producers, consumers, better_cf_managed: true }, {
674
1179
  formattingOptions: { insertSpaces: true, tabSize: 2 }
675
1180
  })
676
1181
  );
677
- fs7.writeFileSync(filePath, text, "utf8");
1182
+ fs8.writeFileSync(filePath, text, "utf8");
678
1183
  }
679
1184
  function ensureJsoncExists(rootDir) {
680
1185
  const filePath = path3.join(rootDir, "wrangler.jsonc");
681
- if (!fs7.existsSync(filePath)) {
1186
+ if (!fs8.existsSync(filePath)) {
682
1187
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
683
1188
  const content = `{
684
1189
  "$schema": "node_modules/wrangler/config-schema.json",
@@ -691,37 +1196,19 @@ function ensureJsoncExists(rootDir) {
691
1196
  }
692
1197
  }
693
1198
  `;
694
- fs7.writeFileSync(filePath, content, "utf8");
1199
+ fs8.writeFileSync(filePath, content, "utf8");
695
1200
  } else {
696
- const parsed = parse(fs7.readFileSync(filePath, "utf8"));
1201
+ const parsed = parse(fs8.readFileSync(filePath, "utf8"));
697
1202
  if (!parsed.queues) {
698
1203
  patchJsoncConfig(filePath, { queues: []});
699
1204
  }
700
1205
  }
701
1206
  return filePath;
702
1207
  }
703
- function parseDurationSeconds(value) {
704
- if (typeof value === "number") {
705
- return value;
706
- }
707
- const match = value.match(/^(\d+)(s|m|h)$/);
708
- if (!match) {
709
- return Number(value) || 0;
710
- }
711
- const amount = Number.parseInt(match[1], 10);
712
- const unit = match[2];
713
- if (unit === "s") {
714
- return amount;
715
- }
716
- if (unit === "m") {
717
- return amount * 60;
718
- }
719
- return amount * 3600;
720
- }
721
1208
  var START_MARKER = "# --- better-cf:start ---";
722
1209
  var END_MARKER = "# --- better-cf:end ---";
723
1210
  function patchTomlConfig(filePath, discovery) {
724
- let content = fs7.readFileSync(filePath, "utf8");
1211
+ let content = fs8.readFileSync(filePath, "utf8");
725
1212
  content = ensureMainEntry(content);
726
1213
  const generatedSection = renderQueueSection(discovery);
727
1214
  const startIndex = content.indexOf(START_MARKER);
@@ -740,11 +1227,11 @@ ${generatedSection}
740
1227
  ${END_MARKER}
741
1228
  `;
742
1229
  }
743
- fs7.writeFileSync(filePath, content, "utf8");
1230
+ fs8.writeFileSync(filePath, content, "utf8");
744
1231
  }
745
1232
  function ensureTomlExists(rootDir) {
746
1233
  const filePath = path3.join(rootDir, "wrangler.toml");
747
- if (!fs7.existsSync(filePath)) {
1234
+ if (!fs8.existsSync(filePath)) {
748
1235
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
749
1236
  const initial = `name = "my-worker"
750
1237
  main = ".better-cf/entry.ts"
@@ -753,7 +1240,7 @@ compatibility_date = "${date}"
753
1240
  ${START_MARKER}
754
1241
  ${END_MARKER}
755
1242
  `;
756
- fs7.writeFileSync(filePath, initial, "utf8");
1243
+ fs8.writeFileSync(filePath, initial, "utf8");
757
1244
  }
758
1245
  return filePath;
759
1246
  }
@@ -768,50 +1255,62 @@ function renderQueueSection(discovery) {
768
1255
  const lines = [];
769
1256
  for (const queue of discovery.queues) {
770
1257
  lines.push("[[queues.producers]]");
771
- lines.push(`queue = "${queue.queueName}"`);
772
- lines.push(`binding = "${queue.bindingName}"`);
1258
+ lines.push(`queue = ${toTomlString(queue.queueName)}`);
1259
+ lines.push(`binding = ${toTomlString(queue.bindingName)}`);
1260
+ if (queue.config.deliveryDelay !== void 0) {
1261
+ lines.push(
1262
+ `delivery_delay = ${parseDurationSecondsStrict(
1263
+ queue.config.deliveryDelay,
1264
+ `${queue.queueName}.deliveryDelay`
1265
+ )}`
1266
+ );
1267
+ }
773
1268
  lines.push("");
774
1269
  lines.push("[[queues.consumers]]");
775
- lines.push(`queue = "${queue.queueName}"`);
776
- if (queue.config.batchMaxSize !== void 0) {
777
- lines.push(`max_batch_size = ${queue.config.batchMaxSize}`);
778
- }
779
- if (queue.config.batchTimeout !== void 0) {
780
- lines.push(`max_batch_timeout = ${parseDurationSeconds2(queue.config.batchTimeout)}`);
1270
+ lines.push(`queue = ${toTomlString(queue.queueName)}`);
1271
+ if (queue.config.consumerType === "http_pull") {
1272
+ lines.push(`type = ${toTomlString("http_pull")}`);
1273
+ if (queue.config.visibilityTimeout !== void 0) {
1274
+ lines.push(
1275
+ `visibility_timeout_ms = ${parseDurationMsStrict(
1276
+ queue.config.visibilityTimeout,
1277
+ `${queue.queueName}.visibilityTimeout`
1278
+ )}`
1279
+ );
1280
+ }
1281
+ } else {
1282
+ if (queue.config.batchMaxSize !== void 0) {
1283
+ lines.push(`max_batch_size = ${queue.config.batchMaxSize}`);
1284
+ }
1285
+ if (queue.config.batchTimeout !== void 0) {
1286
+ lines.push(
1287
+ `max_batch_timeout = ${parseDurationSecondsStrict(
1288
+ queue.config.batchTimeout,
1289
+ `${queue.queueName}.batch.timeout`
1290
+ )}`
1291
+ );
1292
+ }
1293
+ if (queue.config.maxConcurrency !== void 0) {
1294
+ lines.push(`max_concurrency = ${queue.config.maxConcurrency}`);
1295
+ }
781
1296
  }
782
1297
  if (queue.config.retry !== void 0) {
783
1298
  lines.push(`max_retries = ${queue.config.retry}`);
784
1299
  }
785
1300
  if (queue.config.deadLetter !== void 0) {
786
- lines.push(`dead_letter_queue = "${queue.config.deadLetter}"`);
787
- }
788
- if (queue.config.maxConcurrency !== void 0) {
789
- lines.push(`max_concurrency = ${queue.config.maxConcurrency}`);
1301
+ lines.push(`dead_letter_queue = ${toTomlString(queue.config.deadLetter)}`);
790
1302
  }
791
1303
  if (queue.config.retryDelay !== void 0) {
792
- lines.push(`retry_delay = ${parseDurationSeconds2(queue.config.retryDelay)}`);
1304
+ lines.push(
1305
+ `retry_delay = ${parseDurationSecondsStrict(queue.config.retryDelay, `${queue.queueName}.retryDelay`)}`
1306
+ );
793
1307
  }
794
1308
  lines.push("");
795
1309
  }
796
1310
  return lines.join("\n").trimEnd();
797
1311
  }
798
- function parseDurationSeconds2(value) {
799
- if (typeof value === "number") {
800
- return value;
801
- }
802
- const match = value.match(/^(\d+)(s|m|h)$/);
803
- if (!match) {
804
- return Number(value) || 0;
805
- }
806
- const amount = Number.parseInt(match[1], 10);
807
- const unit = match[2];
808
- if (unit === "s") {
809
- return amount;
810
- }
811
- if (unit === "m") {
812
- return amount * 60;
813
- }
814
- return amount * 3600;
1312
+ function toTomlString(value) {
1313
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
815
1314
  }
816
1315
 
817
1316
  // src/cli/wrangler/index.ts
@@ -833,11 +1332,11 @@ function detectWranglerConfig(rootDir) {
833
1332
  const preferred = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
834
1333
  for (const fileName of preferred) {
835
1334
  const absolutePath = path3.join(rootDir, fileName);
836
- if (fs7.existsSync(absolutePath)) {
1335
+ if (fs8.existsSync(absolutePath)) {
837
1336
  return absolutePath;
838
1337
  }
839
1338
  }
840
- if (fs7.existsSync(path3.join(rootDir, "package.json")) && fs7.existsSync(path3.join(rootDir, "src"))) {
1339
+ if (fs8.existsSync(path3.join(rootDir, "package.json")) && fs8.existsSync(path3.join(rootDir, "src"))) {
841
1340
  return ensureJsoncExists(rootDir);
842
1341
  }
843
1342
  return void 0;
@@ -848,23 +1347,26 @@ async function runGenerate(rootDir = process.cwd()) {
848
1347
  const config = loadCliConfig(rootDir);
849
1348
  const discovery = await scanQueues(config);
850
1349
  for (const diagnostic of discovery.diagnostics) {
851
- if (diagnostic.level === "error") {
852
- logger.error(diagnostic.message);
853
- } else {
854
- logger.warn(diagnostic.message);
855
- }
1350
+ logger.diagnostic(diagnostic);
856
1351
  }
857
1352
  const hasErrors = discovery.diagnostics.some((diag) => diag.level === "error");
858
1353
  if (hasErrors) {
859
- throw new Error("Queue discovery failed due to configuration errors.");
1354
+ throw new CliError({
1355
+ code: "QUEUE_DISCOVERY_FAILED",
1356
+ summary: "Queue discovery failed due to configuration errors.",
1357
+ hint: "Fix the diagnostics above and re-run `better-cf generate`.",
1358
+ docsUrl: "https://github.com/better-cf/better-cf#errors"
1359
+ });
860
1360
  }
861
1361
  const generated = generateCode(discovery, config);
862
1362
  const wranglerConfigPath = patchWranglerConfig(config, discovery);
1363
+ const envTypeResult = await generateEnvTypes(config);
863
1364
  return {
864
1365
  discovery,
865
1366
  generatedEntryPath: generated.entryPath,
866
1367
  generatedTypesPath: generated.typesPath,
867
- wranglerConfigPath
1368
+ wranglerConfigPath,
1369
+ autoEnvPath: envTypeResult.autoEnvPath
868
1370
  };
869
1371
  }
870
1372
  async function generateCommand(rootDir = process.cwd()) {
@@ -872,6 +1374,7 @@ async function generateCommand(rootDir = process.cwd()) {
872
1374
  logger.success(`Generated ${result.discovery.queues.length} queue(s)`);
873
1375
  logger.item("entry", result.generatedEntryPath);
874
1376
  logger.item("types", result.generatedTypesPath);
1377
+ logger.item("auto-env", result.autoEnvPath);
875
1378
  logger.item("wrangler", result.wranglerConfigPath);
876
1379
  }
877
1380
 
@@ -881,7 +1384,12 @@ async function deployCommand(rootDir = process.cwd()) {
881
1384
  logger.section("Deploying with wrangler");
882
1385
  const code = await runCommand("npx", ["wrangler", "deploy"], rootDir, "inherit");
883
1386
  if (code !== 0) {
884
- throw new Error(`wrangler deploy failed with code ${code}`);
1387
+ throw new CliError({
1388
+ code: "WRANGLER_DEPLOY_FAILED",
1389
+ summary: "wrangler deploy failed.",
1390
+ details: `wrangler exited with code ${code}`,
1391
+ hint: "Run wrangler deploy manually to inspect environment-specific failures."
1392
+ });
885
1393
  }
886
1394
  logger.success("Deployment complete");
887
1395
  logger.section("Active queue bindings");
@@ -889,28 +1397,48 @@ async function deployCommand(rootDir = process.cwd()) {
889
1397
  logger.item(queue.queueName, queue.bindingName);
890
1398
  }
891
1399
  }
1400
+ var WATCH_GLOBS = [
1401
+ "**/*.ts",
1402
+ "**/*.tsx",
1403
+ "better-cf.config.ts",
1404
+ "wrangler.toml",
1405
+ "wrangler.json",
1406
+ "wrangler.jsonc",
1407
+ "tsconfig.json"
1408
+ ];
892
1409
  function createProjectWatcher(rootDir, options) {
893
- const watcher = chokidar.watch(["**/*.ts", "**/*.tsx"], {
1410
+ const watcher = chokidar.watch(WATCH_GLOBS, {
894
1411
  cwd: rootDir,
895
1412
  ignored: options.ignored.map((entry) => `${entry}/**`),
896
- ignoreInitial: true
1413
+ ignoreInitial: true,
1414
+ usePolling: true,
1415
+ interval: 100,
1416
+ awaitWriteFinish: {
1417
+ stabilityThreshold: 150,
1418
+ pollInterval: 25
1419
+ }
897
1420
  });
898
1421
  const handler = async (filePath) => {
899
- if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) {
900
- return;
901
- }
902
1422
  await options.onRelevantChange(filePath);
903
1423
  };
904
1424
  watcher.on("add", handler);
905
1425
  watcher.on("change", handler);
906
1426
  watcher.on("unlink", handler);
1427
+ watcher.on("ready", () => {
1428
+ void options.onRelevantChange("__watcher_ready__");
1429
+ });
907
1430
  return watcher;
908
1431
  }
909
1432
 
910
1433
  // src/cli/commands/dev.ts
911
1434
  async function devCommand(options, rootDir = process.cwd()) {
912
1435
  if (options.remote) {
913
- throw new Error("Cloudflare Queues do not support wrangler dev --remote. Use local mode only.");
1436
+ throw new CliError({
1437
+ code: "REMOTE_QUEUE_DEV_UNSUPPORTED",
1438
+ summary: "Cloudflare Queues do not support wrangler dev --remote.",
1439
+ hint: "Run better-cf dev without --remote for queue consumer development.",
1440
+ docsUrl: "https://developers.cloudflare.com/queues/configuration/local-development/"
1441
+ });
914
1442
  }
915
1443
  let wranglerProcess = null;
916
1444
  let isRebuilding = false;
@@ -936,7 +1464,8 @@ async function devCommand(options, rootDir = process.cwd()) {
936
1464
  await buildAndRestart("initial build");
937
1465
  if (options.watch) {
938
1466
  const watcher = createProjectWatcher(rootDir, {
939
- ignored: ["node_modules", ".better-cf", "dist"],
1467
+ // Ignore Wrangler's local build output to avoid self-triggered rebuild loops.
1468
+ ignored: ["node_modules", ".better-cf", ".wrangler", "dist"],
940
1469
  onRelevantChange: async (filePath) => {
941
1470
  await buildAndRestart(`file changed: ${filePath}`);
942
1471
  }
@@ -955,46 +1484,53 @@ async function devCommand(options, rootDir = process.cwd()) {
955
1484
  }
956
1485
  async function initCommand(rootDir = process.cwd()) {
957
1486
  const configPath = path3.join(rootDir, "better-cf.config.ts");
958
- if (!fs7.existsSync(configPath)) {
959
- fs7.writeFileSync(configPath, defaultConfigTemplate(), "utf8");
1487
+ if (!fs8.existsSync(configPath)) {
1488
+ fs8.writeFileSync(configPath, defaultConfigTemplate(), "utf8");
960
1489
  logger.success("Created better-cf.config.ts");
961
1490
  }
962
1491
  const workerPath = path3.join(rootDir, "worker.ts");
963
1492
  const srcWorkerPath = path3.join(rootDir, "src", "worker.ts");
964
- if (!fs7.existsSync(workerPath) && !fs7.existsSync(srcWorkerPath)) {
965
- fs7.writeFileSync(workerPath, defaultWorkerTemplate(), "utf8");
1493
+ if (!fs8.existsSync(workerPath) && !fs8.existsSync(srcWorkerPath)) {
1494
+ fs8.writeFileSync(workerPath, defaultWorkerTemplate(), "utf8");
966
1495
  logger.success("Created worker.ts");
967
1496
  }
968
1497
  const outputDir = path3.join(rootDir, ".better-cf");
969
- fs7.mkdirSync(outputDir, { recursive: true });
1498
+ fs8.mkdirSync(outputDir, { recursive: true });
970
1499
  const gitignorePath = path3.join(rootDir, ".gitignore");
971
- if (!fs7.existsSync(gitignorePath)) {
972
- fs7.writeFileSync(gitignorePath, ".better-cf/\nnode_modules/\n", "utf8");
1500
+ if (!fs8.existsSync(gitignorePath)) {
1501
+ fs8.writeFileSync(gitignorePath, ".better-cf/\nnode_modules/\n", "utf8");
973
1502
  logger.success("Created .gitignore");
974
1503
  } else {
975
- const existing = fs7.readFileSync(gitignorePath, "utf8");
1504
+ const existing = fs8.readFileSync(gitignorePath, "utf8");
976
1505
  if (!existing.includes(".better-cf/")) {
977
- fs7.appendFileSync(gitignorePath, "\n.better-cf/\n", "utf8");
1506
+ fs8.appendFileSync(gitignorePath, "\n.better-cf/\n", "utf8");
978
1507
  logger.success("Updated .gitignore");
979
1508
  }
980
1509
  }
981
1510
  const packageJsonPath = path3.join(rootDir, "package.json");
982
- if (fs7.existsSync(packageJsonPath)) {
983
- const packageJson = JSON.parse(fs7.readFileSync(packageJsonPath, "utf8"));
984
- packageJson.scripts = packageJson.scripts ?? {};
985
- packageJson.scripts.dev = packageJson.scripts.dev ?? "better-cf dev";
986
- packageJson.scripts.deploy = packageJson.scripts.deploy ?? "better-cf deploy";
987
- packageJson.scripts.generate = "better-cf generate";
988
- fs7.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
1511
+ const packageJsonExists = fs8.existsSync(packageJsonPath);
1512
+ const packageJson = packageJsonExists ? JSON.parse(fs8.readFileSync(packageJsonPath, "utf8")) : {
1513
+ name: derivePackageName(rootDir),
1514
+ version: "0.0.0",
1515
+ private: true,
1516
+ scripts: {}
1517
+ };
1518
+ if (!packageJsonExists) {
1519
+ logger.success("Created package.json");
1520
+ }
1521
+ packageJson.scripts = packageJson.scripts ?? {};
1522
+ packageJson.scripts.dev = packageJson.scripts.dev ?? "better-cf dev";
1523
+ packageJson.scripts.deploy = packageJson.scripts.deploy ?? "better-cf deploy";
1524
+ packageJson.scripts.generate = "better-cf generate";
1525
+ fs8.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
989
1526
  `, "utf8");
990
- logger.success("Updated package.json scripts");
991
- }
1527
+ logger.success("Updated package.json scripts");
992
1528
  const wranglerTomlPath = path3.join(rootDir, "wrangler.toml");
993
1529
  const wranglerJsoncPath = path3.join(rootDir, "wrangler.jsonc");
994
1530
  const wranglerJsonPath = path3.join(rootDir, "wrangler.json");
995
- if (!fs7.existsSync(wranglerTomlPath) && !fs7.existsSync(wranglerJsoncPath) && !fs7.existsSync(wranglerJsonPath)) {
1531
+ if (!fs8.existsSync(wranglerTomlPath) && !fs8.existsSync(wranglerJsoncPath) && !fs8.existsSync(wranglerJsonPath)) {
996
1532
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
997
- fs7.writeFileSync(
1533
+ fs8.writeFileSync(
998
1534
  wranglerTomlPath,
999
1535
  `name = "my-worker"
1000
1536
  main = ".better-cf/entry.ts"
@@ -1009,19 +1545,23 @@ compatibility_date = "${date}"
1009
1545
  }
1010
1546
  logger.info("Next steps: create a queue export and run `better-cf dev`.");
1011
1547
  }
1548
+ function derivePackageName(rootDir) {
1549
+ const raw = path3.basename(path3.resolve(rootDir)).toLowerCase().trim();
1550
+ const normalized = raw.replace(/[^a-z0-9._-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
1551
+ return normalized || "better-cf-app";
1552
+ }
1012
1553
  function defaultConfigTemplate() {
1013
1554
  return `import { createSDK } from 'better-cf/queue';
1014
1555
 
1015
- export type Env = {
1016
- // DB: D1Database;
1017
- };
1018
-
1019
- export const { defineQueue, defineWorker } = createSDK<Env>();
1556
+ // Auto-inferred env types are generated under .better-cf/*.d.ts
1557
+ // You can still switch to createSDK<Env>() when you need explicit overrides.
1558
+ export const { defineQueue, defineWorker } = createSDK();
1020
1559
 
1021
1560
  export const betterCfConfig = {
1022
1561
  // workerEntry: 'worker.ts',
1023
1562
  // ignore: ['coverage'],
1024
1563
  legacyServiceWorker: false,
1564
+ inferEnvTypes: true,
1025
1565
  };
1026
1566
  `;
1027
1567
  }
@@ -1035,11 +1575,231 @@ export default defineWorker({
1035
1575
  });
1036
1576
  `;
1037
1577
  }
1578
+ var DEFAULT_PROJECT_DIR = "better-cf-app";
1579
+ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
1580
+ var INSTALL_PLANS = {
1581
+ npm: {
1582
+ command: "npm",
1583
+ runtimeArgs: ["install", "better-cf", "zod"],
1584
+ devArgs: ["install", "-D", "wrangler", "@cloudflare/workers-types", "typescript"],
1585
+ devRunCommand: "npm run dev"
1586
+ },
1587
+ pnpm: {
1588
+ command: "pnpm",
1589
+ runtimeArgs: ["add", "better-cf", "zod"],
1590
+ devArgs: ["add", "-D", "wrangler", "@cloudflare/workers-types", "typescript"],
1591
+ devRunCommand: "pnpm dev"
1592
+ },
1593
+ yarn: {
1594
+ command: "yarn",
1595
+ runtimeArgs: ["add", "better-cf", "zod"],
1596
+ devArgs: ["add", "-D", "wrangler", "@cloudflare/workers-types", "typescript"],
1597
+ devRunCommand: "yarn dev"
1598
+ },
1599
+ bun: {
1600
+ command: "bun",
1601
+ runtimeArgs: ["add", "better-cf", "zod"],
1602
+ devArgs: ["add", "-d", "wrangler", "@cloudflare/workers-types", "typescript"],
1603
+ devRunCommand: "bun run dev"
1604
+ }
1605
+ };
1606
+ async function createCommand(projectDirectoryArg, options = {}, rootDir = process.cwd()) {
1607
+ const isYes = options.yes ?? false;
1608
+ const shouldInstallByDefault = options.install ?? true;
1609
+ const isInteractive = Boolean(stdin.isTTY && stdout.isTTY && !isYes);
1610
+ const prompts = isInteractive ? createPrompts() : void 0;
1611
+ try {
1612
+ const projectDirectory = await resolveProjectDirectory(projectDirectoryArg, isYes, prompts);
1613
+ const targetDir = path3.resolve(rootDir, projectDirectory);
1614
+ ensureTargetDirectory(targetDir, options.force ?? false);
1615
+ await initCommand(targetDir);
1616
+ const detectedPackageManager = detectPackageManager(rootDir);
1617
+ const packageManager = options.packageManager ?? (prompts ? await prompts.selectPackageManager(detectedPackageManager) : detectedPackageManager);
1618
+ const shouldInstall = shouldInstallByDefault && (prompts ? await prompts.confirmInstall(true) : true);
1619
+ if (shouldInstall) {
1620
+ await installDependencies(packageManager, targetDir);
1621
+ }
1622
+ const relativePath = path3.relative(rootDir, targetDir) || ".";
1623
+ const plan = INSTALL_PLANS[packageManager];
1624
+ logger.section("Project ready");
1625
+ logger.item("path", targetDir);
1626
+ if (relativePath !== ".") {
1627
+ logger.item("cd", `cd ${relativePath}`);
1628
+ }
1629
+ if (!shouldInstall) {
1630
+ logger.item("install", `${plan.command} ${plan.runtimeArgs.join(" ")}`);
1631
+ logger.item("install (dev)", `${plan.command} ${plan.devArgs.join(" ")}`);
1632
+ }
1633
+ logger.item("dev", plan.devRunCommand);
1634
+ } finally {
1635
+ prompts?.close();
1636
+ }
1637
+ }
1638
+ function detectPackageManager(rootDir = process.cwd()) {
1639
+ if (fs8.existsSync(path3.join(rootDir, "pnpm-lock.yaml"))) {
1640
+ return "pnpm";
1641
+ }
1642
+ if (fs8.existsSync(path3.join(rootDir, "yarn.lock"))) {
1643
+ return "yarn";
1644
+ }
1645
+ if (fs8.existsSync(path3.join(rootDir, "bun.lock")) || fs8.existsSync(path3.join(rootDir, "bun.lockb"))) {
1646
+ return "bun";
1647
+ }
1648
+ const userAgent = process.env.npm_config_user_agent?.toLowerCase() ?? "";
1649
+ if (userAgent.startsWith("pnpm")) {
1650
+ return "pnpm";
1651
+ }
1652
+ if (userAgent.startsWith("yarn")) {
1653
+ return "yarn";
1654
+ }
1655
+ if (userAgent.startsWith("bun")) {
1656
+ return "bun";
1657
+ }
1658
+ if (userAgent.startsWith("npm")) {
1659
+ return "npm";
1660
+ }
1661
+ return "npm";
1662
+ }
1663
+ function ensureTargetDirectory(targetDir, force) {
1664
+ if (!fs8.existsSync(targetDir)) {
1665
+ fs8.mkdirSync(targetDir, { recursive: true });
1666
+ return;
1667
+ }
1668
+ const stats = fs8.statSync(targetDir);
1669
+ if (!stats.isDirectory()) {
1670
+ throw new CliError({
1671
+ code: "CREATE_TARGET_INVALID",
1672
+ summary: `Target path exists and is not a directory: ${targetDir}.`,
1673
+ hint: "Choose a different project directory."
1674
+ });
1675
+ }
1676
+ const contents = fs8.readdirSync(targetDir).filter((entry) => entry !== ".DS_Store");
1677
+ if (contents.length > 0 && !force) {
1678
+ throw new CliError({
1679
+ code: "CREATE_TARGET_NOT_EMPTY",
1680
+ summary: `Target directory is not empty: ${targetDir}.`,
1681
+ hint: "Use --force to scaffold in a non-empty directory."
1682
+ });
1683
+ }
1684
+ }
1685
+ async function resolveProjectDirectory(projectDirectoryArg, isYes, prompts) {
1686
+ if (projectDirectoryArg && projectDirectoryArg.trim().length > 0) {
1687
+ return projectDirectoryArg.trim();
1688
+ }
1689
+ if (isYes) {
1690
+ return DEFAULT_PROJECT_DIR;
1691
+ }
1692
+ if (!prompts) {
1693
+ throw new CliError({
1694
+ code: "CREATE_TARGET_REQUIRED",
1695
+ summary: "Project directory is required in non-interactive mode.",
1696
+ hint: "Pass a directory name, for example: `better-cf create my-worker`."
1697
+ });
1698
+ }
1699
+ return prompts.askProjectDirectory(DEFAULT_PROJECT_DIR);
1700
+ }
1701
+ async function installDependencies(packageManager, targetDir) {
1702
+ const plan = INSTALL_PLANS[packageManager];
1703
+ logger.section(`Installing dependencies with ${plan.command}`);
1704
+ await runInstallCommand(plan.command, plan.runtimeArgs, targetDir);
1705
+ await runInstallCommand(plan.command, plan.devArgs, targetDir);
1706
+ }
1707
+ async function runInstallCommand(command, args, cwd) {
1708
+ try {
1709
+ const code = await runCommand(command, args, cwd, "inherit");
1710
+ if (code !== 0) {
1711
+ throw new CliError({
1712
+ code: "DEPENDENCY_INSTALL_FAILED",
1713
+ summary: `Dependency install failed: ${command} ${args.join(" ")}`,
1714
+ details: `Command exited with code ${code}.`,
1715
+ hint: `Run this command manually in ${cwd}.`
1716
+ });
1717
+ }
1718
+ } catch (error) {
1719
+ if (error instanceof CliError) {
1720
+ throw error;
1721
+ }
1722
+ throw new CliError({
1723
+ code: "DEPENDENCY_INSTALL_FAILED",
1724
+ summary: `Dependency install failed: ${command} ${args.join(" ")}`,
1725
+ details: error instanceof Error ? error.message : String(error),
1726
+ hint: `Install dependencies manually in ${cwd}.`
1727
+ });
1728
+ }
1729
+ }
1730
+ function createPrompts() {
1731
+ const rl = createInterface({ input: stdin, output: stdout });
1732
+ return {
1733
+ async askProjectDirectory(defaultValue) {
1734
+ while (true) {
1735
+ const raw = await rl.question(`Project directory (${defaultValue}): `);
1736
+ const value = raw.trim() || defaultValue;
1737
+ if (value.length > 0) {
1738
+ return value;
1739
+ }
1740
+ }
1741
+ },
1742
+ async selectPackageManager(defaultValue) {
1743
+ const defaultIndex = PACKAGE_MANAGERS.indexOf(defaultValue);
1744
+ while (true) {
1745
+ stdout.write("\nSelect a package manager:\n");
1746
+ PACKAGE_MANAGERS.forEach((manager, index) => {
1747
+ stdout.write(` ${index + 1}) ${manager}${manager === defaultValue ? " (default)" : ""}
1748
+ `);
1749
+ });
1750
+ const raw = await rl.question(`Package manager [${defaultIndex + 1}]: `);
1751
+ const normalized = raw.trim().toLowerCase();
1752
+ if (!normalized) {
1753
+ return defaultValue;
1754
+ }
1755
+ if (PACKAGE_MANAGERS.includes(normalized)) {
1756
+ return normalized;
1757
+ }
1758
+ const asNumber = Number(normalized);
1759
+ if (Number.isInteger(asNumber) && asNumber >= 1 && asNumber <= PACKAGE_MANAGERS.length) {
1760
+ return PACKAGE_MANAGERS[asNumber - 1];
1761
+ }
1762
+ logger.warn(`Unsupported package manager input: ${raw}`);
1763
+ }
1764
+ },
1765
+ async confirmInstall(defaultValue) {
1766
+ const hint = defaultValue ? "Y/n" : "y/N";
1767
+ while (true) {
1768
+ const raw = await rl.question(`Install dependencies now? (${hint}): `);
1769
+ const normalized = raw.trim().toLowerCase();
1770
+ if (!normalized) {
1771
+ return defaultValue;
1772
+ }
1773
+ if (normalized === "y" || normalized === "yes") {
1774
+ return true;
1775
+ }
1776
+ if (normalized === "n" || normalized === "no") {
1777
+ return false;
1778
+ }
1779
+ logger.warn(`Unsupported confirmation input: ${raw}`);
1780
+ }
1781
+ },
1782
+ close() {
1783
+ rl.close();
1784
+ }
1785
+ };
1786
+ }
1038
1787
 
1039
1788
  // src/cli/index.ts
1040
1789
  async function run(argv = process.argv.slice(2)) {
1041
1790
  const program = new Command();
1042
- program.name("better-cf").description("better-cf queue SDK CLI").version("0.1.0");
1791
+ program.name("better-cf").description("better-cf queue SDK CLI").version("0.2.1");
1792
+ program.command("create [project-directory]").description("Create a new better-cf project").option("-y, --yes", "Use defaults and skip prompts").option("--no-install", "Skip dependency installation").option("--force", "Allow creating in a non-empty directory").option("--use-npm", "Use npm to install dependencies").option("--use-pnpm", "Use pnpm to install dependencies").option("--use-yarn", "Use yarn to install dependencies").option("--use-bun", "Use bun to install dependencies").action(
1793
+ async (projectDirectory, options) => {
1794
+ const packageManager = resolveCreatePackageManager(options);
1795
+ await createCommand(projectDirectory, {
1796
+ yes: options.yes,
1797
+ install: options.install,
1798
+ force: options.force,
1799
+ packageManager
1800
+ });
1801
+ }
1802
+ );
1043
1803
  program.command("init").description("Initialize better-cf in the current project").action(async () => {
1044
1804
  await initCommand();
1045
1805
  });
@@ -1052,13 +1812,181 @@ async function run(argv = process.argv.slice(2)) {
1052
1812
  program.command("deploy").description("Generate and deploy via wrangler deploy").action(async () => {
1053
1813
  await deployCommand();
1054
1814
  });
1815
+ program.command("queue:list").description("List queues").action(async () => {
1816
+ await queueListCommand();
1817
+ });
1818
+ program.command("queue:create").description("Create a queue").requiredOption("--name <name>", "Queue name").option("--delivery-delay-secs <seconds>", "Default delivery delay in seconds", parseNumberOption).option(
1819
+ "--message-retention-period-secs <seconds>",
1820
+ "Message retention period in seconds",
1821
+ parseNumberOption
1822
+ ).action(
1823
+ async (options) => {
1824
+ await queueCreateCommand(options.name, {
1825
+ deliveryDelaySecs: options.deliveryDelaySecs,
1826
+ messageRetentionPeriodSecs: options.messageRetentionPeriodSecs
1827
+ });
1828
+ }
1829
+ );
1830
+ program.command("queue:update").description("Update a queue").requiredOption("--name <name>", "Queue name").option("--delivery-delay-secs <seconds>", "Default delivery delay in seconds", parseNumberOption).option(
1831
+ "--message-retention-period-secs <seconds>",
1832
+ "Message retention period in seconds",
1833
+ parseNumberOption
1834
+ ).action(
1835
+ async (options) => {
1836
+ await queueUpdateCommand(options.name, {
1837
+ deliveryDelaySecs: options.deliveryDelaySecs,
1838
+ messageRetentionPeriodSecs: options.messageRetentionPeriodSecs
1839
+ });
1840
+ }
1841
+ );
1842
+ program.command("queue:delete").description("Delete a queue").requiredOption("--name <name>", "Queue name").action(async (options) => {
1843
+ await queueDeleteCommand(options.name);
1844
+ });
1845
+ program.command("queue:info").description("Describe a queue").requiredOption("--name <name>", "Queue name").action(async (options) => {
1846
+ await queueInfoCommand(options.name);
1847
+ });
1848
+ program.command("queue:pause").description("Pause queue delivery").requiredOption("--name <name>", "Queue name").action(async (options) => {
1849
+ await queuePauseCommand(options.name);
1850
+ });
1851
+ program.command("queue:resume").description("Resume queue delivery").requiredOption("--name <name>", "Queue name").action(async (options) => {
1852
+ await queueResumeCommand(options.name);
1853
+ });
1854
+ program.command("queue:purge").description("Purge queue messages").requiredOption("--name <name>", "Queue name").action(async (options) => {
1855
+ await queuePurgeCommand(options.name);
1856
+ });
1857
+ program.command("queue:consumer:http:add").description("Add HTTP pull consumer for a queue").requiredOption("--queue <queue>", "Queue name").option("--batch-size <size>", "Batch size", parseNumberOption).option("--message-retries <retries>", "Message retries", parseNumberOption).option("--dead-letter-queue <queue>", "Dead letter queue name").option("--visibility-timeout-secs <seconds>", "Visibility timeout for pull consumer", parseNumberOption).option("--retry-delay-secs <seconds>", "Retry delay in seconds", parseNumberOption).action(
1858
+ async (options) => {
1859
+ await queueConsumerHttpAddCommand(options.queue, {
1860
+ batchSize: options.batchSize,
1861
+ messageRetries: options.messageRetries,
1862
+ deadLetterQueue: options.deadLetterQueue,
1863
+ visibilityTimeoutSecs: options.visibilityTimeoutSecs,
1864
+ retryDelaySecs: options.retryDelaySecs
1865
+ });
1866
+ }
1867
+ );
1868
+ program.command("queue:consumer:http:remove").description("Remove HTTP pull consumer for a queue").requiredOption("--queue <queue>", "Queue name").action(async (options) => {
1869
+ await queueConsumerHttpRemoveCommand(options.queue);
1870
+ });
1871
+ program.command("queue:consumer:worker:add").description("Add worker consumer for a queue").requiredOption("--queue <queue>", "Queue name").requiredOption("--script <script>", "Worker script name").option("--batch-size <size>", "Batch size", parseNumberOption).option("--batch-timeout <seconds>", "Batch timeout in seconds", parseNumberOption).option("--message-retries <retries>", "Message retries", parseNumberOption).option("--dead-letter-queue <queue>", "Dead letter queue name").option("--max-concurrency <count>", "Max consumer concurrency", parseNumberOption).option("--retry-delay-secs <seconds>", "Retry delay in seconds", parseNumberOption).action(
1872
+ async (options) => {
1873
+ await queueConsumerWorkerAddCommand(
1874
+ options.queue,
1875
+ options.script,
1876
+ {
1877
+ batchSize: options.batchSize,
1878
+ batchTimeout: options.batchTimeout,
1879
+ messageRetries: options.messageRetries,
1880
+ deadLetterQueue: options.deadLetterQueue,
1881
+ maxConcurrency: options.maxConcurrency,
1882
+ retryDelaySecs: options.retryDelaySecs
1883
+ }
1884
+ );
1885
+ }
1886
+ );
1887
+ program.command("queue:consumer:worker:remove").description("Remove worker consumer for a queue").requiredOption("--queue <queue>", "Queue name").requiredOption("--script <script>", "Worker script name").action(async (options) => {
1888
+ await queueConsumerWorkerRemoveCommand(options.queue, options.script);
1889
+ });
1890
+ program.command("subscription:list").description("List queue subscriptions").requiredOption("--queue <queue>", "Queue name").option("--page <page>", "Page number", parseNumberOption).option("--per-page <count>", "Results per page", parseNumberOption).option("--json", "JSON output").action(async (options) => {
1891
+ await subscriptionListCommand(
1892
+ options.queue,
1893
+ { page: options.page, perPage: options.perPage, json: options.json }
1894
+ );
1895
+ });
1896
+ program.command("subscription:create").description("Create queue subscription").requiredOption("--queue <queue>", "Queue name").requiredOption("--source <source>", "Source id (same as queue name in most setups)").requiredOption("--events <events>", 'Event list (for example "message.acked")').option("--name <name>", "Subscription name").option("--enabled <enabled>", "Subscription enabled state (true/false)", parseBooleanOption).option("--model-name <modelName>", "AI model name (for AI gateway subscriptions)").option("--worker-name <workerName>", "Worker destination name").option("--workflow-name <workflowName>", "Workflow destination name").action(
1897
+ async (options) => {
1898
+ await subscriptionCreateCommand(options.queue, {
1899
+ source: options.source,
1900
+ events: options.events,
1901
+ name: options.name,
1902
+ enabled: options.enabled,
1903
+ modelName: options.modelName,
1904
+ workerName: options.workerName,
1905
+ workflowName: options.workflowName
1906
+ });
1907
+ }
1908
+ );
1909
+ program.command("subscription:get").description("Get queue subscription").requiredOption("--queue <queue>", "Queue name").requiredOption("--id <id>", "Subscription ID").option("--json", "JSON output").action(async (options) => {
1910
+ await subscriptionGetCommand(options.queue, options.id, { json: options.json });
1911
+ });
1912
+ program.command("subscription:update").description("Update queue subscription destination").requiredOption("--queue <queue>", "Queue name").requiredOption("--id <id>", "Subscription ID").option("--name <name>", "Subscription name").option("--events <events>", "Event list").option("--enabled <enabled>", "Subscription enabled state (true/false)", parseBooleanOption).option("--json", "JSON output").action(
1913
+ async (options) => {
1914
+ await subscriptionUpdateCommand(options.queue, options.id, {
1915
+ name: options.name,
1916
+ events: options.events,
1917
+ enabled: options.enabled,
1918
+ json: options.json
1919
+ });
1920
+ }
1921
+ );
1922
+ program.command("subscription:delete").description("Delete queue subscription").requiredOption("--queue <queue>", "Queue name").requiredOption("--id <id>", "Subscription ID").option("--force", "Skip confirmation where supported").action(async (options) => {
1923
+ await subscriptionDeleteCommand(options.queue, options.id, { force: options.force });
1924
+ });
1055
1925
  try {
1056
1926
  await program.parseAsync(argv, { from: "user" });
1057
1927
  } catch (error) {
1058
- logger.error(error instanceof Error ? error.message : String(error));
1928
+ const cliError = toCliError(error);
1929
+ logger.cliError({
1930
+ code: cliError.code,
1931
+ summary: cliError.summary,
1932
+ file: cliError.file,
1933
+ details: cliError.details,
1934
+ hint: cliError.hint,
1935
+ docsUrl: cliError.docsUrl
1936
+ });
1059
1937
  process.exitCode = 1;
1060
1938
  }
1061
1939
  }
1940
+ function parseNumberOption(value) {
1941
+ const parsed = Number(value);
1942
+ if (!Number.isInteger(parsed) || parsed < 0) {
1943
+ throw new CliError({
1944
+ code: "INVALID_CLI_OPTION",
1945
+ summary: `Invalid numeric option: ${value}.`,
1946
+ details: "Expected a non-negative integer.",
1947
+ hint: "Use values like 0, 1, 30, 120."
1948
+ });
1949
+ }
1950
+ return parsed;
1951
+ }
1952
+ function parseBooleanOption(value) {
1953
+ const normalized = value.toLowerCase();
1954
+ if (normalized === "true") {
1955
+ return true;
1956
+ }
1957
+ if (normalized === "false") {
1958
+ return false;
1959
+ }
1960
+ throw new CliError({
1961
+ code: "INVALID_CLI_OPTION",
1962
+ summary: `Invalid boolean option: ${value}.`,
1963
+ details: "Expected true or false.",
1964
+ hint: "Use --enabled true or --enabled false."
1965
+ });
1966
+ }
1967
+ function resolveCreatePackageManager(options) {
1968
+ const selected = [];
1969
+ if (options.useNpm) {
1970
+ selected.push("npm");
1971
+ }
1972
+ if (options.usePnpm) {
1973
+ selected.push("pnpm");
1974
+ }
1975
+ if (options.useYarn) {
1976
+ selected.push("yarn");
1977
+ }
1978
+ if (options.useBun) {
1979
+ selected.push("bun");
1980
+ }
1981
+ if (selected.length > 1) {
1982
+ throw new CliError({
1983
+ code: "INVALID_CLI_OPTION",
1984
+ summary: "Conflicting package manager flags.",
1985
+ details: "Use only one of --use-npm, --use-pnpm, --use-yarn, or --use-bun."
1986
+ });
1987
+ }
1988
+ return selected[0];
1989
+ }
1062
1990
 
1063
1991
  export { run };
1064
1992
  //# sourceMappingURL=index.js.map