@biffo/cli 0.108.3 → 0.108.4

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/index.js +77 -334
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command26 } from "commander";
4
+ import { Command as Command25 } from "commander";
5
5
 
6
6
  // src/commands/core.ts
7
7
  import { Command as Command4 } from "commander";
@@ -819,7 +819,7 @@ var GitHubAdapter = class {
819
819
  } catch (err) {
820
820
  if (err.status !== 404) throw err;
821
821
  }
822
- await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
822
+ await new Promise((resolve18) => setTimeout(resolve18, intervalMs));
823
823
  }
824
824
  throw new Error(
825
825
  `Branch "${branch}" not found in ${org}/${repo} after ${timeoutMs / 1e3}s \u2014 GitHub template generation may have stalled. Check the repository and re-run biffo init.`
@@ -834,7 +834,7 @@ var GitHubAdapter = class {
834
834
  } catch (err) {
835
835
  if (err.status !== 404) throw err;
836
836
  }
837
- await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
837
+ await new Promise((resolve18) => setTimeout(resolve18, intervalMs));
838
838
  }
839
839
  throw new Error(
840
840
  `Ref "${ref}" not found in ${org}/${repo} after ${timeoutMs / 1e3}s \u2014 GitHub template generation may have stalled. Check the repository and re-run biffo init.`
@@ -1039,7 +1039,7 @@ var GitHubAdapter = class {
1039
1039
  }
1040
1040
  if (status !== 404 || Date.now() >= deadline) throw err;
1041
1041
  log.info("Branch protection endpoint not yet ready, retrying...");
1042
- await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
1042
+ await new Promise((resolve18) => setTimeout(resolve18, protectionIntervalMs));
1043
1043
  }
1044
1044
  }
1045
1045
  }
@@ -1265,7 +1265,7 @@ var GitHubAdapter = class {
1265
1265
  } catch (err) {
1266
1266
  if (err.status !== 404 || Date.now() >= deadline) throw err;
1267
1267
  log.info(`Workflow ${workflowId} not yet indexed by GitHub Actions, retrying...`);
1268
- await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
1268
+ await new Promise((resolve18) => setTimeout(resolve18, intervalMs));
1269
1269
  }
1270
1270
  }
1271
1271
  }
@@ -1284,7 +1284,7 @@ var GitHubAdapter = class {
1284
1284
  } catch (err) {
1285
1285
  if (err.status !== 404 || Date.now() >= deadline) throw err;
1286
1286
  log.info(`Workflow ${workflowId} not yet indexed by GitHub Actions, retrying...`);
1287
- await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
1287
+ await new Promise((resolve18) => setTimeout(resolve18, intervalMs));
1288
1288
  }
1289
1289
  }
1290
1290
  }
@@ -1308,7 +1308,7 @@ var GitHubAdapter = class {
1308
1308
  } else {
1309
1309
  log.info(" Waiting for run to be queued...");
1310
1310
  }
1311
- await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
1311
+ await new Promise((resolve18) => setTimeout(resolve18, intervalMs));
1312
1312
  }
1313
1313
  throw new Error(
1314
1314
  `Workflow ${workflowId} did not complete within ${timeoutMs / 1e3 / 60} minutes`
@@ -1998,15 +1998,6 @@ function standardArguments(pluginName, handler) {
1998
1998
  ["event_bus_name", "module.events.event_bus_name"],
1999
1999
  ["core_api_url", "module.api_gateway.api_endpoint"],
2000
2000
  ["core_api_execution_arn", "module.api_gateway.execution_arn"],
2001
- // User-facing plugin inputs (ADR-0018 §1): the shared-Cognito coordinates its
2002
- // Lambda verifies the founder's JWT against, and the CloudFront distribution
2003
- // ARN its frontend bucket policy grants read to. Emitted ONLY for a plugin whose
2004
- // module declares them (declaredVariables gate), so an event-only plugin is
2005
- // unaffected — same graceful-degradation contract as core_api_execution_arn.
2006
- ["cognito_user_pool_id", "module.auth.user_pool_id"],
2007
- ["cognito_client_id", "module.auth.client_id"],
2008
- ["cognito_region", "var.aws_region"],
2009
- ["cdn_distribution_arn", "module.cdn.distribution_arn"],
2010
2001
  ["tags", "local.tags"]
2011
2002
  ];
2012
2003
  }
@@ -2092,58 +2083,13 @@ function declaredVariables(moduleDir) {
2092
2083
  }
2093
2084
  return names;
2094
2085
  }
2095
- var USER_FACING_PLUGIN_OUTPUTS = [
2096
- "function_url_domain",
2097
- "frontend_bucket_regional_domain",
2098
- "frontend_bucket_name"
2099
- ];
2100
- function rootPluginOutputName(pluginName, output) {
2101
- return `plugin_${pluginName}_${output}`;
2102
- }
2103
- function pluginOutputsFromRoot(pluginName, rootOutputs) {
2104
- const flat = {};
2105
- for (const key of USER_FACING_PLUGIN_OUTPUTS) {
2106
- const value = rootOutputs[rootPluginOutputName(pluginName, key)];
2107
- if (value !== void 0) flat[key] = value;
2108
- }
2109
- return flat;
2110
- }
2111
- function declaredOutputs(moduleDir) {
2112
- const names = /* @__PURE__ */ new Set();
2113
- let entries;
2114
- try {
2115
- entries = readdirSync3(moduleDir, { withFileTypes: true });
2116
- } catch {
2117
- return names;
2118
- }
2119
- for (const entry of entries) {
2120
- if (!entry.isFile() || !entry.name.endsWith(".tf")) continue;
2121
- let contents;
2122
- try {
2123
- contents = readFileSync7(join10(moduleDir, entry.name), "utf8");
2124
- } catch {
2125
- continue;
2126
- }
2127
- for (const match of contents.matchAll(/^\s*output\s+"([^"]+)"/gm)) {
2128
- names.add(match[1]);
2129
- }
2130
- }
2131
- return names;
2132
- }
2133
2086
  function renderArguments(args, indent) {
2134
2087
  const width = Math.max(...args.map(([key]) => key.length));
2135
2088
  return args.map(([key, value]) => `${indent}${key.padEnd(width)} = ${value}`).join("\n");
2136
2089
  }
2137
- function renderModuleBlock(pluginName, declared, handler, source, declaredOutputNames) {
2090
+ function renderModuleBlock(pluginName, declared, handler, source) {
2138
2091
  const args = standardArguments(pluginName, handler).filter(([key]) => declared.has(key));
2139
2092
  const quoted = JSON.stringify(pluginName);
2140
- const outputBlock = (name, description, attr) => [
2141
- "",
2142
- `output "${name}" {`,
2143
- ` description = "${description}"`,
2144
- ` value = try(module.plugin_${pluginName}[${quoted}].${attr}, null)`,
2145
- "}"
2146
- ];
2147
2093
  const lines = [
2148
2094
  `module "plugin_${pluginName}" {`,
2149
2095
  ` source = "${source}"`,
@@ -2151,22 +2097,12 @@ function renderModuleBlock(pluginName, declared, handler, source, declaredOutput
2151
2097
  "",
2152
2098
  renderArguments(args, " "),
2153
2099
  "}",
2154
- ...outputBlock(
2155
- `plugin_${pluginName}_function_arn`,
2156
- `Lambda ARN of the ${pluginName} plugin, or null when it is not in enabled_plugins.`,
2157
- "function_arn"
2158
- )
2100
+ "",
2101
+ `output "plugin_${pluginName}_function_arn" {`,
2102
+ ` description = "Lambda ARN of the ${pluginName} plugin, or null when it is not in enabled_plugins."`,
2103
+ ` value = try(module.plugin_${pluginName}[${quoted}].function_arn, null)`,
2104
+ "}"
2159
2105
  ];
2160
- for (const output of USER_FACING_PLUGIN_OUTPUTS) {
2161
- if (!declaredOutputNames.has(output)) continue;
2162
- lines.push(
2163
- ...outputBlock(
2164
- rootPluginOutputName(pluginName, output),
2165
- `${output.replace(/_/g, " ")} of the ${pluginName} plugin (ADR-0018 register step), or null when it is not enabled.`,
2166
- output
2167
- )
2168
- );
2169
- }
2170
2106
  return lines.join("\n");
2171
2107
  }
2172
2108
  var GENERATED_HEADER = `# ---------------------------------------------------------------------------
@@ -2197,8 +2133,7 @@ function renderGeneratedTerraform(plugins) {
2197
2133
  p.name,
2198
2134
  p.declaredVariables,
2199
2135
  p.handler ?? DEFAULT_PLUGIN_HANDLER,
2200
- p.source ?? THIRD_PARTY_TERRAFORM(p.name),
2201
- p.declaredOutputs ?? /* @__PURE__ */ new Set()
2136
+ p.source ?? THIRD_PARTY_TERRAFORM(p.name)
2202
2137
  )
2203
2138
  );
2204
2139
  return `${GENERATED_HEADER}
@@ -2219,7 +2154,6 @@ function syncPluginTerraform(cwd) {
2219
2154
  return {
2220
2155
  name,
2221
2156
  declaredVariables: declaredVariables(moduleDir),
2222
- declaredOutputs: declaredOutputs(moduleDir),
2223
2157
  source: pluginModuleSource(cwd, name)
2224
2158
  };
2225
2159
  });
@@ -2917,7 +2851,7 @@ var AwsAdapter = class {
2917
2851
  const code = err.Code;
2918
2852
  if (code === "OperationAborted" && attempt < maxAttempts) {
2919
2853
  log.info(` Waiting for S3 to release "${bucketName}"... (${attempt}/${maxAttempts})`);
2920
- await new Promise((resolve19) => setTimeout(resolve19, retryDelayMs));
2854
+ await new Promise((resolve18) => setTimeout(resolve18, retryDelayMs));
2921
2855
  } else if (code === "OperationAborted") {
2922
2856
  return false;
2923
2857
  } else {
@@ -6032,7 +5966,7 @@ async function promptForConfig(awsAccountId, awsRegion, awsProfile) {
6032
5966
  }
6033
5967
 
6034
5968
  // src/commands/plugin.ts
6035
- import { Command as Command21 } from "commander";
5969
+ import { Command as Command20 } from "commander";
6036
5970
 
6037
5971
  // src/commands/plugin-create.ts
6038
5972
  import { existsSync as existsSync23, readFileSync as readFileSync17 } from "fs";
@@ -6190,19 +6124,13 @@ var RouteDefSchema = z6.object({
6190
6124
  });
6191
6125
  }
6192
6126
  });
6193
- var USER_PATH_SEGMENT = /^[a-z][a-z0-9_-]*$/;
6194
- var HANDLER_IMPORT_PATH = /^[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)+$/;
6195
6127
  var REL_DIR = /^[\w][\w./-]*$/;
6196
6128
  var NON_EMPTY_GROUP = "required_group must be a non-empty Cognito group name.";
6197
6129
  var APP_REF = /^[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*:[a-zA-Z_]\w*$/;
6198
6130
  var UserIngressSchema = z6.object({
6199
6131
  required_group: z6.string().min(1, NON_EMPTY_GROUP),
6200
- app: z6.string().regex(APP_REF, "must be an ASGI app reference '<module>:<attr>', e.g. 'ideation.app:app'").optional(),
6201
- handler: z6.string().regex(HANDLER_IMPORT_PATH, "must be a dotted import path, e.g. 'ideation.app.handler'").optional(),
6202
- path: z6.string().regex(USER_PATH_SEGMENT, "must be a single lowercase path segment, e.g. api").default("api")
6203
- }).strict().refine((ui) => Boolean(ui.app ?? ui.handler), {
6204
- message: "user_ingress must declare `app` (ADR-0021, preferred) or `handler` (legacy)"
6205
- });
6132
+ app: z6.string().regex(APP_REF, "must be an ASGI app reference '<module>:<attr>', e.g. 'ideation.app:app'")
6133
+ }).strict();
6206
6134
  var UserFrontendSchema = z6.object({
6207
6135
  dir: z6.string().regex(
6208
6136
  REL_DIR,
@@ -7027,28 +6955,6 @@ async function runPluginInstall(target, options, deps) {
7027
6955
  console.log(chalk15.dim(` git push`));
7028
6956
  console.log(chalk15.dim(` biffo deploy <environment> --app-only
7029
6957
  `));
7030
- if (manifest.user_ingress || manifest.user_frontend) {
7031
- const group = manifest.user_ingress?.required_group ?? manifest.user_frontend?.required_group;
7032
- console.log(chalk15.bold(` "${pluginName}" is a user-facing plugin (ADR-0018).`));
7033
- console.log(
7034
- ` It serves an authenticated${group ? ` ${group}-gated` : ""} app at <base>/${pluginName}/ and an api at <base>/${pluginName}/api/*.`
7035
- );
7036
- console.log(
7037
- " Two more steps route it onto the shared CloudFront (apply is pipeline-only):\n"
7038
- );
7039
- console.log(
7040
- " 1. After the deploy above applies, register its CDN origins from the plugin module outputs, then redeploy so the CDN gains its behaviours:"
7041
- );
7042
- console.log(chalk15.dim(` biffo plugin wire ${pluginName} <environment>`));
7043
- console.log(chalk15.dim(` git push && biffo deploy <environment>`));
7044
- console.log(" 2. Build and sync its frontend to the bucket that first apply created");
7045
- console.log(
7046
- chalk15.dim(
7047
- ` (the plugin's own deploy workflow does this \u2014 see ${relTargetDir}/.github/workflows/).
7048
- `
7049
- )
7050
- );
7051
- }
7052
6958
  } finally {
7053
6959
  source.cleanup();
7054
6960
  }
@@ -7510,187 +7416,24 @@ function printDryRun6(entry, currentVersion) {
7510
7416
  `);
7511
7417
  }
7512
7418
 
7513
- // src/commands/plugin-wire.ts
7514
- import { existsSync as existsSync31, readFileSync as readFileSync24 } from "fs";
7515
- import { resolve as resolve17 } from "path";
7516
- import chalk20 from "chalk";
7517
- import { Command as Command20 } from "commander";
7518
-
7519
- // src/lib/plugin-origin.ts
7520
- import { existsSync as existsSync30, readFileSync as readFileSync23, writeFileSync as writeFileSync10 } from "fs";
7521
- import { join as join31 } from "path";
7522
- var PLUGIN_API_TFVARS = "plugin-apis.auto.tfvars.json";
7523
- var SIBLINGS_TFVARS = "siblings.auto.tfvars.json";
7524
- function upsertPluginApiOrigin(existing, entry) {
7525
- return [...existing.filter((p) => p.name !== entry.name), entry];
7526
- }
7527
- function serializePluginApiRegistry(origins) {
7528
- return JSON.stringify({ plugin_api_origins: origins }, null, 2) + "\n";
7529
- }
7530
- function readArray(path, key) {
7531
- if (!existsSync30(path)) return [];
7532
- try {
7533
- const parsed = JSON.parse(readFileSync23(path, "utf8"));
7534
- const arr = parsed[key];
7535
- return Array.isArray(arr) ? arr : [];
7536
- } catch {
7537
- return [];
7538
- }
7539
- }
7540
- function registerUserFacingPlugin(cwd, environment, reg) {
7541
- const envDir = join31(cwd, "infra", "environments", environment);
7542
- const apiPath = join31(envDir, PLUGIN_API_TFVARS);
7543
- const siblingPath = join31(envDir, SIBLINGS_TFVARS);
7544
- const apis = upsertPluginApiOrigin(readArray(apiPath, "plugin_api_origins"), {
7545
- name: reg.name,
7546
- function_url_domain: reg.functionUrlDomain
7547
- });
7548
- writeFileSync10(apiPath, serializePluginApiRegistry(apis));
7549
- const siblings = upsertSiblingOrigin(readArray(siblingPath, "sibling_origins"), {
7550
- name: reg.name,
7551
- bucket_regional_domain: reg.bucketRegionalDomain
7552
- });
7553
- writeFileSync10(siblingPath, serializeRegistry(siblings));
7554
- return [
7555
- `infra/environments/${environment}/${PLUGIN_API_TFVARS}`,
7556
- `infra/environments/${environment}/${SIBLINGS_TFVARS}`
7557
- ];
7558
- }
7559
- var REQUIRED_PLUGIN_OUTPUTS = [
7560
- "function_url_domain",
7561
- "frontend_bucket_regional_domain",
7562
- "frontend_bucket_name"
7563
- ];
7564
- function wireUserFacingPluginFromOutputs(cwd, environment, name, outputs) {
7565
- const missing = REQUIRED_PLUGIN_OUTPUTS.filter((k) => !outputs[k]?.trim());
7566
- if (missing.length > 0) {
7567
- throw new Error(
7568
- `Cannot register the user-facing plugin "${name}" for ${environment}: its Terraform apply did not expose ${missing.join(", ")}. A registration written without these would route ${name}/api/* and ${name}/* at nothing (the frontend 403s, the api 502s). Confirm the plugin module applied for ${environment} before wiring.`
7569
- );
7570
- }
7571
- const registeredPaths = registerUserFacingPlugin(cwd, environment, {
7572
- name,
7573
- functionUrlDomain: outputs["function_url_domain"],
7574
- bucketRegionalDomain: outputs["frontend_bucket_regional_domain"]
7575
- });
7576
- return { registeredPaths, frontendBucketName: outputs["frontend_bucket_name"] };
7577
- }
7578
-
7579
- // src/commands/plugin-wire.ts
7580
- var VALID_ENVIRONMENTS2 = ["dev", "staging", "prod"];
7581
- async function runPluginWire(pluginName, environment, config, deps, cwd = process.cwd()) {
7582
- const awsConfig2 = config.cloud.config;
7583
- const stateBucket = awsConfig2.tf_state_bucket ?? `${config.project.name}-terraform-state-${awsConfig2.account_id}`;
7584
- const stateKey = `${environment}/terraform.tfstate`;
7585
- log.info(`Reading Terraform outputs from s3://${stateBucket}/${stateKey}...`);
7586
- const outputs = await deps.readOutputs(stateBucket, stateKey);
7587
- const pluginOutputs = pluginOutputsFromRoot(pluginName, outputs);
7588
- const targets = wireUserFacingPluginFromOutputs(cwd, environment, pluginName, pluginOutputs);
7589
- console.log(chalk20.bold(`
7590
- Registered "${pluginName}" CDN origins for ${environment}
7591
- `));
7592
- for (const path of targets.registeredPaths) console.log(` ${chalk20.green("~")} ${path}`);
7593
- console.log(
7594
- "\n Commit these and redeploy so the CDN gains its behaviours (apply is pipeline-only):"
7595
- );
7596
- console.log(chalk20.dim(` git add ${targets.registeredPaths.join(" ")}`));
7597
- console.log(chalk20.dim(` git commit -m "infra(cdn): route ${pluginName} (${environment})"`));
7598
- console.log(chalk20.dim(` git push && biffo deploy ${environment}
7599
- `));
7600
- console.log(
7601
- ` Then build and sync the frontend to its bucket: ${chalk20.bold(targets.frontendBucketName)}`
7602
- );
7603
- console.log(
7604
- chalk20.dim(
7605
- " (the plugin's own deploy workflow does this \u2014 it reads the bucket from the same outputs.)\n"
7606
- )
7607
- );
7608
- return targets;
7609
- }
7610
- var pluginWireCommand = new Command20("wire").description(
7611
- "Register a user-facing plugin's CDN origins from its deployed outputs (ADR-0018 register step): biffo plugin wire <name> <environment>"
7612
- ).argument("<name>", "Plugin name, e.g. ideation").argument("<environment>", "Target environment: dev | staging | prod").option("-p, --project <name>", "Project name (overrides biffo.config.json in current directory)").option("-c, --config <path>", "Path to biffo.config.json").action(
7613
- async (name, environment, options) => {
7614
- if (!VALID_ENVIRONMENTS2.includes(environment)) {
7615
- log.error(
7616
- `Unknown environment: ${environment}. Must be one of: ${VALID_ENVIRONMENTS2.join(", ")}`
7617
- );
7618
- process.exit(1);
7619
- }
7620
- const config = await resolveConfig4(options);
7621
- const aws = new AwsAdapter(config);
7622
- try {
7623
- await runPluginWire(name, environment, config, {
7624
- readOutputs: (bucket, key) => aws.readTerraformOutputs(bucket, key)
7625
- });
7626
- } catch (err) {
7627
- log.error(err.message);
7628
- process.exit(1);
7629
- }
7630
- }
7631
- );
7632
- async function resolveConfig4(options) {
7633
- if (options.config) {
7634
- const raw = JSON.parse(readFileSync24(resolve17(options.config), "utf8"));
7635
- const result = BiffoConfigSchema.safeParse(raw);
7636
- if (!result.success) {
7637
- log.error(`Invalid config at ${options.config}:`);
7638
- result.error.issues.forEach((i) => log.error(` ${i.path.join(".")} \u2014 ${i.message}`));
7639
- process.exit(1);
7640
- }
7641
- return result.data;
7642
- }
7643
- if (options.project) {
7644
- const cfg = loadProjectConfig(options.project);
7645
- if (!cfg) {
7646
- log.error(
7647
- `Project "${options.project}" not found in ~/.biffo/projects/. Run biffo init first or pass --config <path>.`
7648
- );
7649
- process.exit(1);
7650
- }
7651
- return cfg;
7652
- }
7653
- const localConfigPath = resolve17(process.cwd(), "biffo.config.json");
7654
- if (existsSync31(localConfigPath)) {
7655
- const raw = JSON.parse(readFileSync24(localConfigPath, "utf8"));
7656
- const result = BiffoConfigSchema.safeParse(raw);
7657
- if (result.success) return result.data;
7658
- if (isTemplatePlaceholderConfig(raw)) {
7659
- log.warn(
7660
- `Ignoring ${localConfigPath} \u2014 it is the unsubstituted Biffo template placeholder, not this project's config.`
7661
- );
7662
- } else {
7663
- log.error(`Invalid config at ${localConfigPath}:`);
7664
- result.error.issues.forEach((i) => log.error(` ${i.path.join(".")} \u2014 ${i.message}`));
7665
- log.error("Refusing to fall back to a saved project while a local config file is present.");
7666
- process.exit(1);
7667
- }
7668
- }
7669
- log.error(
7670
- "No config found. Run inside a Biffo project, or pass --project <name> / --config <path>."
7671
- );
7672
- return process.exit(1);
7673
- }
7674
-
7675
7419
  // src/commands/plugin.ts
7676
- var pluginCommand = new Command21("plugin").description("Manage Biffo plugins");
7420
+ var pluginCommand = new Command20("plugin").description("Manage Biffo plugins");
7677
7421
  pluginCommand.addCommand(pluginCreateCommand);
7678
7422
  pluginCommand.addCommand(pluginListCommand);
7679
7423
  pluginCommand.addCommand(pluginInstallCommand);
7680
- pluginCommand.addCommand(pluginWireCommand);
7681
7424
  pluginCommand.addCommand(pluginUninstallCommand);
7682
7425
  pluginCommand.addCommand(pluginUpgradeCommand);
7683
7426
  pluginCommand.addCommand(pluginSyncMigrationsCommand);
7684
7427
  pluginCommand.addCommand(pluginInfoCommand);
7685
7428
 
7686
7429
  // src/commands/sibling.ts
7687
- import { Command as Command23 } from "commander";
7430
+ import { Command as Command22 } from "commander";
7688
7431
 
7689
7432
  // src/commands/sibling-check-identity.ts
7690
- import { existsSync as existsSync32, readFileSync as readFileSync25 } from "fs";
7691
- import { resolve as resolve18 } from "path";
7692
- import chalk21 from "chalk";
7693
- import { Command as Command22 } from "commander";
7433
+ import { existsSync as existsSync30, readFileSync as readFileSync23 } from "fs";
7434
+ import { resolve as resolve17 } from "path";
7435
+ import chalk20 from "chalk";
7436
+ import { Command as Command21 } from "commander";
7694
7437
 
7695
7438
  // src/lib/sibling-identity-check.ts
7696
7439
  function checkSiblingIdentity(envs) {
@@ -7740,19 +7483,19 @@ function checkSiblingIdentity(envs) {
7740
7483
  }
7741
7484
 
7742
7485
  // src/commands/sibling-check-identity.ts
7743
- var VALID_ENVIRONMENTS3 = ["dev", "staging", "prod"];
7486
+ var VALID_ENVIRONMENTS2 = ["dev", "staging", "prod"];
7744
7487
  var SIBLING_CORE_POOL_VAR = "CORE_COGNITO_USER_POOL_ID";
7745
- var siblingCheckIdentityCommand = new Command22("check-identity").description(
7488
+ var siblingCheckIdentityCommand = new Command21("check-identity").description(
7746
7489
  "Detect when a core's Cognito pool has drifted from its published identity document or any sibling's baked-in CORE_COGNITO_USER_POOL_ID (#400). Run from the core repo; exits non-zero on drift so a scheduled/CI run goes red."
7747
7490
  ).option("--env <environment>", "Only check this environment (default: dev, staging, prod)").option("-p, --project <name>", "Project name (overrides biffo.config.json in current directory)").option("-c, --config <path>", "Path to biffo.config.json").action(async (options) => {
7748
- if (options.env && !VALID_ENVIRONMENTS3.includes(options.env)) {
7491
+ if (options.env && !VALID_ENVIRONMENTS2.includes(options.env)) {
7749
7492
  log.error(
7750
- `Unknown environment: ${options.env}. Must be one of: ${VALID_ENVIRONMENTS3.join(", ")}`
7493
+ `Unknown environment: ${options.env}. Must be one of: ${VALID_ENVIRONMENTS2.join(", ")}`
7751
7494
  );
7752
7495
  process.exit(1);
7753
7496
  }
7754
- const environments = options.env ? [options.env] : [...VALID_ENVIRONMENTS3];
7755
- const config = await resolveConfig5(options);
7497
+ const environments = options.env ? [options.env] : [...VALID_ENVIRONMENTS2];
7498
+ const config = await resolveConfig4(options);
7756
7499
  const { org, repo } = config.source_control.config;
7757
7500
  const awsConfig2 = config.cloud.config;
7758
7501
  const stateBucket = awsConfig2.tf_state_bucket ?? `${config.project.name}-terraform-state-${awsConfig2.account_id}`;
@@ -7762,7 +7505,7 @@ var siblingCheckIdentityCommand = new Command22("check-identity").description(
7762
7505
  github: new GitHubAdapter(token),
7763
7506
  fetchIdentityDoc: fetchPublishedIdentity
7764
7507
  };
7765
- console.log(chalk21.bold(`
7508
+ console.log(chalk20.bold(`
7766
7509
  Biffo \u2014 Sibling identity check (${config.project.name})
7767
7510
  `));
7768
7511
  let result;
@@ -7849,16 +7592,16 @@ function printIdentityReport(result) {
7849
7592
  }
7850
7593
  if (result.ok) {
7851
7594
  log.success(
7852
- chalk21.green(
7595
+ chalk20.green(
7853
7596
  "\u2713 identity consistent \u2014 every published document and sibling backend matches the live pool"
7854
7597
  )
7855
7598
  );
7856
7599
  return;
7857
7600
  }
7858
- log.error(chalk21.red(`\u2718 ${String(result.findings.length)} identity drift finding(s):`));
7601
+ log.error(chalk20.red(`\u2718 ${String(result.findings.length)} identity drift finding(s):`));
7859
7602
  for (const f of result.findings) {
7860
7603
  console.error(
7861
- chalk21.red(
7604
+ chalk20.red(
7862
7605
  ` [${f.environment}] ${f.subject}: ${FINDING_LABEL[f.kind]}
7863
7606
  expected (live pool): ${f.expected}
7864
7607
  found: ${f.actual ?? "(unset)"}`
@@ -7879,9 +7622,9 @@ async function fetchPublishedIdentity(portalUrl) {
7879
7622
  return null;
7880
7623
  }
7881
7624
  }
7882
- async function resolveConfig5(options) {
7625
+ async function resolveConfig4(options) {
7883
7626
  if (options.config) {
7884
- const raw = JSON.parse(readFileSync25(resolve18(options.config), "utf8"));
7627
+ const raw = JSON.parse(readFileSync23(resolve17(options.config), "utf8"));
7885
7628
  const result = BiffoConfigSchema.safeParse(raw);
7886
7629
  if (!result.success) {
7887
7630
  log.error(`Invalid config at ${options.config}:`);
@@ -7900,9 +7643,9 @@ async function resolveConfig5(options) {
7900
7643
  }
7901
7644
  return cfg;
7902
7645
  }
7903
- const localConfigPath = resolve18(process.cwd(), "biffo.config.json");
7904
- if (existsSync32(localConfigPath)) {
7905
- const raw = JSON.parse(readFileSync25(localConfigPath, "utf8"));
7646
+ const localConfigPath = resolve17(process.cwd(), "biffo.config.json");
7647
+ if (existsSync30(localConfigPath)) {
7648
+ const raw = JSON.parse(readFileSync23(localConfigPath, "utf8"));
7906
7649
  const result = BiffoConfigSchema.safeParse(raw);
7907
7650
  if (result.success) return result.data;
7908
7651
  if (isTemplatePlaceholderConfig(raw)) {
@@ -7938,14 +7681,14 @@ async function resolveConfig5(options) {
7938
7681
  }
7939
7682
 
7940
7683
  // src/commands/sibling.ts
7941
- var siblingCommand = new Command23("sibling").description(
7684
+ var siblingCommand = new Command22("sibling").description(
7942
7685
  "Create and manage sibling apps that share a Biffo core project (ADR-0007)"
7943
7686
  );
7944
7687
  siblingCommand.addCommand(siblingCreateCommand);
7945
7688
  siblingCommand.addCommand(siblingCheckIdentityCommand);
7946
7689
 
7947
7690
  // src/commands/check.ts
7948
- import { Command as Command24 } from "commander";
7691
+ import { Command as Command23 } from "commander";
7949
7692
 
7950
7693
  // src/scripts/check-core-ownership.ts
7951
7694
  import { execa as execa5 } from "execa";
@@ -7971,8 +7714,8 @@ async function runOwnershipCheck(argv) {
7971
7714
  const { stdout } = await execa5("git", ["diff", "--cached", "--name-status"], { cwd: root });
7972
7715
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
7973
7716
  if (messageFile) {
7974
- const { readFileSync: readFileSync27, existsSync: existsSync34 } = await import("fs");
7975
- if (existsSync34(messageFile)) commitMessage = readFileSync27(messageFile, "utf8");
7717
+ const { readFileSync: readFileSync25, existsSync: existsSync32 } = await import("fs");
7718
+ if (existsSync32(messageFile)) commitMessage = readFileSync25(messageFile, "utf8");
7976
7719
  }
7977
7720
  } else {
7978
7721
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -8076,8 +7819,8 @@ ${BOLD}If the divergence is deliberate${OFF}
8076
7819
  import { execa as execa6 } from "execa";
8077
7820
 
8078
7821
  // src/lib/plugin-terraform-guard.ts
8079
- import { existsSync as existsSync33, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "fs";
8080
- import { dirname as dirname9, join as join32, relative as relative6, sep as sep3 } from "path";
7822
+ import { existsSync as existsSync31, readFileSync as readFileSync24, readdirSync as readdirSync13 } from "fs";
7823
+ import { dirname as dirname9, join as join31, relative as relative6, sep as sep3 } from "path";
8081
7824
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
8082
7825
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
8083
7826
  function findPluginManifests(root) {
@@ -8092,9 +7835,9 @@ function findPluginManifests(root) {
8092
7835
  for (const entry of entries) {
8093
7836
  if (entry.isDirectory()) {
8094
7837
  if (SKIP_DIRS.has(entry.name)) continue;
8095
- walk(join32(dir, entry.name));
7838
+ walk(join31(dir, entry.name));
8096
7839
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
8097
- found.push(relative6(root, join32(dir, entry.name)).split(sep3).join("/"));
7840
+ found.push(relative6(root, join31(dir, entry.name)).split(sep3).join("/"));
8098
7841
  }
8099
7842
  }
8100
7843
  };
@@ -8104,7 +7847,7 @@ function findPluginManifests(root) {
8104
7847
  function readSubscriptions(absManifestPath) {
8105
7848
  let parsed;
8106
7849
  try {
8107
- parsed = JSON.parse(readFileSync26(absManifestPath, "utf8"));
7850
+ parsed = JSON.parse(readFileSync24(absManifestPath, "utf8"));
8108
7851
  } catch {
8109
7852
  return null;
8110
7853
  }
@@ -8119,14 +7862,14 @@ function readSubscriptions(absManifestPath) {
8119
7862
  }
8120
7863
  function checkPluginTerraform(root) {
8121
7864
  const violations = [];
8122
- const coreManifest = existsSync33(join32(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
7865
+ const coreManifest = existsSync31(join31(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
8123
7866
  for (const manifest of findPluginManifests(root)) {
8124
7867
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
8125
- const absManifest = join32(root, manifest);
7868
+ const absManifest = join31(root, manifest);
8126
7869
  const subscriptions = readSubscriptions(absManifest);
8127
7870
  if (subscriptions === null) continue;
8128
7871
  const pluginDir2 = dirname9(absManifest);
8129
- if (existsSync33(join32(pluginDir2, "terraform"))) continue;
7872
+ if (existsSync31(join31(pluginDir2, "terraform"))) continue;
8130
7873
  const relPluginDir = relative6(root, pluginDir2).split(sep3).join("/");
8131
7874
  violations.push({
8132
7875
  manifest,
@@ -8245,7 +7988,7 @@ async function runReleaseSubjectCheck(argv) {
8245
7988
  }
8246
7989
 
8247
7990
  // src/commands/check.ts
8248
- var checkCommand = new Command24("check").description(
7991
+ var checkCommand = new Command23("check").description(
8249
7992
  "Repo guards (ownership, release subject, plugin terraform) \u2014 run in CI and git hooks"
8250
7993
  );
8251
7994
  checkCommand.command("ownership").description("Refuse changes to template-owned paths in an instance (#370)").argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").option("--staged <messageFile>", "Check staged changes instead of a branch diff (commit hook)").allowExcessArguments(true).action(async () => {
@@ -8265,17 +8008,17 @@ function rawArgsAfter(subcommand) {
8265
8008
  // src/commands/teardown.ts
8266
8009
  import { execSync as execSync7 } from "child_process";
8267
8010
  import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STSClient3 } from "@aws-sdk/client-sts";
8268
- import chalk22 from "chalk";
8269
- import { Command as Command25 } from "commander";
8011
+ import chalk21 from "chalk";
8012
+ import { Command as Command24 } from "commander";
8270
8013
  import inquirer8 from "inquirer";
8271
- var teardownCommand = new Command25("teardown").description(
8014
+ var teardownCommand = new Command24("teardown").description(
8272
8015
  "Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
8273
8016
  ).option("--project <name>", "Project name to tear down (reads session if omitted)").option("--skip-destroy", "Skip terraform destroy (only use if infrastructure is already gone)").option(
8274
8017
  "--confirm <name>",
8275
8018
  "Pre-confirm by supplying the project name (the scriptable form of the typed confirmation)"
8276
8019
  ).option("-y, --yes", "Skip the typed project-name confirmation entirely").action(
8277
8020
  async (options) => {
8278
- console.log(chalk22.bold("\n Biffo \u2014 Teardown\n"));
8021
+ console.log(chalk21.bold("\n Biffo \u2014 Teardown\n"));
8279
8022
  const githubToken = resolveGithubToken4();
8280
8023
  const sts = new STSClient3({});
8281
8024
  const { Account: accountId } = await sts.send(new GetCallerIdentityCommand3({}));
@@ -8297,7 +8040,7 @@ var teardownCommand = new Command25("teardown").description(
8297
8040
  adminEmail = session.config.admin?.email ?? "noop@example.com";
8298
8041
  adminUsername = session.config.admin?.username ?? "noop";
8299
8042
  domain = session.config.project.domain ?? "";
8300
- console.log(chalk22.yellow(" Loaded session for: ") + chalk22.bold(projectName));
8043
+ console.log(chalk21.yellow(" Loaded session for: ") + chalk21.bold(projectName));
8301
8044
  if (org && repo) console.log(` Repository: ${org}/${repo}`);
8302
8045
  console.log();
8303
8046
  } else if (savedConfig) {
@@ -8310,7 +8053,7 @@ var teardownCommand = new Command25("teardown").description(
8310
8053
  adminEmail = savedConfig.admin.email;
8311
8054
  adminUsername = savedConfig.admin.username;
8312
8055
  domain = resolveDnsConfig(savedConfig).domain;
8313
- console.log(chalk22.yellow(" Loaded config for: ") + chalk22.bold(projectName));
8056
+ console.log(chalk21.yellow(" Loaded config for: ") + chalk21.bold(projectName));
8314
8057
  console.log(` Repository: ${org}/${repo}`);
8315
8058
  console.log();
8316
8059
  } else {
@@ -8381,35 +8124,35 @@ var teardownCommand = new Command25("teardown").description(
8381
8124
  throw err;
8382
8125
  }
8383
8126
  }
8384
- console.log(chalk22.red.bold(" This will permanently delete:\n"));
8127
+ console.log(chalk21.red.bold(" This will permanently delete:\n"));
8385
8128
  if (deployedEnvs.length > 0 || hasGlobal) {
8386
- console.log(chalk22.red(" Infrastructure (via GitHub Actions terraform destroy):"));
8129
+ console.log(chalk21.red(" Infrastructure (via GitHub Actions terraform destroy):"));
8387
8130
  for (const env of deployedEnvs) {
8388
8131
  console.log(
8389
- ` ${chalk22.red("\u2717")} ${env} \u2014 VPC, RDS, Lambda, Cognito, CloudFront, EventBridge`
8132
+ ` ${chalk21.red("\u2717")} ${env} \u2014 VPC, RDS, Lambda, Cognito, CloudFront, EventBridge`
8390
8133
  );
8391
8134
  }
8392
8135
  if (hasGlobal) {
8393
- console.log(` ${chalk22.red("\u2717")} global \u2014 Route 53 hosted zone, ACM certificate`);
8136
+ console.log(` ${chalk21.red("\u2717")} global \u2014 Route 53 hosted zone, ACM certificate`);
8394
8137
  }
8395
8138
  console.log();
8396
8139
  }
8397
8140
  for (const line of formatSiblingPlan(siblings, options.skipDestroy === true)) {
8398
8141
  console.log(line);
8399
8142
  }
8400
- console.log(chalk22.red(" Biffo resources:"));
8401
- console.log(` ${chalk22.red("\u2717")} GitHub repository ${chalk22.bold(`${org}/${repo}`)}`);
8143
+ console.log(chalk21.red(" Biffo resources:"));
8144
+ console.log(` ${chalk21.red("\u2717")} GitHub repository ${chalk21.bold(`${org}/${repo}`)}`);
8402
8145
  console.log(
8403
- ` ${chalk22.red("\u2717")} IAM role ${chalk22.bold(`biffo-github-actions-${projectName}`)}`
8146
+ ` ${chalk21.red("\u2717")} IAM role ${chalk21.bold(`biffo-github-actions-${projectName}`)}`
8404
8147
  );
8405
8148
  console.log(
8406
- ` ${chalk22.red("\u2717")} S3 bucket ${chalk22.bold(stateBucket)} (all versions)`
8149
+ ` ${chalk21.red("\u2717")} S3 bucket ${chalk21.bold(stateBucket)} (all versions)`
8407
8150
  );
8408
- console.log(` ${chalk22.red("\u2717")} Local session file`);
8151
+ console.log(` ${chalk21.red("\u2717")} Local session file`);
8409
8152
  if (options.skipDestroy) {
8410
8153
  console.log();
8411
8154
  console.log(
8412
- chalk22.yellow(
8155
+ chalk21.yellow(
8413
8156
  " --skip-destroy: NO terraform destroy runs, for this project or any sibling.\n Everything above is deleted, but any infrastructure still standing is left\n standing \u2014 and, with the state buckets gone, orphaned."
8414
8157
  )
8415
8158
  );
@@ -8597,30 +8340,30 @@ async function assertSiblingsAreDestroyable(github, siblings) {
8597
8340
  }
8598
8341
  function formatSiblingPlan(siblings, skipDestroy) {
8599
8342
  if (siblings.length === 0) return [];
8600
- const lines = [chalk22.red(` Sibling apps (${siblings.length}) \u2014 ADR-0007:`)];
8343
+ const lines = [chalk21.red(` Sibling apps (${siblings.length}) \u2014 ADR-0007:`)];
8601
8344
  for (const s of siblings) {
8602
8345
  const envs = s.environments.join(", ");
8603
8346
  if (s.repoState === "gone") {
8604
8347
  lines.push(
8605
- ` ${chalk22.yellow("!")} ${chalk22.bold(`${s.org}/${s.repo}`)} \u2014 repo already deleted; its ${envs} infrastructure CANNOT be destroyed and will be left standing`
8348
+ ` ${chalk21.yellow("!")} ${chalk21.bold(`${s.org}/${s.repo}`)} \u2014 repo already deleted; its ${envs} infrastructure CANNOT be destroyed and will be left standing`
8606
8349
  );
8607
8350
  continue;
8608
8351
  }
8609
8352
  lines.push(
8610
- ` ${chalk22.red("\u2717")} GitHub repository ${chalk22.bold(`${s.org}/${s.repo}`)} ` + (s.pathPrefix === ROOT_SIBLING_NAME ? "(the application, routed at /)" : `(routed at /${s.pathPrefix})`)
8353
+ ` ${chalk21.red("\u2717")} GitHub repository ${chalk21.bold(`${s.org}/${s.repo}`)} ` + (s.pathPrefix === ROOT_SIBLING_NAME ? "(the application, routed at /)" : `(routed at /${s.pathPrefix})`)
8611
8354
  );
8612
8355
  lines.push(
8613
- ` ${chalk22.red("\u2717")} ${skipDestroy ? "infrastructure NOT destroyed (--skip-destroy)" : `${envs} infrastructure \u2014 S3 site bucket, Lambda, API Gateway`}`
8356
+ ` ${chalk21.red("\u2717")} ${skipDestroy ? "infrastructure NOT destroyed (--skip-destroy)" : `${envs} infrastructure \u2014 S3 site bucket, Lambda, API Gateway`}`
8614
8357
  );
8615
8358
  lines.push(
8616
- ` ${chalk22.red("\u2717")} IAM role ${chalk22.bold(`biffo-github-actions-${s.projectName}`)}`
8359
+ ` ${chalk21.red("\u2717")} IAM role ${chalk21.bold(`biffo-github-actions-${s.projectName}`)}`
8617
8360
  );
8618
8361
  lines.push(
8619
- ` ${chalk22.red("\u2717")} S3 bucket ${chalk22.bold(`${s.projectName}-terraform-state-${s.accountId}`)}`
8362
+ ` ${chalk21.red("\u2717")} S3 bucket ${chalk21.bold(`${s.projectName}-terraform-state-${s.accountId}`)}`
8620
8363
  );
8621
8364
  if (s.pendingRegistrationPr !== void 0) {
8622
8365
  lines.push(
8623
- chalk22.dim(` registration PR #${s.pendingRegistrationPr} is still open \u2014 never routed`)
8366
+ chalk21.dim(` registration PR #${s.pendingRegistrationPr} is still open \u2014 never routed`)
8624
8367
  );
8625
8368
  }
8626
8369
  }
@@ -8658,7 +8401,7 @@ async function confirmTeardown(projectName, options) {
8658
8401
  {
8659
8402
  type: "input",
8660
8403
  name: "confirm",
8661
- message: `Type ${chalk22.bold(projectName)} to confirm:`
8404
+ message: `Type ${chalk21.bold(projectName)} to confirm:`
8662
8405
  }
8663
8406
  ]);
8664
8407
  return confirm === projectName;
@@ -8676,7 +8419,7 @@ function resolveGithubToken4() {
8676
8419
  }
8677
8420
 
8678
8421
  // src/index.ts
8679
- var program = new Command26();
8422
+ var program = new Command25();
8680
8423
  function cliVersion() {
8681
8424
  try {
8682
8425
  return getLatestCoreVersion();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.108.3",
3
+ "version": "0.108.4",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",