@bagdock/cli 0.6.0 → 0.7.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 +164 -42
  2. package/package.json +1 -1
package/dist/bagdock.js CHANGED
@@ -2580,6 +2580,14 @@ function getApiBase() {
2580
2580
  function getDashboardBase() {
2581
2581
  return _dashboardBase;
2582
2582
  }
2583
+ function normalizeUrl(raw) {
2584
+ let url = raw.replace(/^['"]|['"]$/g, "").replace(/\/+$/, "");
2585
+ if (!/^https?:\/\//i.test(url)) {
2586
+ console.error(source_default.red(`Invalid URL (must start with http:// or https://): ${raw}`));
2587
+ process.exit(1);
2588
+ }
2589
+ return url;
2590
+ }
2583
2591
  function loadLocalEnv() {
2584
2592
  const envPath = join(process.cwd(), ".env.local");
2585
2593
  if (!existsSync(envPath)) {
@@ -2605,9 +2613,9 @@ function loadLocalEnv() {
2605
2613
  console.error(source_default.dim("Add BAGDOCK_API_URL=https://your-ngrok-url to .env.local"));
2606
2614
  process.exit(1);
2607
2615
  }
2608
- _apiBase = apiUrl;
2616
+ _apiBase = normalizeUrl(apiUrl);
2609
2617
  if (vars["BAGDOCK_DASHBOARD_URL"]) {
2610
- _dashboardBase = vars["BAGDOCK_DASHBOARD_URL"];
2618
+ _dashboardBase = normalizeUrl(vars["BAGDOCK_DASHBOARD_URL"]);
2611
2619
  }
2612
2620
  console.log(source_default.dim(`Using local env → ${_apiBase}`));
2613
2621
  }
@@ -4630,16 +4638,40 @@ function requireConfig() {
4630
4638
  }
4631
4639
  return config;
4632
4640
  }
4633
- async function envList() {
4641
+ function parseTarget(target) {
4642
+ if (!target)
4643
+ return;
4644
+ const t = target.toLowerCase();
4645
+ if (t === "staging")
4646
+ return ["staging"];
4647
+ if (t === "production")
4648
+ return ["production"];
4649
+ if (t === "both")
4650
+ return ["staging", "production"];
4651
+ console.error(source_default.red(`Invalid --target: ${target}. Use staging, production, or both.`));
4652
+ process.exit(1);
4653
+ }
4654
+ async function envList(opts) {
4634
4655
  const config = requireConfig();
4635
4656
  try {
4657
+ if (opts?.reconcile) {
4658
+ status("Reconciling with Cloudflare...");
4659
+ const reconcileRes = await apiFetchJson(`/api/v1/developer/apps/${config.slug}/env/reconcile`, "POST", {});
4660
+ if (!reconcileRes.ok) {
4661
+ console.error(source_default.yellow(`Reconcile failed (${reconcileRes.status}) — showing cached data`));
4662
+ }
4663
+ }
4636
4664
  const res = await apiFetch(`/api/v1/developer/apps/${config.slug}/env`);
4637
4665
  if (!res.ok) {
4638
4666
  console.error(source_default.red(`Failed to list env vars (${res.status})`));
4639
4667
  process.exit(1);
4640
4668
  }
4641
- const { data } = await res.json();
4642
- if (!data.length) {
4669
+ const body = await res.json();
4670
+ if (isJsonMode()) {
4671
+ outputSuccess(body);
4672
+ return;
4673
+ }
4674
+ if (!body.data.length) {
4643
4675
  console.log(source_default.yellow("No environment variables set."));
4644
4676
  console.log("Use", source_default.cyan("bagdock env set <KEY> <VALUE>"), "to add one.");
4645
4677
  return;
@@ -4647,8 +4679,13 @@ async function envList() {
4647
4679
  console.log(source_default.bold(`
4648
4680
  Environment variables for ${config.slug}:
4649
4681
  `));
4650
- for (const v of data) {
4651
- console.log(` ${source_default.cyan(v.key)} ${source_default.dim(`(updated ${v.updatedAt})`)}`);
4682
+ for (const v of body.data) {
4683
+ const envLabel = v.environments?.length ? source_default.dim(`[${v.environments.join(", ")}]`) : source_default.dim("[no target]");
4684
+ console.log(` ${source_default.cyan(v.key)} ${envLabel} ${source_default.dim(`updated ${v.updatedAt}`)}`);
4685
+ }
4686
+ if (body.last_reconciled_at) {
4687
+ console.log(source_default.dim(`
4688
+ Last synced with Cloudflare: ${body.last_reconciled_at}`));
4652
4689
  }
4653
4690
  console.log();
4654
4691
  } catch (err) {
@@ -4656,22 +4693,40 @@ Environment variables for ${config.slug}:
4656
4693
  process.exit(1);
4657
4694
  }
4658
4695
  }
4659
- async function envSet(key, value) {
4696
+ async function envSet(key, value, opts) {
4660
4697
  const config = requireConfig();
4698
+ const environments = parseTarget(opts?.target);
4661
4699
  try {
4662
- const res = await apiFetchJson(`/api/v1/developer/apps/${config.slug}/env`, "PUT", { key, value });
4700
+ const payload = { key, value };
4701
+ if (environments)
4702
+ payload.environments = environments;
4703
+ const res = await apiFetchJson(`/api/v1/developer/apps/${config.slug}/env`, "PUT", payload);
4663
4704
  if (!res.ok) {
4664
4705
  const body = await res.text();
4665
4706
  console.error(source_default.red(`Failed to set ${key} (${res.status}):`), body.slice(0, 200));
4666
4707
  process.exit(1);
4667
4708
  }
4668
- console.log(source_default.green(`Set ${key}`), source_default.dim("— will take effect on next deploy"));
4709
+ const result = await res.json();
4710
+ if (isJsonMode()) {
4711
+ outputSuccess(result);
4712
+ return;
4713
+ }
4714
+ if (result.status === "partial") {
4715
+ console.log(source_default.yellow(`Partially set ${key}:`));
4716
+ for (const [env2, status2] of Object.entries(result.environments ?? {})) {
4717
+ const icon = status2 === "ok" ? source_default.green("✓") : source_default.red("✗");
4718
+ console.log(` ${icon} ${env2}: ${status2}`);
4719
+ }
4720
+ } else {
4721
+ const targetLabel = environments ? ` (${environments.join(", ")})` : "";
4722
+ console.log(source_default.green(`Set ${key}${targetLabel}`));
4723
+ }
4669
4724
  } catch (err) {
4670
4725
  console.error(source_default.red("Failed:"), err.message);
4671
4726
  process.exit(1);
4672
4727
  }
4673
4728
  }
4674
- async function envRemove(key) {
4729
+ async function envRemove(key, opts) {
4675
4730
  const config = requireConfig();
4676
4731
  try {
4677
4732
  const res = await apiFetch(`/api/v1/developer/apps/${config.slug}/env/${key}`, { method: "DELETE" });
@@ -4679,7 +4734,12 @@ async function envRemove(key) {
4679
4734
  console.error(source_default.red(`Failed to remove ${key} (${res.status})`));
4680
4735
  process.exit(1);
4681
4736
  }
4682
- console.log(source_default.green(`Removed ${key}`), source_default.dim("— will take effect on next deploy"));
4737
+ if (isJsonMode()) {
4738
+ const result = await res.json();
4739
+ outputSuccess(result);
4740
+ return;
4741
+ }
4742
+ console.log(source_default.green(`Removed ${key}`));
4683
4743
  } catch (err) {
4684
4744
  console.error(source_default.red("Failed:"), err.message);
4685
4745
  process.exit(1);
@@ -5073,8 +5133,19 @@ async function init(dir, opts) {
5073
5133
  const template = selectTemplate(type, kind, slug);
5074
5134
  writeFileSync2(entryFile, template);
5075
5135
  }
5136
+ if (type === "edge" && kind === "comms") {
5137
+ const typesFile = join2(srcDir, "types.ts");
5138
+ if (!existsSync2(typesFile)) {
5139
+ writeFileSync2(typesFile, COMMS_TYPES_TEMPLATE());
5140
+ }
5141
+ const verifyFile = join2(srcDir, "verify.ts");
5142
+ if (!existsSync2(verifyFile)) {
5143
+ writeFileSync2(verifyFile, COMMS_VERIFY_TEMPLATE());
5144
+ }
5145
+ }
5076
5146
  const pkgFile = join2(projectDir, "package.json");
5077
5147
  if (!existsSync2(pkgFile)) {
5148
+ const deps = {};
5078
5149
  const devDeps = {
5079
5150
  "@cloudflare/workers-types": "^4.20240909.0",
5080
5151
  typescript: "^5.3.3"
@@ -5082,11 +5153,15 @@ async function init(dir, opts) {
5082
5153
  if (type === "edge" && kind === "adapter") {
5083
5154
  devDeps["@bagdock/adapter-worker-template"] = "workspace:*";
5084
5155
  }
5156
+ if (type === "edge" && kind === "comms") {
5157
+ deps["@bagdock/worker-sdk"] = "^0.1.0";
5158
+ }
5085
5159
  writeFileSync2(pkgFile, JSON.stringify({
5086
5160
  name: slug,
5087
5161
  version: "0.1.0",
5088
5162
  private: true,
5089
5163
  main: "src/index.ts",
5164
+ ...Object.keys(deps).length ? { dependencies: deps } : {},
5090
5165
  devDependencies: devDeps
5091
5166
  }, null, 2));
5092
5167
  }
@@ -5095,12 +5170,17 @@ async function init(dir, opts) {
5095
5170
  Initialised Bagdock ${label} project!
5096
5171
  `));
5097
5172
  console.log("Created:");
5098
- console.log(` ${source_default.cyan("bagdock.json")} — project config`);
5099
- console.log(` ${source_default.cyan("src/index.ts")} — worker entry point`);
5173
+ console.log(` ${source_default.cyan("bagdock.json")} — project config`);
5174
+ console.log(` ${source_default.cyan("src/index.ts")} — worker entry point`);
5175
+ if (type === "edge" && kind === "comms") {
5176
+ console.log(` ${source_default.cyan("src/types.ts")} — environment bindings`);
5177
+ console.log(` ${source_default.cyan("src/verify.ts")} — webhook verification (customise for your vendor)`);
5178
+ }
5100
5179
  console.log();
5101
5180
  console.log("Next steps:");
5102
- console.log(` 1. ${source_default.cyan("bagdock dev")} start local dev server`);
5103
- console.log(` 2. ${source_default.cyan("bagdock deploy")} deploy to Bagdock platform`);
5181
+ console.log(` 1. ${source_default.cyan("bun install")} install dependencies`);
5182
+ console.log(` 2. ${source_default.cyan("bagdock dev")} start local dev server`);
5183
+ console.log(` 3. ${source_default.cyan("bagdock deploy")} — deploy to Bagdock platform`);
5104
5184
  }
5105
5185
  function resolveKind(type, kindOpt) {
5106
5186
  if (kindOpt)
@@ -5146,35 +5226,77 @@ export default createAdapterWorker(new ${toPascalCase(slug)}Adapter())
5146
5226
  `;
5147
5227
  }
5148
5228
  function COMMS_TEMPLATE(slug) {
5149
- return `/**
5150
- * ${toPascalCase(slug)} Communications edge worker.
5151
- *
5152
- * Handles SMS, Voice, or Telephony actions dispatched by the workflow engine.
5153
- */
5229
+ return `import { createCommsWorker } from '@bagdock/worker-sdk'
5230
+ import type { HandlerContext } from '@bagdock/worker-sdk'
5231
+ import type { Env } from './types'
5232
+ import { vendorWebhookVerify } from './verify'
5154
5233
 
5155
- interface Env {
5156
- API_KEY: string
5234
+ async function handleSmsSend(ctx: HandlerContext<Env>): Promise<Response> {
5235
+ const { to, body } = await ctx.request.json() as { to: string; body: string }
5236
+ // TODO: call your vendor's SMS API using ctx.env for secrets
5237
+ return Response.json({ id: crypto.randomUUID(), status: 'queued', provider: '${slug}', from: '', to })
5157
5238
  }
5158
5239
 
5159
- export default {
5160
- async fetch(request: Request, env: Env): Promise<Response> {
5161
- const url = new URL(request.url)
5162
- const action = url.pathname.replace(/^\\//, '')
5240
+ async function handleSmsWebhook(ctx: HandlerContext<Env>): Promise<Response> {
5241
+ const payload = await ctx.request.json()
5242
+ ctx.logger.info('webhook.sms', { payload })
5243
+ return Response.json({ received: true })
5244
+ }
5163
5245
 
5164
- if (action === 'send-sms') {
5165
- const body = await request.json() as { to: string; text: string; from?: string }
5166
- return Response.json({ id: crypto.randomUUID(), status: 'queued' })
5167
- }
5246
+ export default createCommsWorker<Env, readonly ['sms']>({
5247
+ version: '0.1.0',
5248
+ capabilities: ['sms'],
5168
5249
 
5169
- if (action === 'health') {
5170
- return Response.json({ status: 'healthy', version: '0.1.0' })
5171
- }
5250
+ async onInstall(ctx) {
5251
+ // TODO: provision vendor resources
5252
+ ctx.logger.info('lifecycle.install', { operatorId: ctx.operatorId })
5253
+ return { installation_state: { provisioned: true } }
5254
+ },
5172
5255
 
5173
- return Response.json({ error: 'Unknown action', action }, { status: 404 })
5256
+ async onUninstall(ctx) {
5257
+ // TODO: clean up vendor resources
5258
+ ctx.logger.info('lifecycle.uninstall', { operatorId: ctx.operatorId })
5174
5259
  },
5260
+
5261
+ routes: {
5262
+ 'sms/send': handleSmsSend,
5263
+ 'webhooks/sms': { handler: handleSmsWebhook, verify: vendorWebhookVerify },
5264
+ },
5265
+ })
5266
+ `;
5267
+ }
5268
+ function COMMS_TYPES_TEMPLATE() {
5269
+ return `import type { BaseEnv } from '@bagdock/worker-sdk'
5270
+
5271
+ export interface Env extends BaseEnv {
5272
+ ADAPTER_NAME: string
5273
+ PROVIDER_SLUG: string
5274
+ API_KEY: string
5275
+ WEBHOOK_SECRET: string
5276
+ OPERATOR_CONFIG?: KVNamespace
5175
5277
  }
5176
5278
  `;
5177
5279
  }
5280
+ function COMMS_VERIFY_TEMPLATE() {
5281
+ return `import { hmacSha256Verify } from '@bagdock/worker-sdk'
5282
+ import type { VerifyFunction } from '@bagdock/worker-sdk'
5283
+ import type { Env } from './types'
5284
+
5285
+ /**
5286
+ * Adapter-local webhook verification.
5287
+ *
5288
+ * Replace with your vendor's signing method. See the @bagdock/worker-sdk
5289
+ * README for examples using vendor SDKs or the ed25519Verify primitive.
5290
+ */
5291
+ export const vendorWebhookVerify: VerifyFunction<Env> = (request, env, rawBody) =>
5292
+ hmacSha256Verify({
5293
+ signature: request.headers.get('x-webhook-signature'),
5294
+ secret: env.WEBHOOK_SECRET,
5295
+ signingString: rawBody,
5296
+ timestamp: request.headers.get('x-webhook-timestamp'),
5297
+ })
5298
+ `;
5299
+ }
5178
5300
  function WEBHOOK_TEMPLATE(slug) {
5179
5301
  return `/**
5180
5302
  * ${toPascalCase(slug)} — Webhook handler edge worker.
@@ -5278,9 +5400,9 @@ init_config();
5278
5400
  var program2 = new Command;
5279
5401
  program2.name("bagdock").description("Bagdock developer CLI — built for humans, AI agents, and CI/CD pipelines").version("0.6.0").option("--json", "Force JSON output (auto-enabled in non-TTY)").option("-q, --quiet", "Suppress status messages (implies --json)").option("--api-key <key>", "API key to use for this invocation").option("-p, --profile <name>", "Profile to use (overrides BAGDOCK_PROFILE)").option("--env <environment>", "Override environment for this invocation (live, test)").option("--ngrok", "Use API URLs from .env.local (for ngrok tunnels)").hook("preAction", (_thisCommand, actionCommand) => {
5280
5402
  const opts = program2.opts();
5403
+ setOutputMode({ json: opts.json, quiet: opts.quiet });
5281
5404
  if (opts.ngrok)
5282
5405
  loadLocalEnv();
5283
- setOutputMode({ json: opts.json, quiet: opts.quiet });
5284
5406
  if (opts.apiKey)
5285
5407
  setApiKeyOverride(opts.apiKey);
5286
5408
  if (opts.profile)
@@ -5350,17 +5472,17 @@ program2.command("link").description("Link current directory to a Bagdock app or
5350
5472
  await link2(opts);
5351
5473
  });
5352
5474
  var envCmd = program2.command("env").description("Manage app environment variables");
5353
- envCmd.command("list").description("List environment variables for this app").action(async () => {
5475
+ envCmd.command("list").description("List environment variables for this app").option("--reconcile", "Force sync with Cloudflare before listing").action(async (opts) => {
5354
5476
  const { envList: envList2 } = await Promise.resolve().then(() => (init_env_cmd(), exports_env_cmd));
5355
- await envList2();
5477
+ await envList2({ reconcile: opts.reconcile });
5356
5478
  });
5357
- envCmd.command("set <key> <value>").description("Set an environment variable").action(async (key, value) => {
5479
+ 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) => {
5358
5480
  const { envSet: envSet2 } = await Promise.resolve().then(() => (init_env_cmd(), exports_env_cmd));
5359
- await envSet2(key, value);
5481
+ await envSet2(key, value, { target: opts.target });
5360
5482
  });
5361
- envCmd.command("remove <key>").description("Remove an environment variable").action(async (key) => {
5483
+ envCmd.command("remove <key>").description("Remove an environment variable").option("--target <target>", "Target namespace: staging, production, or both (default: both)").action(async (key, opts) => {
5362
5484
  const { envRemove: envRemove2 } = await Promise.resolve().then(() => (init_env_cmd(), exports_env_cmd));
5363
- await envRemove2(key);
5485
+ await envRemove2(key, { target: opts.target });
5364
5486
  });
5365
5487
  envCmd.command("pull [file]").description("Pull remote env var keys to a local .env file").action(async (file) => {
5366
5488
  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.0",
3
+ "version": "0.7.0",
4
4
  "description": "Bagdock developer CLI — build, test, and deploy apps and edges on the Bagdock platform",
5
5
  "keywords": [
6
6
  "bagdock",