@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.
- package/dist/index.js +238 -144
- 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
|
|
325
|
-
|
|
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
|
-
|
|
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
|
|
4030
|
-
var AwsConfigSchema =
|
|
4031
|
-
account_id:
|
|
4032
|
-
region:
|
|
4033
|
-
profile:
|
|
4034
|
-
oidc_role_arn:
|
|
4035
|
-
tf_state_bucket:
|
|
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 =
|
|
4038
|
-
org:
|
|
4039
|
-
repo:
|
|
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 =
|
|
4042
|
-
|
|
4135
|
+
var SourceControlConfigSchema = z5.discriminatedUnion("provider", [
|
|
4136
|
+
z5.object({ provider: z5.literal("github"), config: GitHubConfigSchema })
|
|
4043
4137
|
]);
|
|
4044
|
-
var CloudConfigSchema =
|
|
4045
|
-
|
|
4138
|
+
var CloudConfigSchema = z5.discriminatedUnion("provider", [
|
|
4139
|
+
z5.object({ provider: z5.literal("aws"), config: AwsConfigSchema })
|
|
4046
4140
|
]);
|
|
4047
|
-
var ModulesSchema =
|
|
4048
|
-
auth:
|
|
4049
|
-
events:
|
|
4050
|
-
storage:
|
|
4051
|
-
database:
|
|
4052
|
-
compute:
|
|
4053
|
-
cdn:
|
|
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 =
|
|
4056
|
-
mode:
|
|
4057
|
-
domain:
|
|
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 =
|
|
4060
|
-
$schema:
|
|
4061
|
-
project:
|
|
4062
|
-
name:
|
|
4063
|
-
description:
|
|
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:
|
|
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:
|
|
4071
|
-
admin:
|
|
4072
|
-
email:
|
|
4073
|
-
username:
|
|
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:
|
|
4076
|
-
schema_path:
|
|
4077
|
-
migrations_path:
|
|
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:
|
|
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
|
|
5801
|
-
var SiblingConfigSchema =
|
|
5802
|
-
$schema:
|
|
5803
|
-
project:
|
|
5804
|
-
name:
|
|
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:
|
|
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:
|
|
5817
|
-
|
|
5818
|
-
path:
|
|
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:
|
|
5916
|
+
label: z6.string().min(1)
|
|
5823
5917
|
})
|
|
5824
5918
|
).default([])
|
|
5825
5919
|
}),
|
|
5826
5920
|
source_control: SourceControlConfigSchema,
|
|
5827
5921
|
cloud: CloudConfigSchema,
|
|
5828
|
-
environments:
|
|
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:
|
|
5926
|
+
core: z6.object({
|
|
5833
5927
|
// Exactly one of these two must be set — see the superRefine below.
|
|
5834
|
-
project_name:
|
|
5835
|
-
config_path:
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
7062
|
-
name:
|
|
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:
|
|
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:
|
|
7073
|
-
nullable:
|
|
7074
|
-
index:
|
|
7075
|
-
default:
|
|
7076
|
-
description:
|
|
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 =
|
|
7079
|
-
name:
|
|
7080
|
-
columns:
|
|
7081
|
-
unique:
|
|
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 =
|
|
7084
|
-
allowed:
|
|
7085
|
-
required_role:
|
|
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 =
|
|
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 =
|
|
7095
|
-
name:
|
|
7096
|
-
columns:
|
|
7097
|
-
indexes:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
7141
|
-
method:
|
|
7142
|
-
path:
|
|
7143
|
-
table:
|
|
7144
|
-
operation:
|
|
7145
|
-
description:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
7173
|
-
required_group:
|
|
7174
|
-
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 =
|
|
7177
|
-
dir:
|
|
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:
|
|
7275
|
+
required_group: z7.string().min(1, NON_EMPTY_GROUP)
|
|
7182
7276
|
}).strict();
|
|
7183
|
-
var ToolDeclarationSchema =
|
|
7184
|
-
name:
|
|
7185
|
-
description:
|
|
7186
|
-
parameters:
|
|
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 =
|
|
7189
|
-
key:
|
|
7190
|
-
agent_name:
|
|
7191
|
-
system_prompt:
|
|
7192
|
-
model:
|
|
7193
|
-
required_group:
|
|
7194
|
-
max_history_messages:
|
|
7195
|
-
max_output_tokens:
|
|
7196
|
-
timeout_seconds:
|
|
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 =
|
|
7199
|
-
name:
|
|
7200
|
-
version:
|
|
7201
|
-
description:
|
|
7202
|
-
author:
|
|
7203
|
-
tags:
|
|
7204
|
-
tables:
|
|
7205
|
-
api_routes:
|
|
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:
|
|
7212
|
-
required_core_version:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
7728
|
-
var RegistryPluginEntrySchema =
|
|
7729
|
-
name:
|
|
7730
|
-
version:
|
|
7731
|
-
minor_version:
|
|
7732
|
-
repo:
|
|
7733
|
-
description:
|
|
7734
|
-
author:
|
|
7735
|
-
tags:
|
|
7736
|
-
required_core_version:
|
|
7737
|
-
infra_modules:
|
|
7738
|
-
api_routes:
|
|
7739
|
-
ui_components:
|
|
7740
|
-
status:
|
|
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 =
|
|
7743
|
-
schema_version:
|
|
7744
|
-
last_updated:
|
|
7745
|
-
plugins:
|
|
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 {
|