@biffo/cli 0.306.0 → 0.308.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.
@@ -127,6 +127,46 @@ runtime config rather than tenant-scoped table rows:
127
127
  > [ADR-0011](https://github.com/keiranholloway/biffo-template/blob/main/docs/ADR/0011-authorization-is-a-core-concern.md)
128
128
  > (authorization is a Core concern, not a plugin) and no longer exists.
129
129
 
130
+ ## User-facing frontend: the `user_frontend` contract (ADR-0021 §2)
131
+
132
+ If your plugin ships a founder-facing UI, it declares a `user_frontend` block
133
+ in `biffo.plugin.json`:
134
+
135
+ ```json
136
+ "user_frontend": {
137
+ "dir": "web/dist",
138
+ "required_group": "founder"
139
+ }
140
+ ```
141
+
142
+ and ships **`web/` and nothing else** — a built static bundle (`index.html`
143
+ plus hashed `assets/*`, the ordinary Vite build-output shape) at the
144
+ repo-relative path `dir` names. That is the entire contract. In particular a
145
+ `user_frontend` plugin ships **no** `terraform/` frontend resources and
146
+ **no** `deploy-frontend.yml` workflow of its own.
147
+
148
+ **Who serves it, and why there is nothing to provision.** The shared plugin
149
+ host serves the bundle directly — `GET /api/v1/plugins/<name>/ui` and
150
+ `GET /api/v1/plugins/<name>/ui/{proxy+}` on the API Gateway routes every
151
+ installed plugin already shares — the same way it already serves
152
+ `admin_ingress`'s admin shell. There is no per-plugin S3 origin, no per-plugin
153
+ CloudFront behaviour, and nothing for `biffo plugin install` to provision:
154
+ committing `web/dist/` is the whole deploy. See
155
+ [ADR-0021](../../docs/ADR/0021-shared-plugin-hosting.md) section 2 for the
156
+ full design and why this superseded the older, per-plugin hosting model.
157
+
158
+ **This retires ADR-0018 section 2's per-plugin frontend shape** — a dedicated
159
+ `aws_s3_bucket` behind the shared CloudFront distribution, wired through a
160
+ `cdn_distribution_arn` module variable and a `frontend_bucket_*` output. That
161
+ shape is fail-closed at install time: `biffo plugin install` and
162
+ `biffo plugin upgrade` **refuse** a plugin whose `terraform/` declares it —
163
+ an `aws_s3_bucket` whose name interpolates `-plugin-…-web`, an output named
164
+ `frontend_bucket_*`, or a `cdn_distribution_arn` variable — with an error
165
+ naming ADR-0021 section 2 and this contract. If you are updating an older
166
+ plugin that still ships one of these, delete `terraform/`'s frontend
167
+ resources (and `deploy-frontend.yml`, if present) and adopt `user_frontend`
168
+ above instead; there is nothing left to provision once you do.
169
+
130
170
  ## Standalone repo, not a monorepo package
131
171
 
132
172
  Unlike biffo-template's own in-monorepo plugins (e.g. `services/_plugins/orchestrator/`,
package/dist/index.js CHANGED
@@ -3504,6 +3504,45 @@ function declaredOutputs(moduleDir) {
3504
3504
  }
3505
3505
  return names;
3506
3506
  }
3507
+ var RETIRED_FRONTEND_BUCKET_PATTERN = /bucket\s*=\s*"[^"]*-plugin-[^"]*-web"/;
3508
+ function declaresRetiredFrontendBucket(moduleDir) {
3509
+ let entries;
3510
+ try {
3511
+ entries = readdirSync3(moduleDir, { withFileTypes: true });
3512
+ } catch {
3513
+ return false;
3514
+ }
3515
+ for (const entry of entries) {
3516
+ if (!entry.isFile() || !entry.name.endsWith(".tf")) continue;
3517
+ let contents;
3518
+ try {
3519
+ contents = readFileSync10(join13(moduleDir, entry.name), "utf8");
3520
+ } catch {
3521
+ continue;
3522
+ }
3523
+ if (RETIRED_FRONTEND_BUCKET_PATTERN.test(contents)) return true;
3524
+ }
3525
+ return false;
3526
+ }
3527
+ function findRetiredFrontendShape(terraformDir) {
3528
+ const reasons = [];
3529
+ if (!existsSync12(terraformDir)) return reasons;
3530
+ if (declaresRetiredFrontendBucket(terraformDir)) {
3531
+ reasons.push('an aws_s3_bucket whose name interpolates "-plugin-...-web"');
3532
+ }
3533
+ for (const name of [...declaredOutputs(terraformDir)].sort()) {
3534
+ if (name.startsWith("frontend_bucket")) {
3535
+ reasons.push(`an output named "${name}"`);
3536
+ }
3537
+ }
3538
+ if (declaredVariables(terraformDir).has("cdn_distribution_arn")) {
3539
+ reasons.push('a "cdn_distribution_arn" variable');
3540
+ }
3541
+ return reasons;
3542
+ }
3543
+ function retiredFrontendShapeError(pluginName, reasons) {
3544
+ return `Plugin '${pluginName}' ships a terraform/ frontend on the retired ADR-0018 \xA72 per-plugin hosting shape: it declares ${reasons.join(", ")}. ADR-0021 \xA72 replaced this \u2014 a user-facing plugin's frontend is served by the shared plugin host directly from the manifest's \`user_frontend\` field (\`{ dir, required_group }\`); it ships \`web/\` and nothing else, with no terraform/ frontend and no deploy-frontend.yml. Update the plugin to the \`user_frontend\` contract (see _skeletons/plugin-template/README.md) before installing it here.`;
3545
+ }
3507
3546
  function renderArguments(args, indent) {
3508
3547
  const width = Math.max(...args.map(([key]) => key.length));
3509
3548
  return args.map(([key, value]) => `${indent}${key.padEnd(width)} = ${value}`).join("\n");
@@ -9488,6 +9527,10 @@ async function runPluginInstall(target, options, deps) {
9488
9527
  log.success(
9489
9528
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
9490
9529
  );
9530
+ const retiredShapeReasons = findRetiredFrontendShape(join33(source.sourceDir, "terraform"));
9531
+ if (retiredShapeReasons.length > 0) {
9532
+ throw new Error(retiredFrontendShapeError(pluginName, retiredShapeReasons));
9533
+ }
9491
9534
  if (inTreeSource) {
9492
9535
  log.info(`${relTargetDir}/ is already in this checkout \u2014 installing in place.`);
9493
9536
  } else {
@@ -10256,6 +10299,10 @@ async function runPluginUpgrade(target, options, deps) {
10256
10299
  log.success(
10257
10300
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
10258
10301
  );
10302
+ const retiredShapeReasons = findRetiredFrontendShape(join38(tmpDir, "terraform"));
10303
+ if (retiredShapeReasons.length > 0) {
10304
+ throw new Error(retiredFrontendShapeError(entry.name, retiredShapeReasons));
10305
+ }
10259
10306
  if (!existsSync36(join38(tmpDir, "terraform"))) {
10260
10307
  refuseIfModuleStillReferenced(options.cwd, modulesDir, entry.name);
10261
10308
  }
@@ -10370,6 +10417,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
10370
10417
  log.success(
10371
10418
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
10372
10419
  );
10420
+ const retiredShapeReasons = findRetiredFrontendShape(join38(source.sourceDir, "terraform"));
10421
+ if (retiredShapeReasons.length > 0) {
10422
+ throw new Error(retiredFrontendShapeError(source.name, retiredShapeReasons));
10423
+ }
10373
10424
  if (!existsSync36(join38(source.sourceDir, "terraform"))) {
10374
10425
  refuseIfModuleStillReferenced(options.cwd, modulesDir, source.name);
10375
10426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.306.0",
3
+ "version": "0.308.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",