@biffo/cli 0.209.2 → 0.210.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/index.js +238 -144
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -321,8 +321,20 @@ function readCoreManifest(templateRoot) {
321
321
  return result.data;
322
322
  }
323
323
  function globToRegExp(pattern) {
324
- const source = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*");
325
- return new RegExp(`^${source}$`);
324
+ const isSubtree = pattern.endsWith("/");
325
+ const body = isSubtree ? pattern.slice(0, -1) : pattern;
326
+ const segments = body.split("/");
327
+ let source = "";
328
+ for (let i = 0; i < segments.length; i++) {
329
+ const seg = segments[i];
330
+ if (seg === "**") {
331
+ source += "(?:[^/]+/)*";
332
+ continue;
333
+ }
334
+ source += (seg ?? "").split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*");
335
+ if (i < segments.length - 1) source += "/";
336
+ }
337
+ return isSubtree ? new RegExp(`^${source}(?:/.*)?$`) : new RegExp(`^${source}$`);
326
338
  }
327
339
  var GLOB_CACHE = /* @__PURE__ */ new Map();
328
340
  function globMatches(relPath, pattern) {
@@ -585,6 +597,7 @@ import {
585
597
  import { tmpdir } from "os";
586
598
  import { dirname as dirname3, join as join5 } from "path";
587
599
  import { execa } from "execa";
600
+ import { z as z4 } from "zod";
588
601
 
589
602
  // src/lib/core-ownership-guard.ts
590
603
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
@@ -764,14 +777,18 @@ async function planCoreUpgrade(options) {
764
777
  for (const e of entries) summary[e.status]++;
765
778
  const changes = entries.filter((e) => e.status !== "unchanged" && e.status !== "keep-ours");
766
779
  const conflicts = entries.filter((e) => e.conflicted);
767
- return { entries, changes, conflicts, summary, divergenceSkips };
780
+ const oursTracked = gitTrackedFiles(options.oursDir, options.git);
781
+ const orphaned = entries.filter(
782
+ (e) => e.orphaned === true && (oursTracked === null || oursTracked.has(e.path))
783
+ );
784
+ return { entries, changes, conflicts, summary, divergenceSkips, orphaned };
768
785
  }
769
786
  async function classify(path, base, ours, theirs, opts, mergeFile, isDeclaredDivergent, noteDivergenceSkip) {
770
787
  const inBase = base.has(path);
771
788
  const inOurs = ours.has(path);
772
789
  const inTheirs = theirs.has(path);
773
790
  if (!inBase && !inTheirs) {
774
- return { path, status: "keep-ours", conflicted: false };
791
+ return { path, status: "keep-ours", conflicted: false, orphaned: true };
775
792
  }
776
793
  if (!inBase && inTheirs) {
777
794
  const theirsContent2 = read(opts.theirsDir, path);
@@ -837,6 +854,38 @@ function applyUpgradePlan(instanceDir, plan, theirsDir) {
837
854
  }
838
855
  return { written, deleted };
839
856
  }
857
+ var ORPHAN_BASELINE_FILE = "biffo.orphan-baseline.json";
858
+ var OrphanBaselineSchema = z4.object({
859
+ count: z4.number().int().min(0)
860
+ });
861
+ function readOrphanBaseline(instanceRoot) {
862
+ const path = join5(instanceRoot, ORPHAN_BASELINE_FILE);
863
+ if (!existsSync5(path)) return null;
864
+ let raw;
865
+ try {
866
+ raw = JSON.parse(readFileSync4(path, "utf8"));
867
+ } catch (err) {
868
+ throw new Error(`${ORPHAN_BASELINE_FILE} is not valid JSON: ${err.message}`);
869
+ }
870
+ const parsed = OrphanBaselineSchema.safeParse(raw);
871
+ if (!parsed.success) {
872
+ throw new Error(
873
+ `${ORPHAN_BASELINE_FILE} is invalid: ${parsed.error.issues[0]?.message ?? "unexpected shape"}`
874
+ );
875
+ }
876
+ return parsed.data;
877
+ }
878
+ function writeOrphanBaseline(instanceRoot, count) {
879
+ writeFileSync2(join5(instanceRoot, ORPHAN_BASELINE_FILE), `${JSON.stringify({ count }, null, 2)}
880
+ `);
881
+ }
882
+ function checkOrphanRatchet(count, baseline) {
883
+ return {
884
+ count,
885
+ baseline: baseline?.count ?? null,
886
+ increased: baseline !== null && count > baseline.count
887
+ };
888
+ }
840
889
  function parseGitHubRepo(remoteUrl) {
841
890
  const ssh = /^git@[^:]+:([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(remoteUrl);
842
891
  if (ssh && ssh[1] && ssh[2]) return { owner: ssh[1], repo: ssh[2] };
@@ -3187,6 +3236,7 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
3187
3236
  theirsDir,
3188
3237
  manifest
3189
3238
  });
3239
+ const orphanRatchet = checkOrphanRatchet(plan.orphaned.length, readOrphanBaseline(options.cwd));
3190
3240
  const heading = options.apply ? "Biffo core upgrade" : "Biffo core upgrade (dry run)";
3191
3241
  console.log(chalk4.bold(`
3192
3242
  ${heading}
@@ -3195,6 +3245,12 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
3195
3245
  console.log(` merge base: ${fromVersion}`);
3196
3246
  console.log(` target: ${toVersion}
3197
3247
  `);
3248
+ printOrphanReport(plan.orphaned, orphanRatchet);
3249
+ if (orphanRatchet.increased) {
3250
+ throw new Error(
3251
+ `${String(plan.orphaned.length)} unsanctioned instance file(s) under a template-owned path (baseline ${String(orphanRatchet.baseline)}) \u2014 see the list above (#1026). Move new instance-written files under a sanctioned carve-out (services/api/tests/instance/, modules/**/instance/) or otherwise reduce the count, then re-run.`
3252
+ );
3253
+ }
3198
3254
  const migrations = planMigrationCarry({
3199
3255
  templateDir: theirsDir,
3200
3256
  instanceDir: options.cwd,
@@ -3250,10 +3306,11 @@ async function runCoreUpgradeResolved(options, deps, cleanups) {
3250
3306
  toVersion,
3251
3307
  breaking,
3252
3308
  theirsDir,
3253
- coreVersionCleanup
3309
+ coreVersionCleanup,
3310
+ orphanRatchet
3254
3311
  );
3255
3312
  }
3256
- async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVersion, breaking, theirsDir, coreVersionCleanup) {
3313
+ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVersion, breaking, theirsDir, coreVersionCleanup, orphanRatchet) {
3257
3314
  if (breaking.length > 0 && !options.acknowledgeBreaking) {
3258
3315
  throw new Error(
3259
3316
  `This upgrade crosses ${breaking.length} documented breaking change(s): ${breaking.map((b) => b.version).join(", ")}. They are printed above and in ${UPGRADE_GUIDE_PATH}. Read what each one requires \u2014 some destroy data or need manual work after the deploy \u2014 then re-run with --acknowledge-breaking.`
@@ -3284,6 +3341,7 @@ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVe
3284
3341
  breaking,
3285
3342
  theirsDir,
3286
3343
  coreVersionCleanup,
3344
+ orphanRatchet,
3287
3345
  branch,
3288
3346
  token
3289
3347
  );
@@ -3306,11 +3364,18 @@ async function restoreCallerBranch(git, cwd, callerBranch, upgradeBranch) {
3306
3364
  );
3307
3365
  }
3308
3366
  }
3309
- async function buildCommitAndOpenPr(options, deps, plan, migrations, fromVersion, toVersion, breaking, theirsDir, coreVersionCleanup, branch, token) {
3367
+ async function buildCommitAndOpenPr(options, deps, plan, migrations, fromVersion, toVersion, breaking, theirsDir, coreVersionCleanup, orphanRatchet, branch, token) {
3310
3368
  const { git } = deps;
3311
3369
  const applied = applyUpgradePlan(options.cwd, plan, theirsDir);
3312
3370
  const carried = applyMigrationCarry(options.cwd, migrations);
3313
3371
  writeInstanceCoreVersion(options.cwd, toVersion);
3372
+ const establishedOrphanBaseline = orphanRatchet.baseline === null;
3373
+ if (establishedOrphanBaseline) {
3374
+ writeOrphanBaseline(options.cwd, orphanRatchet.count);
3375
+ log.info(
3376
+ `Recorded ${ORPHAN_BASELINE_FILE} with a baseline of ${String(orphanRatchet.count)} unsanctioned instance file(s) under template-owned paths. Future upgrades fail only if this count increases (#1026).`
3377
+ );
3378
+ }
3314
3379
  const cleanedCoreVersion = coreVersionCleanup?.action === "delete";
3315
3380
  if (cleanedCoreVersion && existsSync11(coreVersionCleanup.path)) {
3316
3381
  rmSync5(coreVersionCleanup.path);
@@ -3574,6 +3639,35 @@ function printCoreVersionCleanup(cleanup, applying) {
3574
3639
  const why = cleanup.reason === "repurposed" ? `does not match biffo.core.json \u2014 looks repurposed, keeping ${CORE_VERSION_FILE}` : `biffo.core.json absent or unparseable \u2014 no authority to check, keeping ${CORE_VERSION_FILE}`;
3575
3640
  console.log(` ${chalk4.dim("cleanup".padEnd(15))} ${chalk4.dim(`${cleanup.found}: ${why}`)}`);
3576
3641
  }
3642
+ function printOrphanReport(orphaned, ratchet) {
3643
+ if (orphaned.length === 0 && ratchet.baseline === null) return;
3644
+ console.log(
3645
+ chalk4.bold(
3646
+ ` ${String(orphaned.length)} unsanctioned instance file(s) under a template-owned path (#1026):`
3647
+ )
3648
+ );
3649
+ for (const e of orphaned) console.log(` ${chalk4.yellow(e.path)}`);
3650
+ if (ratchet.baseline === null) {
3651
+ console.log(
3652
+ chalk4.dim(
3653
+ ` No ${ORPHAN_BASELINE_FILE} yet \u2014 an --apply run will record ${String(ratchet.count)} as the baseline.`
3654
+ )
3655
+ );
3656
+ } else if (ratchet.increased) {
3657
+ console.log(
3658
+ chalk4.red(
3659
+ ` Baseline is ${String(ratchet.baseline)} \u2014 this run found ${String(ratchet.count)}, an increase.`
3660
+ )
3661
+ );
3662
+ } else {
3663
+ console.log(
3664
+ chalk4.dim(
3665
+ ` Baseline is ${String(ratchet.baseline)} \u2014 no increase (${String(ratchet.count)} now).`
3666
+ )
3667
+ );
3668
+ }
3669
+ console.log();
3670
+ }
3577
3671
  function printPlan(plan) {
3578
3672
  const ordered = [...plan.conflicts, ...plan.changes.filter((c) => !c.conflicted)];
3579
3673
  for (const e of ordered) {
@@ -4026,62 +4120,62 @@ var AwsAdapter = class {
4026
4120
  };
4027
4121
 
4028
4122
  // src/config/schema.ts
4029
- import { z as z4 } from "zod";
4030
- var AwsConfigSchema = z4.object({
4031
- account_id: z4.string().regex(/^\d{12}$/, "AWS account ID must be 12 digits").describe("12-digit AWS account ID"),
4032
- region: z4.string().default("us-east-1"),
4033
- profile: z4.string().optional(),
4034
- oidc_role_arn: z4.string().regex(/^arn:aws:iam::\d{12}:role\/.+/, "Must be a valid IAM role ARN").optional(),
4035
- tf_state_bucket: z4.string().optional()
4123
+ import { z as z5 } from "zod";
4124
+ var AwsConfigSchema = z5.object({
4125
+ account_id: z5.string().regex(/^\d{12}$/, "AWS account ID must be 12 digits").describe("12-digit AWS account ID"),
4126
+ region: z5.string().default("us-east-1"),
4127
+ profile: z5.string().optional(),
4128
+ oidc_role_arn: z5.string().regex(/^arn:aws:iam::\d{12}:role\/.+/, "Must be a valid IAM role ARN").optional(),
4129
+ tf_state_bucket: z5.string().optional()
4036
4130
  });
4037
- var GitHubConfigSchema = z4.object({
4038
- org: z4.string().min(1).describe("GitHub organisation or username"),
4039
- repo: z4.string().min(1).describe("Repository name (will be created)")
4131
+ var GitHubConfigSchema = z5.object({
4132
+ org: z5.string().min(1).describe("GitHub organisation or username"),
4133
+ repo: z5.string().min(1).describe("Repository name (will be created)")
4040
4134
  });
4041
- var SourceControlConfigSchema = z4.discriminatedUnion("provider", [
4042
- z4.object({ provider: z4.literal("github"), config: GitHubConfigSchema })
4135
+ var SourceControlConfigSchema = z5.discriminatedUnion("provider", [
4136
+ z5.object({ provider: z5.literal("github"), config: GitHubConfigSchema })
4043
4137
  ]);
4044
- var CloudConfigSchema = z4.discriminatedUnion("provider", [
4045
- z4.object({ provider: z4.literal("aws"), config: AwsConfigSchema })
4138
+ var CloudConfigSchema = z5.discriminatedUnion("provider", [
4139
+ z5.object({ provider: z5.literal("aws"), config: AwsConfigSchema })
4046
4140
  ]);
4047
- var ModulesSchema = z4.object({
4048
- auth: z4.enum(["cognito"]).default("cognito"),
4049
- events: z4.enum(["eventbridge"]).default("eventbridge"),
4050
- storage: z4.enum(["s3"]).default("s3"),
4051
- database: z4.enum(["postgresql"]).default("postgresql"),
4052
- compute: z4.enum(["lambda"]).default("lambda"),
4053
- cdn: z4.enum(["cloudfront"]).default("cloudfront")
4141
+ var ModulesSchema = z5.object({
4142
+ auth: z5.enum(["cognito"]).default("cognito"),
4143
+ events: z5.enum(["eventbridge"]).default("eventbridge"),
4144
+ storage: z5.enum(["s3"]).default("s3"),
4145
+ database: z5.enum(["postgresql"]).default("postgresql"),
4146
+ compute: z5.enum(["lambda"]).default("lambda"),
4147
+ cdn: z5.enum(["cloudfront"]).default("cloudfront")
4054
4148
  });
4055
- var DnsSchema = z4.object({
4056
- mode: z4.enum(["managed-route53", "external", "none"]).default("managed-route53"),
4057
- domain: z4.string().min(1).optional()
4149
+ var DnsSchema = z5.object({
4150
+ mode: z5.enum(["managed-route53", "external", "none"]).default("managed-route53"),
4151
+ domain: z5.string().min(1).optional()
4058
4152
  });
4059
- var BiffoConfigSchema = z4.object({
4060
- $schema: z4.string().optional(),
4061
- project: z4.object({
4062
- name: z4.string().min(1).regex(/^[a-z0-9-]+$/, "Must be lowercase kebab-case"),
4063
- description: z4.string().default(""),
4153
+ var BiffoConfigSchema = z5.object({
4154
+ $schema: z5.string().optional(),
4155
+ project: z5.object({
4156
+ name: z5.string().min(1).regex(/^[a-z0-9-]+$/, "Must be lowercase kebab-case"),
4157
+ description: z5.string().default(""),
4064
4158
  // Backward compatibility for existing configs. New configs should use dns.domain.
4065
- domain: z4.string().min(1).optional().describe("Primary domain, e.g. myapp.com")
4159
+ domain: z5.string().min(1).optional().describe("Primary domain, e.g. myapp.com")
4066
4160
  }),
4067
4161
  dns: DnsSchema.optional(),
4068
4162
  source_control: SourceControlConfigSchema,
4069
4163
  cloud: CloudConfigSchema,
4070
- environments: z4.array(z4.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
4071
- admin: z4.object({
4072
- email: z4.string().email(),
4073
- username: z4.string().min(1)
4164
+ environments: z5.array(z5.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
4165
+ admin: z5.object({
4166
+ email: z5.string().email(),
4167
+ username: z5.string().min(1)
4074
4168
  }),
4075
- database: z4.object({
4076
- schema_path: z4.string().nullable().default(null),
4077
- migrations_path: z4.string().default("services/api/migrations")
4169
+ database: z5.object({
4170
+ schema_path: z5.string().nullable().default(null),
4171
+ migrations_path: z5.string().default("services/api/migrations")
4078
4172
  }).default({}),
4079
4173
  modules: ModulesSchema.default({})
4080
4174
  }).superRefine((config, ctx) => {
4081
4175
  const dns = resolveDnsConfig(config);
4082
4176
  if (dns.mode !== "none" && !dns.domain) {
4083
4177
  ctx.addIssue({
4084
- code: z4.ZodIssueCode.custom,
4178
+ code: z5.ZodIssueCode.custom,
4085
4179
  path: ["dns", "domain"],
4086
4180
  message: 'DNS domain is required unless dns.mode is "none"'
4087
4181
  });
@@ -5797,15 +5891,15 @@ async function resolveRepoIds(github, config) {
5797
5891
  }
5798
5892
 
5799
5893
  // src/config/sibling-schema.ts
5800
- import { z as z5 } from "zod";
5801
- var SiblingConfigSchema = z5.object({
5802
- $schema: z5.string().optional(),
5803
- project: z5.object({
5804
- name: z5.string().min(1).regex(
5894
+ import { z as z6 } from "zod";
5895
+ var SiblingConfigSchema = z6.object({
5896
+ $schema: z6.string().optional(),
5897
+ project: z6.object({
5898
+ name: z6.string().min(1).regex(
5805
5899
  /^[a-z][a-z0-9-]*$/,
5806
5900
  "Must be lowercase kebab-case, starting with a letter (it becomes a URL path segment)"
5807
5901
  ),
5808
- description: z5.string().default(""),
5902
+ description: z6.string().default(""),
5809
5903
  // Notable routes this sibling exposes, shown as labelled links on the
5810
5904
  // core project's Microservices tab (ADR-0007). Each `path` is relative to
5811
5905
  // the sibling's own path_prefix (so "demo" renders as /<prefix>/demo), and
@@ -5813,26 +5907,26 @@ var SiblingConfigSchema = z5.object({
5813
5907
  // routes just shows its single root link. Declare real routes here as you
5814
5908
  // build the sibling's pages; the values flow to the core's
5815
5909
  // siblings.auto.tfvars.json at registration and into siblings.json at deploy.
5816
- routes: z5.array(
5817
- z5.object({
5818
- path: z5.string().min(1).regex(
5910
+ routes: z6.array(
5911
+ z6.object({
5912
+ path: z6.string().min(1).regex(
5819
5913
  /^[a-z0-9][a-z0-9/-]*$/,
5820
5914
  'Sub-path relative to the sibling prefix, no leading slash (e.g. "demo" or "apply")'
5821
5915
  ),
5822
- label: z5.string().min(1)
5916
+ label: z6.string().min(1)
5823
5917
  })
5824
5918
  ).default([])
5825
5919
  }),
5826
5920
  source_control: SourceControlConfigSchema,
5827
5921
  cloud: CloudConfigSchema,
5828
- environments: z5.array(z5.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
5922
+ environments: z6.array(z6.enum(["dev", "staging", "prod"])).min(1).default(["dev"]),
5829
5923
  // The core project this sibling is paired with (ADR-0007) — never
5830
5924
  // provisions its own Cognito pool or CloudFront distribution, always
5831
5925
  // plugs into the core project's.
5832
- core: z5.object({
5926
+ core: z6.object({
5833
5927
  // Exactly one of these two must be set — see the superRefine below.
5834
- project_name: z5.string().min(1).optional().describe("Name of a project previously scaffolded with `biffo init` on this machine"),
5835
- config_path: z5.string().min(1).optional().describe(
5928
+ project_name: z6.string().min(1).optional().describe("Name of a project previously scaffolded with `biffo init` on this machine"),
5929
+ config_path: z6.string().min(1).optional().describe(
5836
5930
  "Path to the core project's biffo.config.json, for when it wasn't scaffolded here"
5837
5931
  ),
5838
5932
  // Defaults to project.name at parse time by the caller (sibling-create.ts),
@@ -5845,7 +5939,7 @@ var SiblingConfigSchema = z5.object({
5845
5939
  // CDN's default_cache_behavior instead of a pair of ordered behaviours.
5846
5940
  // It still registers under a non-empty reserved name ("app") — see
5847
5941
  // lib/root-sibling.ts for why the two must not be conflated.
5848
- path_prefix: z5.string().regex(
5942
+ path_prefix: z6.string().regex(
5849
5943
  /^$|^[a-z][a-z0-9-]*$/,
5850
5944
  "Must be lowercase kebab-case, or empty for the root sibling"
5851
5945
  ).optional()
@@ -5853,7 +5947,7 @@ var SiblingConfigSchema = z5.object({
5853
5947
  }).superRefine((config, ctx) => {
5854
5948
  if (!config.core.project_name && !config.core.config_path) {
5855
5949
  ctx.addIssue({
5856
- code: z5.ZodIssueCode.custom,
5950
+ code: z6.ZodIssueCode.custom,
5857
5951
  path: ["core"],
5858
5952
  message: "Either core.project_name or core.config_path is required"
5859
5953
  });
@@ -7055,46 +7149,46 @@ function findInstalledPlugins(cwd) {
7055
7149
  }
7056
7150
 
7057
7151
  // src/lib/plugin-manifest.ts
7058
- import { z as z6 } from "zod";
7152
+ import { z as z7 } from "zod";
7059
7153
  var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["id", "tenant_id", "created_at", "updated_at"]);
7060
7154
  var COLUMN_TYPE_PATTERN = /^(String|Integer|Text|Boolean|Float|DateTime)(\(.*\))?$/;
7061
- var ColumnDefinitionSchema = z6.object({
7062
- name: z6.string().refine(
7155
+ var ColumnDefinitionSchema = z7.object({
7156
+ name: z7.string().refine(
7063
7157
  (n) => !RESERVED_COLUMN_NAMES.has(n),
7064
7158
  (n) => ({
7065
7159
  message: `Column '${n}' is reserved and added automatically; it must not be declared in the manifest.`
7066
7160
  })
7067
7161
  ),
7068
- type: z6.string().regex(
7162
+ type: z7.string().regex(
7069
7163
  COLUMN_TYPE_PATTERN,
7070
7164
  "must be one of String, Integer, Text, Boolean, Float, DateTime (e.g. 'String(255)')"
7071
7165
  ),
7072
- primary_key: z6.boolean().default(false),
7073
- nullable: z6.boolean().default(false),
7074
- index: z6.boolean().default(false),
7075
- default: z6.string().optional(),
7076
- description: z6.string().default("")
7166
+ primary_key: z7.boolean().default(false),
7167
+ nullable: z7.boolean().default(false),
7168
+ index: z7.boolean().default(false),
7169
+ default: z7.string().optional(),
7170
+ description: z7.string().default("")
7077
7171
  });
7078
- var IndexDefinitionSchema = z6.object({
7079
- name: z6.string(),
7080
- columns: z6.array(z6.string()).min(1),
7081
- unique: z6.boolean().default(false)
7172
+ var IndexDefinitionSchema = z7.object({
7173
+ name: z7.string(),
7174
+ columns: z7.array(z7.string()).min(1),
7175
+ unique: z7.boolean().default(false)
7082
7176
  });
7083
- var PermissionRuleSchema = z6.object({
7084
- allowed: z6.boolean().default(false),
7085
- required_role: z6.array(z6.string()).default([])
7177
+ var PermissionRuleSchema = z7.object({
7178
+ allowed: z7.boolean().default(false),
7179
+ required_role: z7.array(z7.string()).default([])
7086
7180
  }).strict();
7087
- var TablePermissionsSchema = z6.object({
7181
+ var TablePermissionsSchema = z7.object({
7088
7182
  list: PermissionRuleSchema.default({}),
7089
7183
  read: PermissionRuleSchema.default({}),
7090
7184
  create: PermissionRuleSchema.default({}),
7091
7185
  update: PermissionRuleSchema.default({}),
7092
7186
  delete: PermissionRuleSchema.default({})
7093
7187
  }).strict();
7094
- var TableDefinitionSchema = z6.object({
7095
- name: z6.string().regex(/^[a-z][a-z0-9_]*$/, "table name must be snake_case, e.g. rbac_roles"),
7096
- columns: z6.array(ColumnDefinitionSchema).default([]),
7097
- indexes: z6.array(IndexDefinitionSchema).default([]),
7188
+ var TableDefinitionSchema = z7.object({
7189
+ name: z7.string().regex(/^[a-z][a-z0-9_]*$/, "table name must be snake_case, e.g. rbac_roles"),
7190
+ columns: z7.array(ColumnDefinitionSchema).default([]),
7191
+ indexes: z7.array(IndexDefinitionSchema).default([]),
7098
7192
  permissions: TablePermissionsSchema.default({})
7099
7193
  }).superRefine((table, ctx) => {
7100
7194
  const colCounts = /* @__PURE__ */ new Map();
@@ -7102,7 +7196,7 @@ var TableDefinitionSchema = z6.object({
7102
7196
  for (const [name, count] of colCounts) {
7103
7197
  if (count > 1) {
7104
7198
  ctx.addIssue({
7105
- code: z6.ZodIssueCode.custom,
7199
+ code: z7.ZodIssueCode.custom,
7106
7200
  message: `Duplicate column name '${name}' in table '${table.name}'`
7107
7201
  });
7108
7202
  }
@@ -7112,7 +7206,7 @@ var TableDefinitionSchema = z6.object({
7112
7206
  for (const [name, count] of idxCounts) {
7113
7207
  if (count > 1) {
7114
7208
  ctx.addIssue({
7115
- code: z6.ZodIssueCode.custom,
7209
+ code: z7.ZodIssueCode.custom,
7116
7210
  message: `Duplicate index name '${name}' in table '${table.name}'`
7117
7211
  });
7118
7212
  }
@@ -7122,7 +7216,7 @@ var TableDefinitionSchema = z6.object({
7122
7216
  for (const col of idx.columns) {
7123
7217
  if (!validColumns.has(col)) {
7124
7218
  ctx.addIssue({
7125
- code: z6.ZodIssueCode.custom,
7219
+ code: z7.ZodIssueCode.custom,
7126
7220
  message: `Index '${idx.name}' on table '${table.name}' references unknown column '${col}'`
7127
7221
  });
7128
7222
  }
@@ -7137,17 +7231,17 @@ var OPERATION_METHODS = {
7137
7231
  delete: /* @__PURE__ */ new Set(["DELETE"])
7138
7232
  };
7139
7233
  var SINGLE_ROW_OPERATIONS = /* @__PURE__ */ new Set(["read", "update", "delete"]);
7140
- var RouteDefSchema = z6.object({
7141
- method: z6.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
7142
- path: z6.string().startsWith("/", "path must start with '/'"),
7143
- table: z6.string(),
7144
- operation: z6.enum(["list", "read", "create", "update", "delete"]),
7145
- description: z6.string().default("")
7234
+ var RouteDefSchema = z7.object({
7235
+ method: z7.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
7236
+ path: z7.string().startsWith("/", "path must start with '/'"),
7237
+ table: z7.string(),
7238
+ operation: z7.enum(["list", "read", "create", "update", "delete"]),
7239
+ description: z7.string().default("")
7146
7240
  }).superRefine((route, ctx) => {
7147
7241
  const allowed = OPERATION_METHODS[route.operation];
7148
7242
  if (allowed && !allowed.has(route.method)) {
7149
7243
  ctx.addIssue({
7150
- code: z6.ZodIssueCode.custom,
7244
+ code: z7.ZodIssueCode.custom,
7151
7245
  message: `operation '${route.operation}' requires method in [${[...allowed].sort().join(", ")}], got '${route.method}'`
7152
7246
  });
7153
7247
  }
@@ -7155,13 +7249,13 @@ var RouteDefSchema = z6.object({
7155
7249
  const needsId = SINGLE_ROW_OPERATIONS.has(route.operation);
7156
7250
  if (needsId && !hasId) {
7157
7251
  ctx.addIssue({
7158
- code: z6.ZodIssueCode.custom,
7252
+ code: z7.ZodIssueCode.custom,
7159
7253
  message: `operation '${route.operation}' addresses a single row and requires an '{id}' path parameter: ${route.path}`
7160
7254
  });
7161
7255
  }
7162
7256
  if (!needsId && hasId) {
7163
7257
  ctx.addIssue({
7164
- code: z6.ZodIssueCode.custom,
7258
+ code: z7.ZodIssueCode.custom,
7165
7259
  message: `operation '${route.operation}' is collection-level and must not have an '{id}' path parameter: ${route.path}`
7166
7260
  });
7167
7261
  }
@@ -7169,47 +7263,47 @@ var RouteDefSchema = z6.object({
7169
7263
  var REL_DIR = /^[\w][\w./-]*$/;
7170
7264
  var NON_EMPTY_GROUP = "required_group must be a non-empty Cognito group name.";
7171
7265
  var APP_REF = /^[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*:[a-zA-Z_]\w*$/;
7172
- var UserIngressSchema = z6.object({
7173
- required_group: z6.string().min(1, NON_EMPTY_GROUP),
7174
- app: z6.string().regex(APP_REF, "must be an ASGI app reference '<module>:<attr>', e.g. 'ideation.app:app'")
7266
+ var UserIngressSchema = z7.object({
7267
+ required_group: z7.string().min(1, NON_EMPTY_GROUP),
7268
+ app: z7.string().regex(APP_REF, "must be an ASGI app reference '<module>:<attr>', e.g. 'ideation.app:app'")
7175
7269
  }).strict();
7176
- var UserFrontendSchema = z6.object({
7177
- dir: z6.string().regex(
7270
+ var UserFrontendSchema = z7.object({
7271
+ dir: z7.string().regex(
7178
7272
  REL_DIR,
7179
7273
  "must be a repo-relative path with no leading slash or traversal, e.g. web/dist"
7180
7274
  ),
7181
- required_group: z6.string().min(1, NON_EMPTY_GROUP)
7275
+ required_group: z7.string().min(1, NON_EMPTY_GROUP)
7182
7276
  }).strict();
7183
- var ToolDeclarationSchema = z6.object({
7184
- name: z6.string(),
7185
- description: z6.string(),
7186
- parameters: z6.record(z6.string(), z6.unknown()).default({})
7277
+ var ToolDeclarationSchema = z7.object({
7278
+ name: z7.string(),
7279
+ description: z7.string(),
7280
+ parameters: z7.record(z7.string(), z7.unknown()).default({})
7187
7281
  });
7188
- var ChatAgentDeclarationSchema = z6.object({
7189
- key: z6.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
7190
- agent_name: z6.string().optional(),
7191
- system_prompt: z6.string().min(1),
7192
- model: z6.string().min(1),
7193
- required_group: z6.string().min(1),
7194
- max_history_messages: z6.number().int().positive().default(40),
7195
- max_output_tokens: z6.number().int().positive().default(1024),
7196
- timeout_seconds: z6.number().positive().default(20)
7282
+ var ChatAgentDeclarationSchema = z7.object({
7283
+ key: z7.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
7284
+ agent_name: z7.string().optional(),
7285
+ system_prompt: z7.string().min(1),
7286
+ model: z7.string().min(1),
7287
+ required_group: z7.string().min(1),
7288
+ max_history_messages: z7.number().int().positive().default(40),
7289
+ max_output_tokens: z7.number().int().positive().default(1024),
7290
+ timeout_seconds: z7.number().positive().default(20)
7197
7291
  }).strict();
7198
- var PluginManifestSchema = z6.object({
7199
- name: z6.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
7200
- version: z6.string().regex(/^\d+\.\d+\.\d+$/, "must be a full semver, e.g. 1.2.3"),
7201
- description: z6.string().default(""),
7202
- author: z6.string().default("Biffo Team"),
7203
- tags: z6.array(z6.string()).default([]),
7204
- tables: z6.array(TableDefinitionSchema).default([]),
7205
- api_routes: z6.array(RouteDefSchema).default([]),
7292
+ var PluginManifestSchema = z7.object({
7293
+ name: z7.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
7294
+ version: z7.string().regex(/^\d+\.\d+\.\d+$/, "must be a full semver, e.g. 1.2.3"),
7295
+ description: z7.string().default(""),
7296
+ author: z7.string().default("Biffo Team"),
7297
+ tags: z7.array(z7.string()).default([]),
7298
+ tables: z7.array(TableDefinitionSchema).default([]),
7299
+ api_routes: z7.array(RouteDefSchema).default([]),
7206
7300
  // Events the plugin reacts to. Parsed (rather than dropped as an unknown
7207
7301
  // key) so `biffo plugin install` can warn when a plugin declares
7208
7302
  // subscriptions but ships no terraform/ to route them — see #194 and
7209
7303
  // lib/plugin-terraform-guard.ts. Kept loose deliberately: the authoritative
7210
7304
  // schema is the registry's, and this consumer only needs to count them.
7211
- event_subscriptions: z6.array(z6.object({ source: z6.string(), detail_type: z6.string() }).passthrough()).default([]),
7212
- required_core_version: z6.string().default(">=0.0.0"),
7305
+ event_subscriptions: z7.array(z7.object({ source: z7.string(), detail_type: z7.string() }).passthrough()).default([]),
7306
+ required_core_version: z7.string().default(">=0.0.0"),
7213
7307
  // ADR-0018 user-facing surfaces. Optional: a plugin without them is an
7214
7308
  // ordinary (data/event/CRUD) plugin.
7215
7309
  user_ingress: UserIngressSchema.optional(),
@@ -7220,16 +7314,16 @@ var PluginManifestSchema = z6.object({
7220
7314
  // object and get silently dropped) is the fix for the parity gap: before
7221
7315
  // this, a manifest declaring `tools` validated with no error and lost the
7222
7316
  // field entirely.
7223
- tools: z6.array(ToolDeclarationSchema).default([]),
7317
+ tools: z7.array(ToolDeclarationSchema).default([]),
7224
7318
  // Chat agents the plugin registers with Core (ADR-0017). Default empty — an
7225
7319
  // ordinary plugin declares none.
7226
- chat_agents: z6.array(ChatAgentDeclarationSchema).default([])
7320
+ chat_agents: z7.array(ChatAgentDeclarationSchema).default([])
7227
7321
  }).superRefine((manifest, ctx) => {
7228
7322
  const tableNames = new Set(manifest.tables.map((t) => t.name));
7229
7323
  for (const route of manifest.api_routes) {
7230
7324
  if (!tableNames.has(route.table)) {
7231
7325
  ctx.addIssue({
7232
- code: z6.ZodIssueCode.custom,
7326
+ code: z7.ZodIssueCode.custom,
7233
7327
  message: `Route ${route.method} ${route.path} references table '${route.table}', which is not declared in this manifest's 'tables' (${[...tableNames].sort().join(", ") || "none"})`
7234
7328
  });
7235
7329
  }
@@ -7724,25 +7818,25 @@ import chalk14 from "chalk";
7724
7818
  import { Command as Command14 } from "commander";
7725
7819
 
7726
7820
  // src/adapters/registry/index.ts
7727
- import { z as z7 } from "zod";
7728
- var RegistryPluginEntrySchema = z7.object({
7729
- name: z7.string().regex(/^[a-z][a-z0-9-]*$/),
7730
- version: z7.string().regex(/^\d+\.\d+\.\d+$/),
7731
- minor_version: z7.string().regex(/^\d+\.\d+$/),
7732
- repo: z7.string().url(),
7733
- description: z7.string().optional(),
7734
- author: z7.string().optional(),
7735
- tags: z7.array(z7.string()).optional(),
7736
- required_core_version: z7.string().optional(),
7737
- infra_modules: z7.array(z7.string()).optional(),
7738
- api_routes: z7.array(z7.string()).optional(),
7739
- ui_components: z7.array(z7.string()).optional(),
7740
- status: z7.enum(["active", "disabled"])
7821
+ import { z as z8 } from "zod";
7822
+ var RegistryPluginEntrySchema = z8.object({
7823
+ name: z8.string().regex(/^[a-z][a-z0-9-]*$/),
7824
+ version: z8.string().regex(/^\d+\.\d+\.\d+$/),
7825
+ minor_version: z8.string().regex(/^\d+\.\d+$/),
7826
+ repo: z8.string().url(),
7827
+ description: z8.string().optional(),
7828
+ author: z8.string().optional(),
7829
+ tags: z8.array(z8.string()).optional(),
7830
+ required_core_version: z8.string().optional(),
7831
+ infra_modules: z8.array(z8.string()).optional(),
7832
+ api_routes: z8.array(z8.string()).optional(),
7833
+ ui_components: z8.array(z8.string()).optional(),
7834
+ status: z8.enum(["active", "disabled"])
7741
7835
  });
7742
- var PluginRegistrySchema = z7.object({
7743
- schema_version: z7.string(),
7744
- last_updated: z7.string(),
7745
- plugins: z7.array(RegistryPluginEntrySchema)
7836
+ var PluginRegistrySchema = z8.object({
7837
+ schema_version: z8.string(),
7838
+ last_updated: z8.string(),
7839
+ plugins: z8.array(RegistryPluginEntrySchema)
7746
7840
  });
7747
7841
  var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/keiranholloway/biffo-plugins-registry/main/plugins.json";
7748
7842
  var RegistryAdapter = class {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.209.2",
3
+ "version": "0.210.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",