@bagdock/cli 0.6.1 → 0.8.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.
Files changed (2) hide show
  1. package/dist/bagdock.js +186 -41
  2. package/package.json +2 -2
package/dist/bagdock.js CHANGED
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
993
993
  this._exitCallback = (err) => {
994
994
  if (err.code !== "commander.executeSubCommandAsync") {
995
995
  throw err;
996
- } else {}
996
+ }
997
997
  };
998
998
  }
999
999
  return this;
@@ -4105,7 +4105,9 @@ Deploying ${config.slug}@${config.version} → ${envLabel}
4105
4105
  formData.append("metadata", JSON.stringify({
4106
4106
  version: config.version,
4107
4107
  environment,
4108
- compatibilityDate: config.compatibilityDate ?? "2024-09-23"
4108
+ compatibilityDate: config.compatibilityDate ?? "2024-09-23",
4109
+ ...config.kv ? { kv: config.kv } : {},
4110
+ ...config.webhooks ? { webhooks: config.webhooks } : {}
4109
4111
  }));
4110
4112
  try {
4111
4113
  const res = await fetch(`${getApiBase()}/api/v1/developer/apps/${config.slug}/deploy`, {
@@ -4376,6 +4378,35 @@ async function validate() {
4376
4378
  checks.push({ name: "Bundle size", status: "pass", message: `${(size / 1024).toFixed(1)} KB` });
4377
4379
  }
4378
4380
  }
4381
+ if (config.webhooks !== undefined) {
4382
+ if (!Array.isArray(config.webhooks)) {
4383
+ checks.push({ name: "Webhooks", status: "fail", message: '"webhooks" must be an array of { name, path, description? }' });
4384
+ } else {
4385
+ const problems = [];
4386
+ const seen = new Set;
4387
+ config.webhooks.forEach((wh, i) => {
4388
+ if (!wh || typeof wh !== "object") {
4389
+ problems.push(`#${i} is not an object`);
4390
+ return;
4391
+ }
4392
+ if (!wh.name)
4393
+ problems.push(`#${i} missing "name"`);
4394
+ if (!wh.path)
4395
+ problems.push(`#${i} missing "path"`);
4396
+ else if (!wh.path.startsWith("/"))
4397
+ problems.push(`"${wh.name ?? i}" path must start with "/" (got "${wh.path}")`);
4398
+ if (wh.name && seen.has(wh.name))
4399
+ problems.push(`duplicate name "${wh.name}"`);
4400
+ if (wh.name)
4401
+ seen.add(wh.name);
4402
+ });
4403
+ if (problems.length) {
4404
+ checks.push({ name: "Webhooks", status: "fail", message: problems.join("; ") });
4405
+ } else {
4406
+ checks.push({ name: "Webhooks", status: "pass", message: `${config.webhooks.length} declared` });
4407
+ }
4408
+ }
4409
+ }
4379
4410
  const linked = resolveSlug();
4380
4411
  if (linked && linked !== config.slug) {
4381
4412
  checks.push({ name: "Project link", status: "warn", message: `bagdock.json slug "${config.slug}" differs from linked project "${linked}"` });
@@ -4638,16 +4669,40 @@ function requireConfig() {
4638
4669
  }
4639
4670
  return config;
4640
4671
  }
4641
- async function envList() {
4672
+ function parseTarget(target) {
4673
+ if (!target)
4674
+ return;
4675
+ const t = target.toLowerCase();
4676
+ if (t === "staging")
4677
+ return ["staging"];
4678
+ if (t === "production")
4679
+ return ["production"];
4680
+ if (t === "both")
4681
+ return ["staging", "production"];
4682
+ console.error(source_default.red(`Invalid --target: ${target}. Use staging, production, or both.`));
4683
+ process.exit(1);
4684
+ }
4685
+ async function envList(opts) {
4642
4686
  const config = requireConfig();
4643
4687
  try {
4688
+ if (opts?.reconcile) {
4689
+ status("Reconciling with Cloudflare...");
4690
+ const reconcileRes = await apiFetchJson(`/api/v1/developer/apps/${config.slug}/env/reconcile`, "POST", {});
4691
+ if (!reconcileRes.ok) {
4692
+ console.error(source_default.yellow(`Reconcile failed (${reconcileRes.status}) — showing cached data`));
4693
+ }
4694
+ }
4644
4695
  const res = await apiFetch(`/api/v1/developer/apps/${config.slug}/env`);
4645
4696
  if (!res.ok) {
4646
4697
  console.error(source_default.red(`Failed to list env vars (${res.status})`));
4647
4698
  process.exit(1);
4648
4699
  }
4649
- const { data } = await res.json();
4650
- if (!data.length) {
4700
+ const body = await res.json();
4701
+ if (isJsonMode()) {
4702
+ outputSuccess(body);
4703
+ return;
4704
+ }
4705
+ if (!body.data.length) {
4651
4706
  console.log(source_default.yellow("No environment variables set."));
4652
4707
  console.log("Use", source_default.cyan("bagdock env set <KEY> <VALUE>"), "to add one.");
4653
4708
  return;
@@ -4655,8 +4710,13 @@ async function envList() {
4655
4710
  console.log(source_default.bold(`
4656
4711
  Environment variables for ${config.slug}:
4657
4712
  `));
4658
- for (const v of data) {
4659
- console.log(` ${source_default.cyan(v.key)} ${source_default.dim(`(updated ${v.updatedAt})`)}`);
4713
+ for (const v of body.data) {
4714
+ const envLabel = v.environments?.length ? source_default.dim(`[${v.environments.join(", ")}]`) : source_default.dim("[no target]");
4715
+ console.log(` ${source_default.cyan(v.key)} ${envLabel} ${source_default.dim(`updated ${v.updatedAt}`)}`);
4716
+ }
4717
+ if (body.last_reconciled_at) {
4718
+ console.log(source_default.dim(`
4719
+ Last synced with Cloudflare: ${body.last_reconciled_at}`));
4660
4720
  }
4661
4721
  console.log();
4662
4722
  } catch (err) {
@@ -4664,22 +4724,40 @@ Environment variables for ${config.slug}:
4664
4724
  process.exit(1);
4665
4725
  }
4666
4726
  }
4667
- async function envSet(key, value) {
4727
+ async function envSet(key, value, opts) {
4668
4728
  const config = requireConfig();
4729
+ const environments = parseTarget(opts?.target);
4669
4730
  try {
4670
- const res = await apiFetchJson(`/api/v1/developer/apps/${config.slug}/env`, "PUT", { key, value });
4731
+ const payload = { key, value };
4732
+ if (environments)
4733
+ payload.environments = environments;
4734
+ const res = await apiFetchJson(`/api/v1/developer/apps/${config.slug}/env`, "PUT", payload);
4671
4735
  if (!res.ok) {
4672
4736
  const body = await res.text();
4673
4737
  console.error(source_default.red(`Failed to set ${key} (${res.status}):`), body.slice(0, 200));
4674
4738
  process.exit(1);
4675
4739
  }
4676
- console.log(source_default.green(`Set ${key}`), source_default.dim("— will take effect on next deploy"));
4740
+ const result = await res.json();
4741
+ if (isJsonMode()) {
4742
+ outputSuccess(result);
4743
+ return;
4744
+ }
4745
+ if (result.status === "partial") {
4746
+ console.log(source_default.yellow(`Partially set ${key}:`));
4747
+ for (const [env2, status2] of Object.entries(result.environments ?? {})) {
4748
+ const icon = status2 === "ok" ? source_default.green("✓") : source_default.red("✗");
4749
+ console.log(` ${icon} ${env2}: ${status2}`);
4750
+ }
4751
+ } else {
4752
+ const targetLabel = environments ? ` (${environments.join(", ")})` : "";
4753
+ console.log(source_default.green(`Set ${key}${targetLabel}`));
4754
+ }
4677
4755
  } catch (err) {
4678
4756
  console.error(source_default.red("Failed:"), err.message);
4679
4757
  process.exit(1);
4680
4758
  }
4681
4759
  }
4682
- async function envRemove(key) {
4760
+ async function envRemove(key, opts) {
4683
4761
  const config = requireConfig();
4684
4762
  try {
4685
4763
  const res = await apiFetch(`/api/v1/developer/apps/${config.slug}/env/${key}`, { method: "DELETE" });
@@ -4687,7 +4765,12 @@ async function envRemove(key) {
4687
4765
  console.error(source_default.red(`Failed to remove ${key} (${res.status})`));
4688
4766
  process.exit(1);
4689
4767
  }
4690
- console.log(source_default.green(`Removed ${key}`), source_default.dim("— will take effect on next deploy"));
4768
+ if (isJsonMode()) {
4769
+ const result = await res.json();
4770
+ outputSuccess(result);
4771
+ return;
4772
+ }
4773
+ console.log(source_default.green(`Removed ${key}`));
4691
4774
  } catch (err) {
4692
4775
  console.error(source_default.red("Failed:"), err.message);
4693
4776
  process.exit(1);
@@ -5081,8 +5164,19 @@ async function init(dir, opts) {
5081
5164
  const template = selectTemplate(type, kind, slug);
5082
5165
  writeFileSync2(entryFile, template);
5083
5166
  }
5167
+ if (type === "edge" && kind === "comms") {
5168
+ const typesFile = join2(srcDir, "types.ts");
5169
+ if (!existsSync2(typesFile)) {
5170
+ writeFileSync2(typesFile, COMMS_TYPES_TEMPLATE());
5171
+ }
5172
+ const verifyFile = join2(srcDir, "verify.ts");
5173
+ if (!existsSync2(verifyFile)) {
5174
+ writeFileSync2(verifyFile, COMMS_VERIFY_TEMPLATE());
5175
+ }
5176
+ }
5084
5177
  const pkgFile = join2(projectDir, "package.json");
5085
5178
  if (!existsSync2(pkgFile)) {
5179
+ const deps = {};
5086
5180
  const devDeps = {
5087
5181
  "@cloudflare/workers-types": "^4.20240909.0",
5088
5182
  typescript: "^5.3.3"
@@ -5090,11 +5184,15 @@ async function init(dir, opts) {
5090
5184
  if (type === "edge" && kind === "adapter") {
5091
5185
  devDeps["@bagdock/adapter-worker-template"] = "workspace:*";
5092
5186
  }
5187
+ if (type === "edge" && kind === "comms") {
5188
+ deps["@bagdock/worker-sdk"] = "^0.1.0";
5189
+ }
5093
5190
  writeFileSync2(pkgFile, JSON.stringify({
5094
5191
  name: slug,
5095
5192
  version: "0.1.0",
5096
5193
  private: true,
5097
5194
  main: "src/index.ts",
5195
+ ...Object.keys(deps).length ? { dependencies: deps } : {},
5098
5196
  devDependencies: devDeps
5099
5197
  }, null, 2));
5100
5198
  }
@@ -5103,12 +5201,17 @@ async function init(dir, opts) {
5103
5201
  Initialised Bagdock ${label} project!
5104
5202
  `));
5105
5203
  console.log("Created:");
5106
- console.log(` ${source_default.cyan("bagdock.json")} — project config`);
5107
- console.log(` ${source_default.cyan("src/index.ts")} — worker entry point`);
5204
+ console.log(` ${source_default.cyan("bagdock.json")} — project config`);
5205
+ console.log(` ${source_default.cyan("src/index.ts")} — worker entry point`);
5206
+ if (type === "edge" && kind === "comms") {
5207
+ console.log(` ${source_default.cyan("src/types.ts")} — environment bindings`);
5208
+ console.log(` ${source_default.cyan("src/verify.ts")} — webhook verification (customise for your vendor)`);
5209
+ }
5108
5210
  console.log();
5109
5211
  console.log("Next steps:");
5110
- console.log(` 1. ${source_default.cyan("bagdock dev")} start local dev server`);
5111
- console.log(` 2. ${source_default.cyan("bagdock deploy")} deploy to Bagdock platform`);
5212
+ console.log(` 1. ${source_default.cyan("bun install")} install dependencies`);
5213
+ console.log(` 2. ${source_default.cyan("bagdock dev")} start local dev server`);
5214
+ console.log(` 3. ${source_default.cyan("bagdock deploy")} — deploy to Bagdock platform`);
5112
5215
  }
5113
5216
  function resolveKind(type, kindOpt) {
5114
5217
  if (kindOpt)
@@ -5154,33 +5257,75 @@ export default createAdapterWorker(new ${toPascalCase(slug)}Adapter())
5154
5257
  `;
5155
5258
  }
5156
5259
  function COMMS_TEMPLATE(slug) {
5157
- return `/**
5158
- * ${toPascalCase(slug)} Communications edge worker.
5159
- *
5160
- * Handles SMS, Voice, or Telephony actions dispatched by the workflow engine.
5161
- */
5260
+ return `import { createCommsWorker } from '@bagdock/worker-sdk'
5261
+ import type { HandlerContext } from '@bagdock/worker-sdk'
5262
+ import type { Env } from './types'
5263
+ import { vendorWebhookVerify } from './verify'
5162
5264
 
5163
- interface Env {
5164
- API_KEY: string
5265
+ async function handleSmsSend(ctx: HandlerContext<Env>): Promise<Response> {
5266
+ const { to, body } = await ctx.request.json() as { to: string; body: string }
5267
+ // TODO: call your vendor's SMS API using ctx.env for secrets
5268
+ return Response.json({ id: crypto.randomUUID(), status: 'queued', provider: '${slug}', from: '', to })
5165
5269
  }
5166
5270
 
5167
- export default {
5168
- async fetch(request: Request, env: Env): Promise<Response> {
5169
- const url = new URL(request.url)
5170
- const action = url.pathname.replace(/^\\//, '')
5271
+ async function handleSmsWebhook(ctx: HandlerContext<Env>): Promise<Response> {
5272
+ const payload = await ctx.request.json()
5273
+ ctx.logger.info('webhook.sms', { payload })
5274
+ return Response.json({ received: true })
5275
+ }
5171
5276
 
5172
- if (action === 'send-sms') {
5173
- const body = await request.json() as { to: string; text: string; from?: string }
5174
- return Response.json({ id: crypto.randomUUID(), status: 'queued' })
5175
- }
5277
+ export default createCommsWorker<Env, readonly ['sms']>({
5278
+ version: '0.1.0',
5279
+ capabilities: ['sms'],
5176
5280
 
5177
- if (action === 'health') {
5178
- return Response.json({ status: 'healthy', version: '0.1.0' })
5179
- }
5281
+ async onInstall(ctx) {
5282
+ // TODO: provision vendor resources
5283
+ ctx.logger.info('lifecycle.install', { operatorId: ctx.operatorId })
5284
+ return { installation_state: { provisioned: true } }
5285
+ },
5286
+
5287
+ async onUninstall(ctx) {
5288
+ // TODO: clean up vendor resources
5289
+ ctx.logger.info('lifecycle.uninstall', { operatorId: ctx.operatorId })
5290
+ },
5180
5291
 
5181
- return Response.json({ error: 'Unknown action', action }, { status: 404 })
5292
+ routes: {
5293
+ 'sms/send': handleSmsSend,
5294
+ 'webhooks/sms': { handler: handleSmsWebhook, verify: vendorWebhookVerify },
5182
5295
  },
5296
+ })
5297
+ `;
5183
5298
  }
5299
+ function COMMS_TYPES_TEMPLATE() {
5300
+ return `import type { BaseEnv } from '@bagdock/worker-sdk'
5301
+
5302
+ export interface Env extends BaseEnv {
5303
+ ADAPTER_NAME: string
5304
+ PROVIDER_SLUG: string
5305
+ API_KEY: string
5306
+ WEBHOOK_SECRET: string
5307
+ OPERATOR_CONFIG?: KVNamespace
5308
+ }
5309
+ `;
5310
+ }
5311
+ function COMMS_VERIFY_TEMPLATE() {
5312
+ return `import { hmacSha256Verify } from '@bagdock/worker-sdk'
5313
+ import type { VerifyFunction } from '@bagdock/worker-sdk'
5314
+ import type { Env } from './types'
5315
+
5316
+ /**
5317
+ * Adapter-local webhook verification.
5318
+ *
5319
+ * Replace with your vendor's signing method. See the @bagdock/worker-sdk
5320
+ * README for examples using vendor SDKs or the ed25519Verify primitive.
5321
+ */
5322
+ export const vendorWebhookVerify: VerifyFunction<Env> = (request, env, rawBody) =>
5323
+ hmacSha256Verify({
5324
+ signature: request.headers.get('x-webhook-signature'),
5325
+ secret: env.WEBHOOK_SECRET,
5326
+ signingString: rawBody,
5327
+ timestamp: request.headers.get('x-webhook-timestamp'),
5328
+ })
5184
5329
  `;
5185
5330
  }
5186
5331
  function WEBHOOK_TEMPLATE(slug) {
@@ -5358,17 +5503,17 @@ program2.command("link").description("Link current directory to a Bagdock app or
5358
5503
  await link2(opts);
5359
5504
  });
5360
5505
  var envCmd = program2.command("env").description("Manage app environment variables");
5361
- envCmd.command("list").description("List environment variables for this app").action(async () => {
5506
+ envCmd.command("list").description("List environment variables for this app").option("--reconcile", "Force sync with Cloudflare before listing").action(async (opts) => {
5362
5507
  const { envList: envList2 } = await Promise.resolve().then(() => (init_env_cmd(), exports_env_cmd));
5363
- await envList2();
5508
+ await envList2({ reconcile: opts.reconcile });
5364
5509
  });
5365
- envCmd.command("set <key> <value>").description("Set an environment variable").action(async (key, value) => {
5510
+ envCmd.command("set <key> <value>").description("Set an environment variable").option("--target <target>", "Target namespace: staging, production, or both (default: both)").action(async (key, value, opts) => {
5366
5511
  const { envSet: envSet2 } = await Promise.resolve().then(() => (init_env_cmd(), exports_env_cmd));
5367
- await envSet2(key, value);
5512
+ await envSet2(key, value, { target: opts.target });
5368
5513
  });
5369
- envCmd.command("remove <key>").description("Remove an environment variable").action(async (key) => {
5514
+ envCmd.command("remove <key>").description("Remove an environment variable").option("--target <target>", "Target namespace: staging, production, or both (default: both)").action(async (key, opts) => {
5370
5515
  const { envRemove: envRemove2 } = await Promise.resolve().then(() => (init_env_cmd(), exports_env_cmd));
5371
- await envRemove2(key);
5516
+ await envRemove2(key, { target: opts.target });
5372
5517
  });
5373
5518
  envCmd.command("pull [file]").description("Pull remote env var keys to a local .env file").action(async (file) => {
5374
5519
  const { envPull: envPull2 } = await Promise.resolve().then(() => (init_env_cmd(), exports_env_cmd));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bagdock/cli",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Bagdock developer CLI — build, test, and deploy apps and edges on the Bagdock platform",
5
5
  "keywords": [
6
6
  "bagdock",
@@ -17,7 +17,7 @@
17
17
  "type": "git",
18
18
  "url": "git+https://github.com/bagdock/bagdock-cli.git"
19
19
  },
20
- "homepage": "https://github.com/bagdock/bagdock-cli#readme",
20
+ "homepage": "https://bagdock.com/docs/cli",
21
21
  "bugs": {
22
22
  "url": "https://github.com/bagdock/bagdock-cli/issues"
23
23
  },