@velajs/cli 1.22.1 → 1.24.0

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/index.js CHANGED
@@ -1,13 +1,47 @@
1
1
  #!/usr/bin/env node
2
- import { defineVelaConfig, loadConfig } from "./config.js";
2
+ import { defineVelaConfig, loadConfig, resolveConfig } from "./config.js";
3
3
  import { t as generateClientContract } from "./client-contract-C7P2btFE.js";
4
- import { Builtins, Cli, Command, Option } from "clipanion";
5
- import { mkdir, readFile, writeFile } from "node:fs/promises";
6
- import { createOpenApiDocument, describeToken, getEntrypointKinds } from "@velajs/vela";
7
- import { dirname, join } from "node:path";
4
+ import { Builtins, Cli, Command, Option, UsageError } from "clipanion";
5
+ import { lstat, mkdir, open, readFile, readdir, rmdir, unlink, writeFile } from "node:fs/promises";
6
+ import { createOpenApiDocument, describeToken, getEntrypointKinds, parseCron, parseCronMetadata } from "@velajs/vela";
7
+ import { dirname, extname, join, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { z } from "zod";
10
- import { runSeeders } from "@velajs/vela/seeder";
10
+ import { SeederRegistry, runSeeders } from "@velajs/vela/seeder";
11
+ import { execFile } from "node:child_process";
12
+ import { createHash } from "node:crypto";
13
+ import { promisify } from "node:util";
14
+ import { parse } from "jsonc-parser";
15
+ import { parse as parse$1 } from "smol-toml";
16
+ //#region package.json
17
+ var version = "1.24.0";
18
+ //#endregion
19
+ //#region src/with-app.ts
20
+ /** Own one app for the whole command, including output and long-lived transports. */
21
+ async function withApp(config, work, warn) {
22
+ const app = await config.createApp();
23
+ const report = (error) => {
24
+ try {
25
+ warn(`Warning: teardown failed: ${String(error)}`);
26
+ } catch {}
27
+ };
28
+ try {
29
+ return await work(app);
30
+ } finally {
31
+ try {
32
+ if (typeof app.dispose === "function") await app.dispose();
33
+ else await app.getContainer().dispose();
34
+ } catch (error) {
35
+ report(error);
36
+ try {
37
+ await app.getContainer().dispose();
38
+ } catch (cleanupError) {
39
+ report(cleanupError);
40
+ }
41
+ }
42
+ }
43
+ }
44
+ //#endregion
11
45
  //#region src/format.ts
12
46
  /**
13
47
  * Render seeder results to a logger and return a process exit code
@@ -145,17 +179,9 @@ var AppCommand = class extends Command {
145
179
  config = Option.String("--config", { description: "Path to the vela config file." });
146
180
  json = Option.Boolean("--json", false, { description: "Emit machine-readable JSON." });
147
181
  async execute() {
148
- const app = await (await loadConfig(process.cwd(), this.config)).createApp();
149
- try {
150
- return await this.run(app);
151
- } finally {
152
- const dispose = app.dispose;
153
- if (typeof dispose === "function") try {
154
- await dispose.call(app);
155
- } catch (error) {
156
- this.context.stderr.write(`Warning: teardown failed: ${String(error)}\n`);
157
- }
158
- }
182
+ return withApp(await loadConfig(process.cwd(), this.config), (app) => this.run(app), (message) => {
183
+ this.context.stderr.write(`${message}\n`);
184
+ });
159
185
  }
160
186
  print(text) {
161
187
  this.context.stdout.write(`${text}\n`);
@@ -258,12 +284,12 @@ var OpenApiDumpCommand = class extends Command {
258
284
  this.context.stderr.write("openapi dump needs the root module. Add it to your vela.config:\n\n export default defineVelaConfig({\n rootModule: AppModule,\n async createApp() { ... },\n });\n");
259
285
  return 1;
260
286
  }
261
- const app = await velaConfig.createApp();
262
- try {
287
+ const rootModule = velaConfig.rootModule;
288
+ return withApp(velaConfig, async (app) => {
263
289
  const info = {};
264
290
  if (this.title) info.title = this.title;
265
291
  if (this.apiVersion) info.version = this.apiVersion;
266
- const document = createOpenApiDocument(velaConfig.rootModule, {
292
+ const document = createOpenApiDocument(rootModule, {
267
293
  globalPrefix: this.globalPrefix ?? app.getGlobalPrefix(),
268
294
  ...Object.keys(info).length > 0 ? { info } : {}
269
295
  });
@@ -273,14 +299,7 @@ var OpenApiDumpCommand = class extends Command {
273
299
  this.context.stdout.write(`Wrote ${this.out}\n`);
274
300
  } else this.context.stdout.write(`${text}\n`);
275
301
  return 0;
276
- } finally {
277
- const dispose = app.dispose;
278
- if (typeof dispose === "function") try {
279
- await dispose.call(app);
280
- } catch (error) {
281
- this.context.stderr.write(`Warning: teardown failed: ${String(error)}\n`);
282
- }
283
- }
302
+ }, (message) => this.context.stderr.write(`${message}\n`));
284
303
  }
285
304
  };
286
305
  //#endregion
@@ -351,10 +370,8 @@ function describeToken$1(app, token) {
351
370
  * introspection as the `route`/`module`/`entrypoint`/`openapi` commands, so an
352
371
  * AI agent can query a Vela app's shape over the Model Context Protocol.
353
372
  *
354
- * Deliberately does NOT extend `AppCommand`: that base disposes the app in its
355
- * `finally` the moment `run()` returns, but an MCP server must stay alive until
356
- * the transport closes. stdout is reserved for JSON-RPC framing; every human
357
- * message goes to stderr.
373
+ * The application lifetime includes the transport's close promise. stdout is
374
+ * reserved for JSON-RPC framing; every human message goes to stderr.
358
375
  */
359
376
  var McpServeCommand = class extends Command {
360
377
  static paths = [["mcp", "serve"]];
@@ -372,10 +389,10 @@ var McpServeCommand = class extends Command {
372
389
  this.context.stderr.write(`${message}\n`);
373
390
  };
374
391
  const velaConfig = await loadConfig(process.cwd(), this.config);
375
- const app = await velaConfig.createApp();
376
392
  const rootModule = velaConfig.rootModule;
377
- try {
378
- const server = new McpServer(await readCliIdentity());
393
+ return withApp(velaConfig, async (app) => {
394
+ const identity = await readCliIdentity();
395
+ const server = new McpServer(identity);
379
396
  server.registerTool("route_list", {
380
397
  description: "The app's HTTP route table: framework-composed controller routes (method, full path, Controller#handler) plus everything else mounted on the router, labeled (mounted). Empty when the app builds no HTTP routes.",
381
398
  inputSchema: {}
@@ -431,14 +448,7 @@ var McpServeCommand = class extends Command {
431
448
  log(`vela mcp serve — ready (5 tools${rootModule ? " + vela://openapi resource" : ""}). Awaiting client on stdio; stdout is JSON-RPC only.`);
432
449
  await closed;
433
450
  return 0;
434
- } finally {
435
- const dispose = app.dispose;
436
- if (typeof dispose === "function") try {
437
- await dispose.call(app);
438
- } catch (error) {
439
- this.context.stderr.write(`Warning: teardown failed: ${String(error)}\n`);
440
- }
441
- }
451
+ }, log);
442
452
  }
443
453
  };
444
454
  //#endregion
@@ -450,22 +460,44 @@ var SeedCommand = class extends Command {
450
460
  category: "Database",
451
461
  description: "Run database seeders for the Vela app.",
452
462
  details: "Loads vela.config.{js,mjs,ts}, builds the app, and runs all @Seeder() classes in order.",
453
- examples: [["Run all seeders", "vela db seed"], ["Use a specific config", "vela db seed --config ./config/vela.config.js"]]
463
+ examples: [
464
+ ["Run all seeders", "vela db seed"],
465
+ ["Use a specific config", "vela db seed --config ./config/vela.config.js"],
466
+ ["List seeders and their module owners", "vela db seed --list --json"]
467
+ ]
454
468
  });
455
469
  config = Option.String("--config", { description: "Path to the vela config file." });
456
470
  continueOnError = Option.Boolean("--continue-on-error", false, { description: "Run all seeders even if one fails." });
471
+ list = Option.Boolean("--list", false, { description: "List registered seeders and their owners without running them." });
472
+ json = Option.Boolean("--json", false, { description: "Emit --list inventory as JSON." });
457
473
  async execute() {
458
- const { createApp } = await loadConfig(process.cwd(), this.config);
459
- const app = await createApp();
460
- this.context.stdout.write("Running seeders…\n");
461
- const code = formatSeedResults(await runSeeders(app, { stopOnError: !this.continueOnError }), (message) => this.context.stdout.write(`${message}\n`));
462
- const dispose = app.dispose;
463
- if (typeof dispose === "function") try {
464
- await dispose.call(app);
465
- } catch (error) {
466
- this.context.stderr.write(`Warning: teardown failed after seeding: ${String(error)}\n`);
467
- }
468
- return code;
474
+ if (this.json && !this.list) throw new UsageError("--json requires --list.");
475
+ if (this.list && this.continueOnError) throw new UsageError("--list cannot be combined with --continue-on-error.");
476
+ return withApp(await loadConfig(process.cwd(), this.config), async (app) => {
477
+ if (this.list) {
478
+ const inventory = app.get(SeederRegistry).list().map((seeder) => ({
479
+ name: seeder.name,
480
+ order: seeder.order,
481
+ target: describeToken(seeder.target),
482
+ moduleId: seeder.moduleId ?? null
483
+ }));
484
+ const output = this.json ? JSON.stringify(inventory, null, 2) : renderTable([
485
+ "ORDER",
486
+ "NAME",
487
+ "MODULE",
488
+ "TARGET"
489
+ ], inventory.map((entry) => [
490
+ String(entry.order),
491
+ entry.name,
492
+ entry.moduleId ?? "(unknown)",
493
+ entry.target
494
+ ])).join("\n");
495
+ this.context.stdout.write(`${output}\n`);
496
+ return 0;
497
+ }
498
+ this.context.stdout.write("Running seeders…\n");
499
+ return formatSeedResults(await runSeeders(app, { stopOnError: !this.continueOnError }), (message) => this.context.stdout.write(`${message}\n`));
500
+ }, (message) => this.context.stderr.write(`${message}\n`));
469
501
  }
470
502
  };
471
503
  //#endregion
@@ -479,7 +511,7 @@ var SeedCommand = class extends Command {
479
511
  const HOST_PACKAGE = "@velajs/studio-host";
480
512
  /** True for a failed dynamic `import()` of a missing module (ESM or CJS code). */
481
513
  function isModuleNotFound(error, specifier) {
482
- const code = error.code;
514
+ const code = error !== null && typeof error === "object" && "code" in error ? error.code : void 0;
483
515
  if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return true;
484
516
  return (error instanceof Error ? error.message : "").includes(specifier);
485
517
  }
@@ -517,9 +549,9 @@ var StudioCommand = class extends Command {
517
549
  }
518
550
  let port;
519
551
  if (this.port !== void 0) {
520
- port = Number.parseInt(this.port, 10);
521
- if (Number.isNaN(port) || port < 0 || port > 65535) {
522
- this.context.stderr.write(`vela studio: --port must be a number 0-65535, got: ${this.port}\n`);
552
+ port = Number(this.port);
553
+ if (!/^\d+$/.test(this.port) || !Number.isInteger(port) || port < 0 || port > 65535) {
554
+ this.context.stderr.write(`vela studio: --port must be a decimal integer 0-65535, got: ${this.port}\n`);
523
555
  return 1;
524
556
  }
525
557
  }
@@ -579,7 +611,7 @@ var ClientGenerateCommand = class extends Command {
579
611
  async execute() {
580
612
  if (this.input && this.config) throw new Error("Use either --input or --config, not both.");
581
613
  if (this.check && !this.out) throw new Error("--check requires --out.");
582
- const document = this.input ? await this.readDocument(this.input) : await this.fromApp();
614
+ const document = this.input ? await this.#readDocument(this.input) : await this.#fromApp();
583
615
  const { source, warnings } = generateClientContract(document);
584
616
  for (const warning of warnings) this.context.stderr.write(`Warning: ${warning}\n`);
585
617
  if (this.strict && warnings.length) return 1;
@@ -601,27 +633,517 @@ var ClientGenerateCommand = class extends Command {
601
633
  } else this.context.stdout.write(source);
602
634
  return 0;
603
635
  }
604
- async readDocument(file) {
636
+ async #readDocument(file) {
605
637
  return JSON.parse(await readFile(file, "utf8"));
606
638
  }
607
- async fromApp() {
639
+ async #fromApp() {
608
640
  const config = await loadConfig(process.cwd(), this.config);
609
641
  if (!config.rootModule) throw new Error("client generate needs rootModule in vela.config, or pass --input openapi.json.");
610
- const app = await config.createApp();
611
- try {
612
- const document = createOpenApiDocument(config.rootModule, { globalPrefix: app.getGlobalPrefix() });
642
+ const rootModule = config.rootModule;
643
+ return withApp(config, async (app) => {
644
+ const document = createOpenApiDocument(rootModule, { globalPrefix: app.getGlobalPrefix() });
613
645
  for (const route of app.describeRoutes()) {
614
646
  const path = route.path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
615
647
  const item = document.paths[path];
616
648
  if (!item || !Object.hasOwn(item, route.method.toLowerCase())) throw new Error(`OpenAPI is missing ${route.method} ${route.path}. Update Vela or pass a complete document with --input.`);
617
649
  }
618
650
  return document;
619
- } finally {
651
+ }, (message) => this.context.stderr.write(`${message}\n`));
652
+ }
653
+ };
654
+ //#endregion
655
+ //#region src/new-project.ts
656
+ const template = new URL("../templates/worker/", import.meta.url);
657
+ const files = [
658
+ "package.json",
659
+ "pnpm-workspace.yaml",
660
+ "tsconfig.json",
661
+ ".swcrc",
662
+ "wrangler.jsonc",
663
+ "vela.config.mjs",
664
+ "gitignore",
665
+ "README.md",
666
+ "src/worker.ts",
667
+ "src/app.module.ts",
668
+ "src/app.controller.ts",
669
+ "src/app.service.ts"
670
+ ];
671
+ function hasCode(error, code) {
672
+ return error instanceof Error && "code" in error && error.code === code;
673
+ }
674
+ async function createProject(name, cwd) {
675
+ if (name.length > 63 || name !== name.trim() || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(name) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/.test(name)) throw new UsageError("Use a project name of at most 63 lowercase letters, digits, and single hyphens, starting with a letter. Paths and reserved device names are not supported.");
676
+ const contents = await Promise.all(files.map(async (file) => ({
677
+ file: file === "gitignore" ? ".gitignore" : file,
678
+ content: (await readFile(new URL(file, template), "utf8")).replaceAll("__PROJECT_NAME__", name)
679
+ })));
680
+ const destination = join(cwd, name);
681
+ const directories = [];
682
+ const written = [];
683
+ try {
684
+ try {
685
+ await mkdir(destination);
686
+ directories.push(destination);
687
+ } catch (error) {
688
+ if (!hasCode(error, "EEXIST")) throw error;
689
+ const stat = await lstat(destination);
690
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new UsageError(`Destination is not a regular directory: ${destination}`);
691
+ if ((await readdir(destination)).length) throw new UsageError(`Destination is not empty: ${destination}. Choose a new project name.`);
692
+ }
693
+ const source = join(destination, "src");
694
+ await mkdir(source);
695
+ directories.push(source);
696
+ for (const { file, content } of contents) {
697
+ const path = join(destination, file);
698
+ const handle = await open(path, "wx");
699
+ written.push(path);
620
700
  try {
621
- await app.dispose();
622
- } catch (error) {
623
- this.context.stderr.write(`Warning: teardown failed: ${String(error)}\n`);
701
+ await writeFile(handle, content, "utf8");
702
+ } finally {
703
+ await handle.close();
704
+ }
705
+ }
706
+ } catch (error) {
707
+ for (const path of written.toReversed()) await unlink(path).catch(() => {});
708
+ for (const path of directories.toReversed()) await rmdir(path).catch(() => {});
709
+ throw error;
710
+ }
711
+ return destination;
712
+ }
713
+ //#endregion
714
+ //#region src/commands/new.command.ts
715
+ var NewCommand = class extends Command {
716
+ static paths = [["new"]];
717
+ static usage = Command.Usage({
718
+ category: "Project",
719
+ description: "Create a minimal Vela application for Cloudflare Workers.",
720
+ details: "Creates a directory in the current working directory. An existing directory must be empty. Dependencies are installed separately with pnpm install.",
721
+ examples: [["Create an API", "vela new my-api"]]
722
+ });
723
+ name = Option.String({
724
+ name: "name",
725
+ required: true
726
+ });
727
+ async execute() {
728
+ await createProject(this.name, process.cwd());
729
+ this.context.stdout.write(`Created ${this.name}.\n\nNext steps:\n cd ${this.name}\n pnpm install\n pnpm typecheck\n pnpm build\n pnpm dev\n\nThen visit http://localhost:8787 or run: curl http://localhost:8787\n`);
730
+ return 0;
731
+ }
732
+ };
733
+ //#endregion
734
+ //#region src/commands/doctor.command.ts
735
+ /** Read only app-owned description APIs; never resolve providers or serialize their values. */
736
+ function describeApplication(app) {
737
+ return {
738
+ globalPrefix: app.getGlobalPrefix(),
739
+ modules: collectModules(app),
740
+ routes: collectRoutes(app),
741
+ entrypoints: app.entrypoints.kinds().flatMap((kind) => app.entrypoints.ofKind(kind).map((entry) => ({
742
+ kind,
743
+ target: `${describeToken(entry.token)}${entry.methodName === void 0 ? "" : `#${String(entry.methodName)}`}`,
744
+ ..."moduleId" in entry && typeof entry.moduleId === "string" ? { moduleId: entry.moduleId } : {}
745
+ })))
746
+ };
747
+ }
748
+ var DoctorCommand = class extends Command {
749
+ static paths = [["doctor"]];
750
+ static usage = Command.Usage({
751
+ category: "Introspection",
752
+ description: "Explain config resolution and optionally inspect the application graph.",
753
+ details: "Checks config file resolution without importing it. --app opts into config import and application bootstrap, then reads app-local module, route and entrypoint snapshots and disposes the app. No files are written, providers are not resolved by the snapshot, and entrypoint metadata is omitted.",
754
+ examples: [["Explain config selection", "vela doctor --json"], ["Inspect a built app", "vela doctor --app --config vela.config.mjs --json"]]
755
+ });
756
+ config = Option.String("--config", { description: "Path to the Vela config file." });
757
+ app = Option.Boolean("--app", false, { description: "Import config, bootstrap the app and inspect its graph." });
758
+ json = Option.Boolean("--json", false, { description: "Emit machine-readable diagnostics." });
759
+ async execute() {
760
+ const report = {
761
+ schemaVersion: 1,
762
+ cwd: process.cwd(),
763
+ nodeVersion: process.versions.node,
764
+ config: null,
765
+ issues: []
766
+ };
767
+ try {
768
+ report.config = await resolveConfig(report.cwd, this.config);
769
+ if (this.app) report.application = await withApp(await loadConfig(report.cwd, report.config.path), describeApplication, (message) => {
770
+ report.issues.push(message);
771
+ });
772
+ } catch (error) {
773
+ report.issues.push(error instanceof Error ? error.message : String(error));
774
+ }
775
+ if (this.json) this.context.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
776
+ else {
777
+ this.context.stdout.write(`Node ${report.nodeVersion}\nWorking directory: ${report.cwd}\n`);
778
+ if (report.config) {
779
+ this.context.stdout.write(`Config: ${report.config.path} (${report.config.source})\n`);
780
+ for (const candidate of report.config.candidates) this.context.stdout.write(` Checked: ${candidate}\n`);
781
+ }
782
+ if (report.application) {
783
+ const { modules, routes, entrypoints } = report.application;
784
+ this.context.stdout.write(`Application: ${modules.length} modules, ${routes?.length ?? 0} routes, ${entrypoints.length} entrypoints\n`);
785
+ this.context.stdout.write("Use --json for the full graph.\n");
786
+ } else if (!this.app) this.context.stdout.write("Config was not imported. Use --app to bootstrap and inspect a built application.\n");
787
+ for (const issue of report.issues) this.context.stderr.write(`${issue}\n`);
788
+ }
789
+ return report.issues.length ? 1 : 0;
790
+ }
791
+ };
792
+ //#endregion
793
+ //#region src/commands/deploy-check.config.ts
794
+ const record = z.record(z.string(), z.unknown());
795
+ const text = z.string().min(1).refine((value) => value.trim() === value && !/\p{Cc}/u.test(value));
796
+ const bindingName = text.regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/u);
797
+ const workerName = text.max(255).regex(/^[a-zA-Z0-9-]+$/u);
798
+ const date = z.string().regex(/^\d{4}-\d{2}-\d{2}$/u).refine((value) => {
799
+ const parsed = new Date(value);
800
+ return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
801
+ });
802
+ /** Decode only data; parser errors deliberately omit configuration values. */
803
+ function parseDeploymentConfig(source, path) {
804
+ const extension = extname(path).toLowerCase();
805
+ if (extension === ".toml") try {
806
+ return parse$1(source);
807
+ } catch {
808
+ throw new Error("Invalid Wrangler TOML configuration.");
809
+ }
810
+ if (extension !== ".json" && extension !== ".jsonc") throw new Error("Wrangler configuration must be a .json, .jsonc or .toml file.");
811
+ const errors = [];
812
+ const result = parse(source, errors, {
813
+ allowTrailingComma: extension === ".jsonc",
814
+ disallowComments: extension === ".json"
815
+ });
816
+ if (errors.length > 0) throw new Error("Invalid Wrangler JSON configuration.");
817
+ return result;
818
+ }
819
+ function checked(schema, value, field) {
820
+ const result = schema.safeParse(value);
821
+ if (!result.success) throw new Error(`Invalid Wrangler field: ${field}.`);
822
+ return result.data;
823
+ }
824
+ /** A projection, not a replacement for Wrangler's full configuration validator. */
825
+ function selectDeploymentTarget(raw, environment) {
826
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(environment)) throw new Error("An explicit environment name using letters, digits, underscores or dashes is required.");
827
+ const root = checked(record, raw, "configuration");
828
+ const environments = checked(record, root.env, "env");
829
+ if (!Object.hasOwn(environments, environment)) throw new Error("The requested environment is not declared in Wrangler configuration.");
830
+ const selected = checked(record, environments[environment], `env.${environment}`);
831
+ const inherit = (key) => Object.hasOwn(selected, key) ? selected[key] : root[key];
832
+ const worker = checked(workerName, Object.hasOwn(selected, "name") ? selected.name : `${checked(workerName, root.name, "name")}-${environment}`, "name");
833
+ const main = checked(text.optional(), inherit("main"), "main") ?? null;
834
+ if (main === null) {
835
+ const assets = checked(record, inherit("assets"), "assets (required without main)");
836
+ checked(text, assets.directory, "assets.directory");
837
+ }
838
+ const compatibilityDate = checked(date, inherit("compatibility_date"), "compatibility_date");
839
+ const compatibilityFlags = checked(z.array(text).optional(), inherit("compatibility_flags"), "compatibility_flags") ?? [];
840
+ const triggers = checked(z.object({ crons: z.array(text) }).optional(), inherit("triggers"), "triggers");
841
+ const build = checked(z.object({ command: text.optional() }).optional(), inherit("build"), "build");
842
+ const bindings = [];
843
+ const names = /* @__PURE__ */ new Set();
844
+ const add = (name, kind) => {
845
+ if (names.has(name)) throw new Error(`Duplicate Worker binding name: ${name}.`);
846
+ names.add(name);
847
+ bindings.push({
848
+ name,
849
+ kind
850
+ });
851
+ };
852
+ const variables = checked(record.optional(), selected.vars, "vars") ?? {};
853
+ for (const name of Object.keys(variables)) add(checked(bindingName, name, "vars binding name"), "var");
854
+ for (const kind of [
855
+ "kv_namespaces",
856
+ "d1_databases",
857
+ "r2_buckets",
858
+ "services",
859
+ "hyperdrive",
860
+ "vectorize",
861
+ "workflows",
862
+ "analytics_engine_datasets"
863
+ ]) {
864
+ const rows = checked(z.array(record).optional(), selected[kind], kind) ?? [];
865
+ for (const row of rows) {
866
+ add(checked(bindingName, row.binding, `${kind}.binding`), kind);
867
+ for (const field of [
868
+ "id",
869
+ "database_id",
870
+ "database_name",
871
+ "bucket_name"
872
+ ]) if (Object.hasOwn(row, field)) checked(text, row[field], `${kind}.${field}`);
873
+ if (kind === "services") checked(workerName, row.service, "services.service");
874
+ if (kind === "workflows") checked(text, row.class_name, "workflows.class_name");
875
+ }
876
+ }
877
+ const durable = checked(z.object({ bindings: z.array(z.object({
878
+ name: bindingName,
879
+ class_name: text,
880
+ script_name: workerName.optional()
881
+ })) }).optional(), selected.durable_objects, "durable_objects");
882
+ for (const row of durable?.bindings ?? []) add(row.name, "durable_objects");
883
+ const queues = checked(z.object({
884
+ producers: z.array(z.object({
885
+ binding: bindingName,
886
+ queue: text.optional()
887
+ })).optional(),
888
+ consumers: z.array(z.object({ queue: text })).optional()
889
+ }).optional(), selected.queues, "queues");
890
+ for (const producer of queues?.producers ?? []) add(producer.binding, "queues");
891
+ const queueConsumers = queues?.consumers?.map((row) => row.queue) ?? [];
892
+ if (new Set(queueConsumers).size !== queueConsumers.length) throw new Error("Duplicate queue consumer configuration.");
893
+ return {
894
+ environment,
895
+ worker,
896
+ main,
897
+ compatibilityDate,
898
+ compatibilityFlags,
899
+ crons: triggers?.crons ?? [],
900
+ bindings,
901
+ queueConsumers,
902
+ customBuild: build?.command !== void 0
903
+ };
904
+ }
905
+ //#endregion
906
+ //#region src/commands/deploy-check.plan.ts
907
+ const rowsSchema = z.array(z.object({
908
+ kind: z.string().min(1),
909
+ target: z.string().min(1),
910
+ meta: z.unknown()
911
+ }));
912
+ const metadataSchema = z.record(z.string(), z.unknown());
913
+ /** Accept existing `vela entrypoint list --json` output without loading an application. */
914
+ function entrypoints(value) {
915
+ const parsed = rowsSchema.safeParse(value);
916
+ if (!parsed.success) throw new Error("Entrypoint snapshot must be an array of { kind, target, meta } rows.");
917
+ return parsed.data.filter((row) => !(row.target === "(no entrypoints)" && row.meta === "")).map((row) => {
918
+ let meta = row.meta;
919
+ if (typeof meta === "string") try {
920
+ meta = JSON.parse(meta);
921
+ } catch {
922
+ throw new Error("Invalid JSON metadata in entrypoint snapshot.");
923
+ }
924
+ return {
925
+ kind: row.kind,
926
+ meta
927
+ };
928
+ });
929
+ }
930
+ /** Compare literal dispatch keys; equivalent cron expressions are not interchangeable. */
931
+ function checkDeployment(rawConfig, environment, snapshot) {
932
+ const target = selectDeploymentTarget(rawConfig, environment);
933
+ const errors = [];
934
+ const warnings = [];
935
+ const report = (code, message) => errors.push({
936
+ code,
937
+ message
938
+ });
939
+ const validateCron = (cron) => {
940
+ if (!parseCron(cron, {
941
+ dialect: "cloudflare",
942
+ timeZone: "UTC"
943
+ })) report("invalid-cron", "A trigger or handler has an invalid Cloudflare cron expression.");
944
+ };
945
+ const crons = new Set(target.crons);
946
+ if (crons.size !== target.crons.length) report("duplicate-cron", "Duplicate cron trigger configuration.");
947
+ for (const cron of crons) validateCron(cron);
948
+ const handlerCrons = /* @__PURE__ */ new Set();
949
+ const handlerQueues = /* @__PURE__ */ new Set();
950
+ for (const row of entrypoints(snapshot)) {
951
+ if (row.kind === "schedule:interval") {
952
+ report("unsupported-interval", "Workers cannot drive @Interval handlers; use a scheduled trigger or a separate Node adapter.");
953
+ continue;
954
+ }
955
+ if (![
956
+ "cf:scheduled",
957
+ "cf:vela-cron",
958
+ "schedule:cron",
959
+ "cf:queue",
960
+ "websocket"
961
+ ].includes(row.kind)) continue;
962
+ const parsed = metadataSchema.safeParse(row.meta);
963
+ if (!parsed.success) {
964
+ report("invalid-metadata", `Invalid ${row.kind} metadata.`);
965
+ continue;
966
+ }
967
+ const meta = parsed.data;
968
+ if (row.kind === "cf:vela-cron" || row.kind === "schedule:cron") try {
969
+ const cron = parseCronMetadata(meta);
970
+ if (cron.dialect !== void 0 && cron.dialect !== "cloudflare" || cron.timeZone !== void 0 && cron.timeZone !== "UTC") report("incompatible-cron-options", "A cron handler explicitly requests options incompatible with Cloudflare UTC delivery.");
971
+ } catch {
972
+ report("invalid-metadata", `Invalid ${row.kind} metadata.`);
973
+ continue;
974
+ }
975
+ if (row.kind === "websocket") {
976
+ const options = Object.hasOwn(meta, "options") ? metadataSchema.safeParse(meta.options) : parsed;
977
+ const binding = options.success ? options.data.binding : void 0;
978
+ if (typeof binding !== "string" || !target.bindings.some((b) => b.kind === "durable_objects" && b.name === binding)) report("missing-durable-binding", "A WebSocket gateway requires a Durable Object binding in the selected environment.");
979
+ continue;
980
+ }
981
+ const key = row.kind === "cf:scheduled" ? "cron" : row.kind === "cf:queue" ? "queueName" : "expression";
982
+ const value = meta[key];
983
+ if (typeof value !== "string" || value.trim().length === 0) {
984
+ report("invalid-metadata", `Invalid ${row.kind}.${key} metadata.`);
985
+ continue;
986
+ }
987
+ if (row.kind === "cf:queue") handlerQueues.add(value);
988
+ else {
989
+ handlerCrons.add(value);
990
+ validateCron(value);
991
+ }
992
+ }
993
+ for (const cron of handlerCrons) if (!crons.has(cron)) report("missing-cron-trigger", `No exact Wrangler trigger for handler cron ${JSON.stringify(cron)}.`);
994
+ for (const cron of crons) if (!handlerCrons.has(cron)) report("unhandled-cron-trigger", `No metadata handler for Wrangler cron ${JSON.stringify(cron)}.`);
995
+ for (const queue of handlerQueues) if (!target.queueConsumers.includes(queue)) report("missing-queue-consumer", `No selected queue consumer for handler queue ${JSON.stringify(queue)}.`);
996
+ for (const queue of target.queueConsumers) if (!handlerQueues.has(queue)) report("unhandled-queue-consumer", `No metadata handler for selected queue ${JSON.stringify(queue)}.`);
997
+ warnings.push({
998
+ code: "static-only",
999
+ message: "Snapshot freshness, custom platform handlers, deployed resources, secrets and bundle/runtime behavior are not verified. Run Wrangler and native tests separately."
1000
+ });
1001
+ if (target.customBuild) warnings.push({
1002
+ code: "custom-build",
1003
+ message: "Wrangler has a custom build command. It was not executed by this check."
1004
+ });
1005
+ return {
1006
+ status: errors.length ? "failed" : "passed",
1007
+ target,
1008
+ errors,
1009
+ warnings
1010
+ };
1011
+ }
1012
+ //#endregion
1013
+ //#region src/commands/deploy-check.command.ts
1014
+ const exec = promisify(execFile);
1015
+ const MAX_INPUT_BYTES = 1048576;
1016
+ async function readInput(path) {
1017
+ const file = await open(path, "r");
1018
+ try {
1019
+ if (!(await file.stat()).isFile()) throw new Error("Input must be a regular file.");
1020
+ const buffer = Buffer.alloc(1048577);
1021
+ let length = 0;
1022
+ while (length < buffer.length) {
1023
+ const { bytesRead } = await file.read(buffer, length, buffer.length - length, length);
1024
+ if (bytesRead === 0) break;
1025
+ length += bytesRead;
1026
+ }
1027
+ if (length > MAX_INPUT_BYTES) throw new Error("Deployment inputs must not exceed 1 MiB.");
1028
+ return buffer.subarray(0, length).toString("utf8");
1029
+ } finally {
1030
+ await file.close();
1031
+ }
1032
+ }
1033
+ async function gitProvenance(cwd) {
1034
+ try {
1035
+ const options = {
1036
+ cwd,
1037
+ timeout: 5e3,
1038
+ maxBuffer: 1048576
1039
+ };
1040
+ const [head, status] = await Promise.all([exec("git", [
1041
+ "rev-parse",
1042
+ "--verify",
1043
+ "HEAD"
1044
+ ], options), exec("git", [
1045
+ "--no-optional-locks",
1046
+ "-c",
1047
+ "core.fsmonitor=false",
1048
+ "status",
1049
+ "--porcelain=v1",
1050
+ "--untracked-files=normal"
1051
+ ], options)]);
1052
+ const commit = head.stdout.trim();
1053
+ if (!/^[a-f0-9]{40,64}$/u.test(commit)) throw new Error("Invalid git commit.");
1054
+ return {
1055
+ commit,
1056
+ dirty: status.stdout.length > 0
1057
+ };
1058
+ } catch {
1059
+ return {
1060
+ commit: null,
1061
+ dirty: null
1062
+ };
1063
+ }
1064
+ }
1065
+ const digest = (text) => createHash("sha256").update(text).digest("hex");
1066
+ /** Static application deployment preflight; never invokes Wrangler or application code. */
1067
+ var DeployCheckCommand = class extends Command {
1068
+ static paths = [["deploy", "check"]];
1069
+ static usage = Command.Usage({
1070
+ category: "Deployment",
1071
+ description: "Check an explicit Wrangler target against a saved entrypoint snapshot.",
1072
+ details: "Read-only: no app bootstrap, custom build, credential loading or upload. Compares cron/queue dispatch keys and WebSocket Durable Object bindings. Wrangler remains the deployment tool.",
1073
+ examples: [["Check staging", "vela deploy check --config wrangler.jsonc --env staging --entrypoints entrypoints.json"]]
1074
+ });
1075
+ config = Option.String("--config", {
1076
+ required: true,
1077
+ description: "Explicit Wrangler .json/.jsonc/.toml file."
1078
+ });
1079
+ environment = Option.String("--env", {
1080
+ required: true,
1081
+ description: "Exact named environment in the Wrangler file."
1082
+ });
1083
+ entrypoints = Option.String("--entrypoints", {
1084
+ required: true,
1085
+ description: "Saved vela entrypoint list --json array."
1086
+ });
1087
+ json = Option.Boolean("--json", false, { description: "Emit the redacted report as JSON." });
1088
+ async execute() {
1089
+ try {
1090
+ if (!this.config.trim() || !this.entrypoints.trim()) throw new Error("Explicit configuration and snapshot paths are required.");
1091
+ const configPath = resolve(this.config);
1092
+ const snapshotPath = resolve(this.entrypoints);
1093
+ const [config, snapshot] = await Promise.all([readInput(configPath), readInput(snapshotPath)]);
1094
+ let rows;
1095
+ try {
1096
+ rows = JSON.parse(snapshot);
1097
+ } catch {
1098
+ throw new Error("Invalid entrypoint snapshot JSON.");
1099
+ }
1100
+ const plan = checkDeployment(parseDeploymentConfig(config, configPath), this.environment, rows);
1101
+ const provenance = {
1102
+ ...await gitProvenance(dirname(configPath)),
1103
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1104
+ config: {
1105
+ path: configPath,
1106
+ sha256: digest(config)
1107
+ },
1108
+ entrypoints: {
1109
+ path: snapshotPath,
1110
+ sha256: digest(snapshot)
1111
+ }
1112
+ };
1113
+ const wrangler = {
1114
+ command: "pnpm",
1115
+ args: [
1116
+ "exec",
1117
+ "wrangler",
1118
+ "deploy",
1119
+ "--config",
1120
+ configPath,
1121
+ "--env",
1122
+ this.environment,
1123
+ "--dry-run"
1124
+ ]
1125
+ };
1126
+ const result = {
1127
+ ...plan,
1128
+ provenance,
1129
+ nextStep: wrangler
1130
+ };
1131
+ if (this.json) this.context.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1132
+ else {
1133
+ this.context.stdout.write(`Deployment check: ${plan.status}\nWorker: ${plan.target.worker}\nEnvironment: ${plan.target.environment}\nConfig: ${configPath}\nCommit: ${provenance.commit ?? "unavailable"} (${provenance.dirty === null ? "cleanliness unknown" : provenance.dirty ? "dirty" : "clean"})\nConfig SHA-256: ${provenance.config.sha256}\nSnapshot SHA-256: ${provenance.entrypoints.sha256}\nBindings: ${plan.target.bindings.map((binding) => `${binding.name} (${binding.kind})`).join(", ") || "(none)"}\n`);
1134
+ for (const issue of plan.errors) this.context.stdout.write(`Error [${issue.code}]: ${issue.message}\n`);
1135
+ for (const issue of plan.warnings) this.context.stdout.write(`Warning [${issue.code}]: ${issue.message}\n`);
1136
+ this.context.stdout.write(`Next step (not executed): ${[wrangler.command, ...wrangler.args].map((arg) => `'${arg.replaceAll("'", "'\\''")}'`).join(" ")}\n`);
624
1137
  }
1138
+ return plan.status === "passed" ? 0 : 1;
1139
+ } catch (error) {
1140
+ const message = error instanceof Error ? error.message : "Deployment check failed.";
1141
+ if (this.json) this.context.stdout.write(`${JSON.stringify({
1142
+ status: "failed",
1143
+ error: message
1144
+ })}\n`);
1145
+ else this.context.stderr.write(`${message}\n`);
1146
+ return 1;
625
1147
  }
626
1148
  }
627
1149
  };
@@ -630,10 +1152,11 @@ var ClientGenerateCommand = class extends Command {
630
1152
  const cli = new Cli({
631
1153
  binaryName: "vela",
632
1154
  binaryLabel: "Vela CLI",
633
- binaryVersion: "0.2.0"
1155
+ binaryVersion: version
634
1156
  });
635
1157
  cli.register(Builtins.HelpCommand);
636
1158
  cli.register(Builtins.VersionCommand);
1159
+ cli.register(NewCommand);
637
1160
  cli.register(SeedCommand);
638
1161
  cli.register(RouteListCommand);
639
1162
  cli.register(ModuleGraphCommand);
@@ -642,8 +1165,10 @@ cli.register(OpenApiDumpCommand);
642
1165
  cli.register(McpServeCommand);
643
1166
  cli.register(StudioCommand);
644
1167
  cli.register(ClientGenerateCommand);
1168
+ cli.register(DoctorCommand);
1169
+ cli.register(DeployCheckCommand);
645
1170
  cli.runExit(process.argv.slice(2));
646
1171
  //#endregion
647
- export { ClientGenerateCommand, EntrypointListCommand, McpServeCommand, ModuleGraphCommand, OpenApiDumpCommand, RouteListCommand, SeedCommand, StudioCommand, collectEntrypoints, collectModules, collectRoutes, defineVelaConfig, formatSeedResults, generateClientContract, loadConfig, renderModuleTree, renderTable };
1172
+ export { ClientGenerateCommand, DeployCheckCommand, DoctorCommand, EntrypointListCommand, McpServeCommand, ModuleGraphCommand, NewCommand, OpenApiDumpCommand, RouteListCommand, SeedCommand, StudioCommand, collectEntrypoints, collectModules, collectRoutes, defineVelaConfig, formatSeedResults, generateClientContract, loadConfig, renderModuleTree, renderTable, resolveConfig };
648
1173
 
649
1174
  //# sourceMappingURL=index.js.map