@moku-labs/worker 0.1.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/cli.cjs ADDED
@@ -0,0 +1,743 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_storage = require("./storage-BaQ6BBtl.cjs");
3
+ let node_child_process = require("node:child_process");
4
+ let node_fs_promises = require("node:fs/promises");
5
+ let node_path = require("node:path");
6
+ node_path = require_storage.__toESM(node_path, 1);
7
+ let node_fs = require("node:fs");
8
+ //#region src/plugins/deploy/runner.ts
9
+ /**
10
+ * @file deploy plugin — wrangler subprocess wrapper (node:child_process).
11
+ *
12
+ * Spawns `wrangler` with the given args and resolves the deployed URL
13
+ * (extracted from stdout for `wrangler deploy`), or the full stdout for other verbs.
14
+ * This module is node-only; never imported by the runtime Worker bundle.
15
+ */
16
+ /**
17
+ * Extract the deployed URL from `wrangler deploy` stdout.
18
+ * Wrangler prints a line like: "Published my-worker (1.23 sec) https://..."
19
+ * or "Deployed my-worker (1.23 sec) https://...".
20
+ *
21
+ * @param output - The combined stdout from wrangler deploy.
22
+ * @returns The deployed URL, or empty string when not found.
23
+ * @example
24
+ * ```ts
25
+ * extractDeployedUrl("Deployed my-worker (0.5 sec) https://my-worker.workers.dev");
26
+ * // "https://my-worker.workers.dev"
27
+ * ```
28
+ */
29
+ const extractDeployedUrl = (output) => {
30
+ return /https:\/\/[^\s]+\.workers\.dev[^\s]*/u.exec(output)?.[0] ?? "";
31
+ };
32
+ /**
33
+ * Spawn `wrangler` with the given args and resolve the output string.
34
+ * For `wrangler deploy`, the resolved value is the deployed URL parsed from stdout.
35
+ * For all other verbs (dev, kv namespace create, etc.), the resolved value is stdout.
36
+ *
37
+ * @param args - Wrangler CLI arguments (e.g. ["deploy", "--config", "wrangler.jsonc"]).
38
+ * @returns Resolves with the deployed URL (deploy verb) or full stdout (other verbs).
39
+ * @throws {Error} When wrangler exits with a non-zero code.
40
+ * @example
41
+ * ```ts
42
+ * const url = await runWrangler(["deploy", "--config", "wrangler.jsonc"]);
43
+ * await runWrangler(["kv", "namespace", "create", "CACHE"]);
44
+ * ```
45
+ */
46
+ const runWrangler = (args) => new Promise((resolve, reject) => {
47
+ const chunks = [];
48
+ const errChunks = [];
49
+ const child = (0, node_child_process.spawn)("wrangler", args, {
50
+ env: { ...process.env },
51
+ stdio: [
52
+ "ignore",
53
+ "pipe",
54
+ "pipe"
55
+ ]
56
+ });
57
+ child.stdout.on("data", (chunk) => {
58
+ chunks.push(chunk);
59
+ });
60
+ child.stderr.on("data", (chunk) => {
61
+ errChunks.push(chunk);
62
+ });
63
+ child.on("error", (err) => {
64
+ reject(/* @__PURE__ */ new Error(`[moku-worker] Failed to spawn wrangler.\n ${err.message}`));
65
+ });
66
+ child.on("close", (code) => {
67
+ const stdout = Buffer.concat(chunks).toString("utf8");
68
+ const stderr = Buffer.concat(errChunks).toString("utf8");
69
+ if (code !== 0) {
70
+ reject(/* @__PURE__ */ new Error(`[moku-worker] wrangler exited with code ${String(code)}.\n ${stderr || stdout}`));
71
+ return;
72
+ }
73
+ resolve(args[0] === "deploy" ? extractDeployedUrl(stdout) : stdout);
74
+ });
75
+ });
76
+ //#endregion
77
+ //#region src/plugins/deploy/providers/d1.ts
78
+ /**
79
+ * @file deploy plugin — D1 provisioning adapter.
80
+ *
81
+ * Creates a Cloudflare D1 database via `wrangler d1 create <binding>`.
82
+ * Node-only; never imported by the runtime Worker bundle.
83
+ */
84
+ /**
85
+ * Provision a D1 database via `wrangler d1 create` and apply migrations.
86
+ *
87
+ * @param manifest - The D1 resource descriptor.
88
+ * @param _ci - Whether running non-interactively.
89
+ * @returns Resolves once the database is created (and migrations applied when specified).
90
+ * @example
91
+ * ```ts
92
+ * await provisionD1({ kind: "d1", binding: "DB", migrations: "./migrations" }, false);
93
+ * ```
94
+ */
95
+ const provisionD1 = async (manifest, _ci) => {
96
+ await runWrangler([
97
+ "d1",
98
+ "create",
99
+ manifest.binding
100
+ ]);
101
+ if (manifest.migrations) await runWrangler([
102
+ "d1",
103
+ "migrations",
104
+ "apply",
105
+ manifest.binding,
106
+ "--local"
107
+ ]);
108
+ };
109
+ //#endregion
110
+ //#region src/plugins/deploy/providers/do.ts
111
+ /**
112
+ * Provision Durable Object bindings. DOs are config-driven (no `wrangler do create` command
113
+ * exists) — the actual binding entries are written by writeWranglerConfig. This function is
114
+ * a resolved no-op for the dispatch step.
115
+ *
116
+ * @param _manifest - The Durable Objects resource descriptor.
117
+ * @param _ci - Whether running non-interactively.
118
+ * @returns Resolves immediately (DOs are config-only provisioning).
119
+ * @example
120
+ * ```ts
121
+ * await provisionDurableObject({ kind: "do", bindings: { counter: "COUNTER" } }, false);
122
+ * ```
123
+ */
124
+ const provisionDurableObject = async (_manifest, _ci) => {};
125
+ //#endregion
126
+ //#region src/plugins/deploy/providers/kv.ts
127
+ /**
128
+ * @file deploy plugin — KV provisioning adapter.
129
+ *
130
+ * Creates a Cloudflare KV namespace via `wrangler kv namespace create <binding>`.
131
+ * Node-only; never imported by the runtime Worker bundle.
132
+ */
133
+ /**
134
+ * Provision a KV namespace via `wrangler kv namespace create`.
135
+ *
136
+ * @param manifest - The KV resource descriptor.
137
+ * @param _ci - Whether running non-interactively (passed through; wrangler respects env vars).
138
+ * @returns Resolves once the namespace is created.
139
+ * @example
140
+ * ```ts
141
+ * await provisionKv({ kind: "kv", binding: "CACHE" }, false);
142
+ * ```
143
+ */
144
+ const provisionKv = async (manifest, _ci) => {
145
+ await runWrangler([
146
+ "kv",
147
+ "namespace",
148
+ "create",
149
+ manifest.binding
150
+ ]);
151
+ };
152
+ //#endregion
153
+ //#region src/plugins/deploy/providers/queues.ts
154
+ /**
155
+ * @file deploy plugin — Queues provisioning adapter.
156
+ *
157
+ * Creates Cloudflare Queues via `wrangler queues create <name>` for each producer.
158
+ * Node-only; never imported by the runtime Worker bundle.
159
+ */
160
+ /**
161
+ * Provision queues via `wrangler queues create` for each declared producer.
162
+ *
163
+ * @param manifest - The queue resource descriptor.
164
+ * @param _ci - Whether running non-interactively.
165
+ * @returns Resolves once all queues are created.
166
+ * @example
167
+ * ```ts
168
+ * await provisionQueue({ kind: "queue", producers: ["orders"] }, false);
169
+ * ```
170
+ */
171
+ const provisionQueue = async (manifest, _ci) => {
172
+ for (const producer of manifest.producers) await runWrangler([
173
+ "queues",
174
+ "create",
175
+ producer
176
+ ]);
177
+ };
178
+ //#endregion
179
+ //#region src/plugins/deploy/providers/r2.ts
180
+ /**
181
+ * @file deploy plugin — R2 provisioning + asset upload adapter.
182
+ *
183
+ * Provides two exports:
184
+ * - `provisionR2`: creates an R2 bucket via `wrangler r2 bucket create`.
185
+ * - `uploadDirToR2`: walks a directory recursively and uploads each file via
186
+ * `wrangler r2 object put`, returning the uploaded file count.
187
+ *
188
+ * Node-only; never imported by the runtime Worker bundle.
189
+ */
190
+ /**
191
+ * Provision an R2 bucket via `wrangler r2 bucket create`.
192
+ *
193
+ * @param manifest - The R2 resource descriptor.
194
+ * @param _ci - Whether running non-interactively.
195
+ * @returns Resolves once the bucket is created.
196
+ * @example
197
+ * ```ts
198
+ * await provisionR2({ kind: "r2", bucket: "ASSETS" }, false);
199
+ * ```
200
+ */
201
+ const provisionR2 = async (manifest, _ci) => {
202
+ await runWrangler([
203
+ "r2",
204
+ "bucket",
205
+ "create",
206
+ manifest.bucket
207
+ ]);
208
+ };
209
+ /**
210
+ * Walk a directory recursively and return all file paths (absolute).
211
+ *
212
+ * @param directory - Directory path to walk.
213
+ * @returns All file paths found under the directory.
214
+ * @example
215
+ * ```ts
216
+ * const files = await walkDir("./public");
217
+ * ```
218
+ */
219
+ const walkDir = async (directory) => {
220
+ const entries = await (0, node_fs_promises.readdir)(directory);
221
+ const results = [];
222
+ for (const entry of entries) {
223
+ const fullPath = node_path.default.join(directory, entry);
224
+ if ((await (0, node_fs_promises.stat)(fullPath)).isDirectory()) {
225
+ const nested = await walkDir(fullPath);
226
+ results.push(...nested);
227
+ } else results.push(fullPath);
228
+ }
229
+ return results;
230
+ };
231
+ /**
232
+ * Upload a directory to an R2 bucket and return the uploaded file count.
233
+ * Each file is uploaded via `wrangler r2 object put <bucket>/<key> --file <path>`.
234
+ *
235
+ * @param bucket - The R2 bucket binding name.
236
+ * @param directory - The directory to upload.
237
+ * @returns The number of files uploaded.
238
+ * @example
239
+ * ```ts
240
+ * const count = await uploadDirToR2("ASSETS", "./public");
241
+ * ```
242
+ */
243
+ const uploadDirToR2 = async (bucket, directory) => {
244
+ const files = await walkDir(directory);
245
+ for (const filePath of files) await runWrangler([
246
+ "r2",
247
+ "object",
248
+ "put",
249
+ `${bucket}/${node_path.default.relative(directory, filePath)}`,
250
+ "--file",
251
+ filePath
252
+ ]);
253
+ return files.length;
254
+ };
255
+ //#endregion
256
+ //#region src/plugins/deploy/providers/index.ts
257
+ /**
258
+ * Dispatch a resource descriptor to the matching provider's provisioning routine.
259
+ *
260
+ * @param resource - The resource descriptor to provision.
261
+ * @param ci - Whether running non-interactively.
262
+ * @returns Resolves once the resource is provisioned.
263
+ * @example
264
+ * ```ts
265
+ * await provisionResource({ kind: "kv", binding: "CACHE" }, false);
266
+ * await provisionResource({ kind: "r2", bucket: "ASSETS" }, false);
267
+ * ```
268
+ */
269
+ const provisionResource = async (resource, ci) => {
270
+ switch (resource.kind) {
271
+ case "kv":
272
+ await provisionKv(resource, ci);
273
+ break;
274
+ case "r2":
275
+ await provisionR2(resource, ci);
276
+ break;
277
+ case "d1":
278
+ await provisionD1(resource, ci);
279
+ break;
280
+ case "queue":
281
+ await provisionQueue(resource, ci);
282
+ break;
283
+ case "do":
284
+ await provisionDurableObject(resource, ci);
285
+ break;
286
+ }
287
+ };
288
+ //#endregion
289
+ //#region src/plugins/deploy/wrangler-config.ts
290
+ /**
291
+ * @file deploy plugin — wrangler config generation + scaffold.
292
+ *
293
+ * Provides two exports:
294
+ * - `writeWranglerConfig`: generates/updates a wrangler.jsonc file from an ExternalManifest.
295
+ * Non-destructive: preserves existing top-level keys not managed by deploy.
296
+ * - `scaffoldWranglerAndCi`: creates a minimal starter wrangler config when the file does not
297
+ * exist yet; idempotent (leaves existing files untouched).
298
+ *
299
+ * Node-only; never imported by the runtime Worker bundle.
300
+ */
301
+ /**
302
+ * Strip JSONC line- and block-comments, then JSON.parse the result.
303
+ *
304
+ * @param source - Raw JSONC file contents.
305
+ * @returns The parsed object.
306
+ * @example
307
+ * ```ts
308
+ * const cfg = parseJsonc('{ "name": "w" } // trailing comment');
309
+ * ```
310
+ */
311
+ const parseJsonc = (source) => {
312
+ const stripped = source.replaceAll(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/gu, "");
313
+ return JSON.parse(stripped);
314
+ };
315
+ /**
316
+ * Build the wrangler `kv_namespaces` array from the manifest's kv resources.
317
+ *
318
+ * @param resources - All resource descriptors from the manifest.
319
+ * @returns One wrangler KV namespace entry per kv resource.
320
+ * @example
321
+ * ```ts
322
+ * const kv = buildKvNamespaces([{ kind: "kv", binding: "CACHE" }]);
323
+ * ```
324
+ */
325
+ const buildKvNamespaces = (resources) => resources.filter((resource) => resource.kind === "kv").map((resource) => ({
326
+ binding: resource.binding,
327
+ id: ""
328
+ }));
329
+ /**
330
+ * Build the wrangler `r2_buckets` array from the manifest's r2 resources.
331
+ *
332
+ * @param resources - All resource descriptors from the manifest.
333
+ * @returns One wrangler R2 bucket entry per r2 resource.
334
+ * @example
335
+ * ```ts
336
+ * const r2 = buildR2Buckets([{ kind: "r2", bucket: "ASSETS" }]);
337
+ * ```
338
+ */
339
+ const buildR2Buckets = (resources) => resources.filter((resource) => resource.kind === "r2").map((resource) => ({
340
+ binding: resource.bucket,
341
+ bucket_name: resource.bucket.toLowerCase()
342
+ }));
343
+ /**
344
+ * Build the wrangler `d1_databases` array from the manifest's d1 resources.
345
+ *
346
+ * @param resources - All resource descriptors from the manifest.
347
+ * @returns One wrangler D1 database entry per d1 resource (migrations_dir set when present).
348
+ * @example
349
+ * ```ts
350
+ * const d1 = buildD1Databases([{ kind: "d1", binding: "DB" }]);
351
+ * ```
352
+ */
353
+ const buildD1Databases = (resources) => resources.filter((resource) => resource.kind === "d1").map((resource) => {
354
+ const entry = {
355
+ binding: resource.binding,
356
+ database_name: resource.binding.toLowerCase(),
357
+ database_id: ""
358
+ };
359
+ if (resource.migrations) entry.migrations_dir = resource.migrations;
360
+ return entry;
361
+ });
362
+ /**
363
+ * Build the wrangler `queues` producers section from the manifest's queue resources.
364
+ *
365
+ * @param resources - All resource descriptors from the manifest.
366
+ * @returns The queues section, or undefined when there are no queue resources.
367
+ * @example
368
+ * ```ts
369
+ * const q = buildQueues([{ kind: "queue", producers: ["jobs"] }]);
370
+ * ```
371
+ */
372
+ const buildQueues = (resources) => {
373
+ const queueResources = resources.filter((resource) => resource.kind === "queue");
374
+ if (queueResources.length === 0) return void 0;
375
+ return { producers: queueResources.flatMap((resource) => resource.producers.map((producer) => ({
376
+ queue: producer,
377
+ binding: producer.toUpperCase()
378
+ }))) };
379
+ };
380
+ /**
381
+ * Build the wrangler `durable_objects` bindings section from the manifest's do resources.
382
+ *
383
+ * @param resources - All resource descriptors from the manifest.
384
+ * @returns The durable_objects section, or undefined when there are no do resources.
385
+ * @example
386
+ * ```ts
387
+ * const dobj = buildDurableObjects([{ kind: "do", bindings: { Counter: "COUNTER" } }]);
388
+ * ```
389
+ */
390
+ const buildDurableObjects = (resources) => {
391
+ const doResources = resources.filter((resource) => resource.kind === "do");
392
+ if (doResources.length === 0) return void 0;
393
+ return { bindings: doResources.flatMap((resource) => Object.entries(resource.bindings).map(([className, bindingName]) => ({
394
+ name: bindingName,
395
+ class_name: className
396
+ }))) };
397
+ };
398
+ /**
399
+ * Generate/update the wrangler config file from a manifest (non-destructive merge).
400
+ * If the file exists, its top-level keys are preserved and only deploy-managed keys
401
+ * (name, compatibility_date, kv_namespaces, r2_buckets, d1_databases, queues,
402
+ * durable_objects) are updated.
403
+ *
404
+ * @param configFile - Path to the wrangler config file.
405
+ * @param manifest - The assembled deploy manifest.
406
+ * @returns Resolves once the file is written.
407
+ * @example
408
+ * ```ts
409
+ * await writeWranglerConfig("wrangler.jsonc", manifest);
410
+ * ```
411
+ */
412
+ const writeWranglerConfig = async (configFile, manifest) => {
413
+ let existing = {};
414
+ if ((0, node_fs.existsSync)(configFile)) try {
415
+ existing = parseJsonc((0, node_fs.readFileSync)(configFile, "utf8"));
416
+ } catch {
417
+ existing = {};
418
+ }
419
+ const kvNamespaces = buildKvNamespaces(manifest.resources);
420
+ const r2Buckets = buildR2Buckets(manifest.resources);
421
+ const d1Databases = buildD1Databases(manifest.resources);
422
+ const queues = buildQueues(manifest.resources);
423
+ const durableObjects = buildDurableObjects(manifest.resources);
424
+ const updated = {
425
+ ...existing,
426
+ name: manifest.name,
427
+ compatibility_date: manifest.compatibilityDate
428
+ };
429
+ if (kvNamespaces.length > 0) updated.kv_namespaces = kvNamespaces;
430
+ if (r2Buckets.length > 0) updated.r2_buckets = r2Buckets;
431
+ if (d1Databases.length > 0) updated.d1_databases = d1Databases;
432
+ if (queues !== void 0) updated.queues = queues;
433
+ if (durableObjects !== void 0) updated.durable_objects = durableObjects;
434
+ await (0, node_fs_promises.writeFile)(configFile, JSON.stringify(updated, void 0, 2));
435
+ };
436
+ /**
437
+ * Scaffold a starting wrangler config and, when ci is set, CI workflow files.
438
+ * Idempotent: an existing config file is left completely untouched.
439
+ *
440
+ * @param configFile - Path to the wrangler config file.
441
+ * @param _ci - Whether to also scaffold CI workflow files.
442
+ * @returns Resolves once scaffolding is written.
443
+ * @example
444
+ * ```ts
445
+ * await scaffoldWranglerAndCi("wrangler.jsonc", true);
446
+ * ```
447
+ */
448
+ const scaffoldWranglerAndCi = async (configFile, _ci) => {
449
+ if ((0, node_fs.existsSync)(configFile)) return;
450
+ const starter = {
451
+ name: "my-worker",
452
+ main: "src/worker.ts",
453
+ compatibility_date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
454
+ };
455
+ await (0, node_fs_promises.writeFile)(configFile, JSON.stringify(starter, void 0, 2));
456
+ };
457
+ //#endregion
458
+ //#region src/plugins/deploy/api.ts
459
+ /**
460
+ * @file deploy plugin — API factory (run, dev, init).
461
+ *
462
+ * Pure ctx-taking factory. Assembles the deploy manifest from each resource plugin's own
463
+ * deployManifest() api (never sibling pluginConfigs — design F6), provisions resources,
464
+ * generates/updates the wrangler config, uploads the R2 upload dir, and runs wrangler deploy.
465
+ * Emits only global events: deploy:phase, deploy:complete, provision:resource.
466
+ *
467
+ * Node-only: uses node:child_process (via runner.ts) and node:fs (via wrangler-config.ts).
468
+ * Never called in the deployed Worker runtime.
469
+ */
470
+ /**
471
+ * Derive a human-readable name string from a resource descriptor (used in provision:resource).
472
+ *
473
+ * @param resource - The resource descriptor.
474
+ * @returns A name suitable for the provision:resource event payload.
475
+ * @example
476
+ * ```ts
477
+ * resourceName({ kind: "kv", binding: "CACHE" }); // "CACHE"
478
+ * ```
479
+ */
480
+ const resourceName = (resource) => {
481
+ switch (resource.kind) {
482
+ case "r2": return resource.bucket;
483
+ case "do": return Object.values(resource.bindings).join(",");
484
+ case "queue": return resource.producers.join(",");
485
+ default: return resource.binding;
486
+ }
487
+ };
488
+ /**
489
+ * Create the deploy api. Assembles the manifest from each resource plugin's own
490
+ * deployManifest() (never sibling config), provisions, generates config, uploads,
491
+ * and runs `wrangler deploy`, emitting global deploy events along the way.
492
+ *
493
+ * @param ctx - Plugin context (own config + require + has + emit + global).
494
+ * @returns The app.deploy api: run / dev / init.
495
+ * @example
496
+ * ```ts
497
+ * const api = createDeployApi(ctx);
498
+ * await api.run();
499
+ * ```
500
+ */
501
+ const createDeployApi = (ctx) => ({
502
+ /**
503
+ * Run the full deploy pipeline: detect → provision → wrangler-config → upload → deploy.
504
+ * When opts.manifest is supplied, it is used verbatim (universal path).
505
+ *
506
+ * @param opts - Optional run options.
507
+ * @param opts.guided - Enable interactive confirmation steps (skipped when ci=true).
508
+ * @param opts.yes - Auto-confirm all prompts.
509
+ * @param opts.manifest - Caller-supplied manifest (bypasses deployManifest() assembly).
510
+ * @returns Resolves once the deploy completes.
511
+ * @example
512
+ * ```ts
513
+ * await api.run({ guided: true });
514
+ * await api.run({ manifest: { name: "w", compatibilityDate: "2026-06-17", resources: [] } });
515
+ * ```
516
+ */
517
+ async run(opts) {
518
+ ctx.emit("deploy:phase", { phase: "detect" });
519
+ const manifest = opts?.manifest ?? {
520
+ name: ctx.global.name,
521
+ compatibilityDate: ctx.global.compatibilityDate,
522
+ resources: [
523
+ ctx.has("storage") ? ctx.require(require_storage.storagePlugin).deployManifest() : void 0,
524
+ ctx.has("kv") ? ctx.require(require_storage.kvPlugin).deployManifest() : void 0,
525
+ ctx.has("d1") ? ctx.require(require_storage.d1Plugin).deployManifest() : void 0,
526
+ ctx.has("queues") ? ctx.require(require_storage.queuesPlugin).deployManifest() : void 0,
527
+ ctx.has("durableObjects") ? ctx.require(require_storage.durableObjectsPlugin).deployManifest() : void 0
528
+ ].filter((resource) => resource !== void 0)
529
+ };
530
+ ctx.emit("deploy:phase", { phase: "provision" });
531
+ for (const resource of manifest.resources) {
532
+ await provisionResource(resource, ctx.config.ci);
533
+ ctx.emit("provision:resource", {
534
+ kind: resource.kind,
535
+ name: resourceName(resource)
536
+ });
537
+ }
538
+ ctx.emit("deploy:phase", { phase: "wrangler-config" });
539
+ await writeWranglerConfig(ctx.config.configFile, manifest);
540
+ const r2Resource = manifest.resources.find((resource) => resource.kind === "r2");
541
+ if (r2Resource?.upload) {
542
+ const count = await uploadDirToR2(r2Resource.bucket, r2Resource.upload);
543
+ ctx.emit("deploy:phase", {
544
+ phase: "upload",
545
+ detail: `${String(count)} files`
546
+ });
547
+ }
548
+ ctx.emit("deploy:phase", { phase: "deploy" });
549
+ const url = await runWrangler([
550
+ "deploy",
551
+ "--config",
552
+ ctx.config.configFile
553
+ ]);
554
+ ctx.emit("deploy:complete", { url });
555
+ },
556
+ /**
557
+ * Start a local Cloudflare dev session via `wrangler dev`.
558
+ *
559
+ * @param opts - Optional options.
560
+ * @param opts.port - Local dev port (default 8787).
561
+ * @returns Resolves when the dev session ends.
562
+ * @example
563
+ * ```ts
564
+ * await api.dev({ port: 8787 });
565
+ * ```
566
+ */
567
+ dev: async (opts) => {
568
+ await runWrangler([
569
+ "dev",
570
+ "--port",
571
+ String(opts?.port ?? 8787),
572
+ "--config",
573
+ ctx.config.configFile
574
+ ]);
575
+ },
576
+ /**
577
+ * Scaffold a starting wrangler config (and CI files when ci is set).
578
+ * Idempotent: an existing config file is left untouched.
579
+ *
580
+ * @param opts - Optional options.
581
+ * @param opts.ci - Also scaffold CI workflow files.
582
+ * @returns Resolves once scaffolding is written.
583
+ * @example
584
+ * ```ts
585
+ * await api.init({ ci: true });
586
+ * ```
587
+ */
588
+ init: async (opts) => {
589
+ await scaffoldWranglerAndCi(ctx.config.configFile, opts?.ci ?? ctx.config.ci);
590
+ }
591
+ });
592
+ /**
593
+ * Complex tier (node-only) — build-time deploy orchestrator over the five resource plugins.
594
+ *
595
+ * Assembles each resource plugin's deployManifest() via ctx.require, provisions resources,
596
+ * generates/updates wrangler config, uploads the R2 upload dir, and runs wrangler deploy.
597
+ * Also supports a universal path: run({ manifest }) uses a caller-supplied manifest verbatim.
598
+ *
599
+ * @see README.md
600
+ */
601
+ const deployPlugin = require_storage.createPlugin("deploy", {
602
+ config: {
603
+ configFile: "wrangler.jsonc",
604
+ ci: false
605
+ },
606
+ depends: [
607
+ require_storage.storagePlugin,
608
+ require_storage.kvPlugin,
609
+ require_storage.d1Plugin,
610
+ require_storage.queuesPlugin,
611
+ require_storage.durableObjectsPlugin
612
+ ],
613
+ api: (ctx) => createDeployApi(ctx)
614
+ });
615
+ //#endregion
616
+ //#region src/plugins/cli/api.ts
617
+ /**
618
+ * Builds app.cli.* — thin passthroughs to the deploy plugin via ctx.require(deployPlugin).
619
+ * Both verbs forward their opts verbatim; `dev` defaults port to ctx.config.port when no
620
+ * opts are supplied.
621
+ *
622
+ * @param ctx - CLI plugin context (own config + typed require to deployPlugin).
623
+ * @returns The cli API object with `dev` and `deploy` methods.
624
+ * @example
625
+ * ```ts
626
+ * const api = createCliApi(ctx);
627
+ * await api.dev(); // → deploy.dev({ port: 8787 })
628
+ * await api.deploy({ yes: true }); // → deploy.run({ yes: true })
629
+ * ```
630
+ */
631
+ const createCliApi = (ctx) => ({
632
+ /**
633
+ * Run the Worker locally; defaults port to ctx.config.port (8787) when no opts supplied.
634
+ *
635
+ * @param opts - Optional local dev options.
636
+ * @param opts.port - Local dev port to bind. Defaults to ctx.config.port (8787).
637
+ * @returns Resolves when the dev session ends.
638
+ * @example
639
+ * ```ts
640
+ * await api.dev(); // port 8787
641
+ * await api.dev({ port: 3000 }); // port 3000
642
+ * ```
643
+ */
644
+ dev(opts) {
645
+ return ctx.require(deployPlugin).dev(opts ?? { port: ctx.config.port });
646
+ },
647
+ /**
648
+ * One-command guided Cloudflare deploy; forwards flags verbatim to deploy.run.
649
+ * Passes `undefined` when called with no opts (not a default empty object).
650
+ *
651
+ * @param opts - Optional deploy options.
652
+ * @param opts.guided - Walk through each step interactively.
653
+ * @param opts.yes - Skip confirmation prompts (non-interactive / CI).
654
+ * @returns Resolves once the deploy completes.
655
+ * @example
656
+ * ```ts
657
+ * await api.deploy({ guided: true });
658
+ * await api.deploy({ yes: true }); // CI
659
+ * await api.deploy(); // opts === undefined
660
+ * ```
661
+ */
662
+ deploy(opts) {
663
+ return ctx.require(deployPlugin).run(opts);
664
+ }
665
+ });
666
+ //#endregion
667
+ //#region src/plugins/cli/handlers.ts
668
+ /**
669
+ * Builds the hook handlers that turn global deploy events into a live progress TUI
670
+ * via ctx.log. Pure observers — print and return; never mutate state, never block
671
+ * the deploy pipeline (fire-and-forget, spec/07 §3,§4).
672
+ *
673
+ * @param ctx - CLI plugin context with injected log core API.
674
+ * @returns Hook map for the three global deploy events.
675
+ * @example
676
+ * ```ts
677
+ * const hooks = createCliHooks(ctx);
678
+ * hooks["deploy:phase"]({ phase: "detect" }); // logs "> detect"
679
+ * hooks["provision:resource"]({ kind: "kv", name: "KV" }); // logs " + kv KV"
680
+ * hooks["deploy:complete"]({ url: "https://x.workers.dev" }); // logs "done -> https://x.workers.dev"
681
+ * ```
682
+ */
683
+ const createCliHooks = (ctx) => ({
684
+ /**
685
+ * Print one line per pipeline phase: "> phase" or "> phase - detail".
686
+ *
687
+ * @param p - The deploy:phase event payload.
688
+ * @example
689
+ * ```ts
690
+ * handler({ phase: "detect" }); // "> detect"
691
+ * handler({ phase: "upload", detail: "3 files" }); // "> upload - 3 files"
692
+ * ```
693
+ */
694
+ "deploy:phase"(p) {
695
+ const detail = p.detail ? ` - ${p.detail}` : "";
696
+ ctx.log.info(`> ${p.phase}${detail}`);
697
+ },
698
+ /**
699
+ * Print one indented line per provisioned resource: " + kind name".
700
+ *
701
+ * @param p - The provision:resource event payload.
702
+ * @example
703
+ * ```ts
704
+ * handler({ kind: "kv", name: "KV" }); // " + kv KV"
705
+ * ```
706
+ */
707
+ "provision:resource"(p) {
708
+ ctx.log.info(` + ${p.kind} ${p.name}`);
709
+ },
710
+ /**
711
+ * Print the terminal success line with the deployed URL.
712
+ *
713
+ * @param p - The deploy:complete event payload.
714
+ * @example
715
+ * ```ts
716
+ * handler({ url: "https://my-worker.workers.dev" }); // "done -> https://my-worker.workers.dev"
717
+ * ```
718
+ */
719
+ "deploy:complete"(p) {
720
+ ctx.log.info(`done -> ${p.url}`);
721
+ }
722
+ });
723
+ /**
724
+ * Standard tier (node-only) — developer-facing CLI surface.
725
+ *
726
+ * Mounts `app.cli.dev()` and `app.cli.deploy()` as thin passthroughs to deployPlugin.
727
+ * Hooks subscribe to the global deploy:phase / provision:resource / deploy:complete events
728
+ * and print a live progress TUI via the injected ctx.log core API.
729
+ *
730
+ * Inline lambdas on `api`/`hooks` preserve event-name inference so the hook map keys
731
+ * are constrained to `WorkerEvents` keys (spec/15 §5).
732
+ *
733
+ * @see README.md
734
+ */
735
+ const cliPlugin = require_storage.createPlugin("cli", {
736
+ depends: [deployPlugin],
737
+ config: { port: 8787 },
738
+ api: (ctx) => createCliApi(ctx),
739
+ hooks: (ctx) => createCliHooks(ctx)
740
+ });
741
+ //#endregion
742
+ exports.cliPlugin = cliPlugin;
743
+ exports.deployPlugin = deployPlugin;