@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.
- package/dist/bagdock.js +164 -42
- 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
|
-
|
|
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
|
|
4642
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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")}
|
|
5099
|
-
console.log(` ${source_default.cyan("src/index.ts")}
|
|
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("
|
|
5103
|
-
console.log(` 2. ${source_default.cyan("bagdock
|
|
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
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
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
|
-
|
|
5156
|
-
|
|
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
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
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
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
}
|
|
5246
|
+
export default createCommsWorker<Env, readonly ['sms']>({
|
|
5247
|
+
version: '0.1.0',
|
|
5248
|
+
capabilities: ['sms'],
|
|
5168
5249
|
|
|
5169
|
-
|
|
5170
|
-
|
|
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
|
-
|
|
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));
|