@biffo/cli 0.308.2 → 0.309.1
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.
|
@@ -330,6 +330,36 @@
|
|
|
330
330
|
"type": "object",
|
|
331
331
|
"additionalProperties": { "type": "string" },
|
|
332
332
|
"description": "Python package dependencies (including biffo-plugin-sdk)."
|
|
333
|
+
},
|
|
334
|
+
"config": {
|
|
335
|
+
"type": "array",
|
|
336
|
+
"items": {
|
|
337
|
+
"type": "object",
|
|
338
|
+
"required": ["name", "kind", "description"],
|
|
339
|
+
"properties": {
|
|
340
|
+
"name": {
|
|
341
|
+
"type": "string",
|
|
342
|
+
"pattern": "^[a-z][a-z0-9_]*$",
|
|
343
|
+
"description": "Config key, snake_case. Referenced by other manifest fields (e.g. chat_agents[].required_group) and used as the per-plugin, per-name lookup key the host resolves at runtime."
|
|
344
|
+
},
|
|
345
|
+
"kind": {
|
|
346
|
+
"enum": ["secret", "setting"],
|
|
347
|
+
"description": "'secret' — the instance supplies an SSM parameter PATH, never the credential itself. 'setting' — the instance supplies a literal value (a Cognito group name, a feature flag, a numeric limit). Never a value itself; this manifest declares only the NEED (biffo-template#1517)."
|
|
348
|
+
},
|
|
349
|
+
"required": {
|
|
350
|
+
"type": "boolean",
|
|
351
|
+
"default": true,
|
|
352
|
+
"description": "If true, `biffo plugin install` fails loudly when the instance supplies no value for this name, rather than installing an unconfigured plugin."
|
|
353
|
+
},
|
|
354
|
+
"description": {
|
|
355
|
+
"type": "string",
|
|
356
|
+
"minLength": 1,
|
|
357
|
+
"description": "What this need is for, shown to whoever installs the plugin so they know what to supply."
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
"additionalProperties": false
|
|
361
|
+
},
|
|
362
|
+
"description": "Named needs (a credential, a group name, a feature flag) this manifest declares that the INSTANCE must supply at install time — never values, only name/kind/required/description (biffo-template#1517). See ConfigDeclaration in biffo_plugin_sdk.plugin for the full mechanism."
|
|
333
363
|
}
|
|
334
364
|
},
|
|
335
365
|
"additionalProperties": false
|
|
@@ -330,6 +330,36 @@
|
|
|
330
330
|
"type": "object",
|
|
331
331
|
"additionalProperties": { "type": "string" },
|
|
332
332
|
"description": "Python package dependencies (including biffo-plugin-sdk)."
|
|
333
|
+
},
|
|
334
|
+
"config": {
|
|
335
|
+
"type": "array",
|
|
336
|
+
"items": {
|
|
337
|
+
"type": "object",
|
|
338
|
+
"required": ["name", "kind", "description"],
|
|
339
|
+
"properties": {
|
|
340
|
+
"name": {
|
|
341
|
+
"type": "string",
|
|
342
|
+
"pattern": "^[a-z][a-z0-9_]*$",
|
|
343
|
+
"description": "Config key, snake_case. Referenced by other manifest fields (e.g. chat_agents[].required_group) and used as the per-plugin, per-name lookup key the host resolves at runtime."
|
|
344
|
+
},
|
|
345
|
+
"kind": {
|
|
346
|
+
"enum": ["secret", "setting"],
|
|
347
|
+
"description": "'secret' — the instance supplies an SSM parameter PATH, never the credential itself. 'setting' — the instance supplies a literal value (a Cognito group name, a feature flag, a numeric limit). Never a value itself; this manifest declares only the NEED (biffo-template#1517)."
|
|
348
|
+
},
|
|
349
|
+
"required": {
|
|
350
|
+
"type": "boolean",
|
|
351
|
+
"default": true,
|
|
352
|
+
"description": "If true, `biffo plugin install` fails loudly when the instance supplies no value for this name, rather than installing an unconfigured plugin."
|
|
353
|
+
},
|
|
354
|
+
"description": {
|
|
355
|
+
"type": "string",
|
|
356
|
+
"minLength": 1,
|
|
357
|
+
"description": "What this need is for, shown to whoever installs the plugin so they know what to supply."
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
"additionalProperties": false
|
|
361
|
+
},
|
|
362
|
+
"description": "Named needs (a credential, a group name, a feature flag) this manifest declares that the INSTANCE must supply at install time — never values, only name/kind/required/description (biffo-template#1517). See ConfigDeclaration in biffo_plugin_sdk.plugin for the full mechanism."
|
|
333
363
|
}
|
|
334
364
|
},
|
|
335
365
|
"additionalProperties": false
|
package/dist/index.js
CHANGED
|
@@ -630,6 +630,7 @@ import { Command as Command3 } from "commander";
|
|
|
630
630
|
// src/adapters/git/index.ts
|
|
631
631
|
import { randomUUID } from "crypto";
|
|
632
632
|
import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "fs";
|
|
633
|
+
import { readFile as readFile2, rm } from "fs/promises";
|
|
633
634
|
import { tmpdir as tmpdir2 } from "os";
|
|
634
635
|
import { join as join6 } from "path";
|
|
635
636
|
|
|
@@ -1237,6 +1238,51 @@ var GitAdapter = class {
|
|
|
1237
1238
|
async hasFleetWorktreeClaim(worktreePath) {
|
|
1238
1239
|
return existsSync6(join6(worktreePath, ".fleet-worktree-claim"));
|
|
1239
1240
|
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Milliseconds since `worktreePath`'s `.fleet-worktree-claim` lock was
|
|
1243
|
+
* written (biffo-template#1948), or `null` when there is no lock, or its
|
|
1244
|
+
* `holder` file cannot be read and parsed.
|
|
1245
|
+
*
|
|
1246
|
+
* `bin/fleet.sh worktree-claim` (biffo-fleet) writes the holder token on
|
|
1247
|
+
* line 1 and a UTC ISO-8601 timestamp (`now()`, i.e. `date -u
|
|
1248
|
+
* +%Y-%m-%dT%H:%M:%SZ`) on line 2 — this reads that second line and diffs
|
|
1249
|
+
* it against the current time. `null` is the fail-closed default for
|
|
1250
|
+
* "cannot tell": `doctor-reaper.ts` treats it exactly like a live claim,
|
|
1251
|
+
* never like a stale one, the same posture `hasFleetWorktreeClaim` already
|
|
1252
|
+
* takes and fleet.sh's own comment on an unwritten holder file takes ("An
|
|
1253
|
+
* unwritten holder is still a HELD lock").
|
|
1254
|
+
*/
|
|
1255
|
+
async fleetWorktreeClaimAgeMs(worktreePath) {
|
|
1256
|
+
try {
|
|
1257
|
+
const raw = await readFile2(join6(worktreePath, ".fleet-worktree-claim", "holder"), "utf8");
|
|
1258
|
+
const claimedAt = raw.split("\n")[1]?.trim();
|
|
1259
|
+
if (!claimedAt) return null;
|
|
1260
|
+
const claimedMs = Date.parse(claimedAt);
|
|
1261
|
+
return Number.isNaN(claimedMs) ? null : Date.now() - claimedMs;
|
|
1262
|
+
} catch {
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Removes a `.fleet-worktree-claim` lock directory outright (biffo-template#1948).
|
|
1268
|
+
*
|
|
1269
|
+
* Used only once `doctor --fix` has independently judged the lock stale —
|
|
1270
|
+
* see `FLEET_CLAIM_STALE_AFTER_MS` in `lib/doctor-reaper.ts`. Needed because
|
|
1271
|
+
* the lock directory is untracked: leaving it in place after deciding to
|
|
1272
|
+
* disregard it would trip `hasUncommittedChanges`'s `git status
|
|
1273
|
+
* --porcelain` and keep the worktree anyway, under the less specific
|
|
1274
|
+
* `uncommitted-changes` reason, defeating the point of ignoring it.
|
|
1275
|
+
*
|
|
1276
|
+
* Best-effort — swallows any error, so a permissions problem or a
|
|
1277
|
+
* concurrent release falls back to the existing `isDirty` keep path rather
|
|
1278
|
+
* than crashing the whole `--fix` sweep.
|
|
1279
|
+
*/
|
|
1280
|
+
async clearStaleFleetWorktreeClaim(worktreePath) {
|
|
1281
|
+
try {
|
|
1282
|
+
await rm(join6(worktreePath, ".fleet-worktree-claim"), { recursive: true, force: true });
|
|
1283
|
+
} catch {
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1240
1286
|
/** Best-effort fetch of the tracking remote, so the ahead/behind check below
|
|
1241
1287
|
* compares against reality rather than a stale local ref (#394). Never throws:
|
|
1242
1288
|
* offline or a missing remote must not block an upgrade on its own — the
|
|
@@ -8317,6 +8363,12 @@ var ChatAgentDeclarationSchema = z7.object({
|
|
|
8317
8363
|
max_output_tokens: z7.number().int().positive().default(1024),
|
|
8318
8364
|
timeout_seconds: z7.number().positive().default(20)
|
|
8319
8365
|
}).strict();
|
|
8366
|
+
var ConfigDeclarationSchema = z7.object({
|
|
8367
|
+
name: z7.string().regex(/^[a-z][a-z0-9_]*$/, "must be snake_case, e.g. image_provider_api_key"),
|
|
8368
|
+
kind: z7.enum(["secret", "setting"]),
|
|
8369
|
+
required: z7.boolean().default(true),
|
|
8370
|
+
description: z7.string().min(1, "description is required so an installer knows what this need is for")
|
|
8371
|
+
}).strict();
|
|
8320
8372
|
var PluginManifestSchema = z7.object({
|
|
8321
8373
|
name: z7.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
|
|
8322
8374
|
version: z7.string().regex(/^\d+\.\d+\.\d+$/, "must be a full semver, e.g. 1.2.3"),
|
|
@@ -8350,7 +8402,10 @@ var PluginManifestSchema = z7.object({
|
|
|
8350
8402
|
// The plugin's tenant-scoped baseline-row seed (ADR-0005, biffo-template#1554).
|
|
8351
8403
|
// Optional — a plugin with no baseline data omits this entirely, and
|
|
8352
8404
|
// `biffo plugin install`/`upgrade` vendor nothing for it.
|
|
8353
|
-
seed: SeedDeclarationSchema.optional()
|
|
8405
|
+
seed: SeedDeclarationSchema.optional(),
|
|
8406
|
+
// The needs this plugin declares the instance must supply (biffo-template#1517)
|
|
8407
|
+
// — never values, only name/kind/required/description.
|
|
8408
|
+
config: z7.array(ConfigDeclarationSchema).default([])
|
|
8354
8409
|
}).superRefine((manifest, ctx) => {
|
|
8355
8410
|
const tableNames = new Set(manifest.tables.map((t) => t.name));
|
|
8356
8411
|
for (const route of manifest.api_routes) {
|
|
@@ -8371,6 +8426,27 @@ var PluginManifestSchema = z7.object({
|
|
|
8371
8426
|
}
|
|
8372
8427
|
}
|
|
8373
8428
|
}
|
|
8429
|
+
const configCounts = /* @__PURE__ */ new Map();
|
|
8430
|
+
for (const c of manifest.config) configCounts.set(c.name, (configCounts.get(c.name) ?? 0) + 1);
|
|
8431
|
+
for (const [name, count] of configCounts) {
|
|
8432
|
+
if (count > 1) {
|
|
8433
|
+
ctx.addIssue({
|
|
8434
|
+
code: z7.ZodIssueCode.custom,
|
|
8435
|
+
message: `Duplicate config declaration name '${name}'`
|
|
8436
|
+
});
|
|
8437
|
+
}
|
|
8438
|
+
}
|
|
8439
|
+
const settingNames = new Set(
|
|
8440
|
+
manifest.config.filter((c) => c.kind === "setting").map((c) => c.name)
|
|
8441
|
+
);
|
|
8442
|
+
for (const agent of manifest.chat_agents) {
|
|
8443
|
+
if (!settingNames.has(agent.required_group)) {
|
|
8444
|
+
ctx.addIssue({
|
|
8445
|
+
code: z7.ZodIssueCode.custom,
|
|
8446
|
+
message: `chat_agents[key=${agent.key}].required_group '${agent.required_group}' must name a 'config' declaration with kind: "setting" on this same manifest \u2014 a literal Cognito group name is no longer accepted here (biffo-template#1517). Declared setting config names: ${[...settingNames].sort().join(", ") || "none"}`
|
|
8447
|
+
});
|
|
8448
|
+
}
|
|
8449
|
+
}
|
|
8374
8450
|
});
|
|
8375
8451
|
function validateManifest(raw) {
|
|
8376
8452
|
const result = PluginManifestSchema.safeParse(raw);
|
|
@@ -9046,7 +9122,7 @@ function printScopeSeamEntitlement(name) {
|
|
|
9046
9122
|
}
|
|
9047
9123
|
|
|
9048
9124
|
// src/commands/plugin-install.ts
|
|
9049
|
-
import { cpSync as cpSync5, existsSync as existsSync31, mkdirSync as mkdirSync11, readFileSync as readFileSync23, statSync as statSync7 } from "fs";
|
|
9125
|
+
import { cpSync as cpSync5, existsSync as existsSync31, mkdirSync as mkdirSync11, readFileSync as readFileSync23, statSync as statSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
9050
9126
|
import { join as join33, relative as relative4, resolve as resolve12 } from "path";
|
|
9051
9127
|
import chalk15 from "chalk";
|
|
9052
9128
|
import { Command as Command15 } from "commander";
|
|
@@ -9092,6 +9168,58 @@ var PluginMigrationsAdapter = class {
|
|
|
9092
9168
|
}
|
|
9093
9169
|
};
|
|
9094
9170
|
|
|
9171
|
+
// src/lib/plugin-config-resolution.ts
|
|
9172
|
+
function pluginConfigEnvNames(pluginName, configName) {
|
|
9173
|
+
const prefix = `BIFFO_PLUGIN_${pluginName.toUpperCase().replace(/-/g, "_")}_${configName.toUpperCase()}`;
|
|
9174
|
+
return { literalEnv: prefix, parameterEnv: `${prefix}_PARAMETER` };
|
|
9175
|
+
}
|
|
9176
|
+
function resolvePluginConfigSupply(pluginName, declarations, supplied) {
|
|
9177
|
+
const resolved = [];
|
|
9178
|
+
const missingRequired = [];
|
|
9179
|
+
for (const decl of declarations) {
|
|
9180
|
+
const raw = supplied[decl.name];
|
|
9181
|
+
const value = raw?.trim();
|
|
9182
|
+
if (!value) {
|
|
9183
|
+
if (decl.required) {
|
|
9184
|
+
missingRequired.push({ name: decl.name, kind: decl.kind, description: decl.description });
|
|
9185
|
+
}
|
|
9186
|
+
continue;
|
|
9187
|
+
}
|
|
9188
|
+
if (decl.kind === "secret" && !value.startsWith("/")) {
|
|
9189
|
+
throw new Error(
|
|
9190
|
+
`--config ${decl.name}=... must be an SSM parameter PATH starting with '/' for a 'secret' declaration (e.g. /myproject/dev/${pluginName}/${decl.name}) \u2014 never the credential itself. Store the credential in SSM first, then pass its parameter name here.`
|
|
9191
|
+
);
|
|
9192
|
+
}
|
|
9193
|
+
const { literalEnv, parameterEnv } = pluginConfigEnvNames(pluginName, decl.name);
|
|
9194
|
+
resolved.push({
|
|
9195
|
+
name: decl.name,
|
|
9196
|
+
kind: decl.kind,
|
|
9197
|
+
description: decl.description,
|
|
9198
|
+
value,
|
|
9199
|
+
envName: decl.kind === "secret" ? parameterEnv : literalEnv
|
|
9200
|
+
});
|
|
9201
|
+
}
|
|
9202
|
+
return { resolved, missingRequired };
|
|
9203
|
+
}
|
|
9204
|
+
function parseConfigOptionValues(entries) {
|
|
9205
|
+
const record = {};
|
|
9206
|
+
for (const entry of entries) {
|
|
9207
|
+
const eq = entry.indexOf("=");
|
|
9208
|
+
if (eq <= 0) {
|
|
9209
|
+
throw new Error(`--config value '${entry}' must be of the form name=value`);
|
|
9210
|
+
}
|
|
9211
|
+
record[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
9212
|
+
}
|
|
9213
|
+
return record;
|
|
9214
|
+
}
|
|
9215
|
+
function missingRequiredConfigMessage(pluginName, missing) {
|
|
9216
|
+
const lines = missing.map((m) => ` - ${m.name} (${m.kind}): ${m.description}`);
|
|
9217
|
+
return `Plugin '${pluginName}' declares ${missing.length} required config value(s) with no supplied value:
|
|
9218
|
+
${lines.join("\n")}
|
|
9219
|
+
|
|
9220
|
+
Supply each with --config <name>=<value> (repeatable) and re-run install. For a 'secret', pass the SSM parameter PATH holding the credential (create the parameter first, e.g. \`aws ssm put-parameter --type SecureString ...\`), never the credential itself.`;
|
|
9221
|
+
}
|
|
9222
|
+
|
|
9095
9223
|
// src/lib/plugin-provenance.ts
|
|
9096
9224
|
import { existsSync as existsSync28, readFileSync as readFileSync21, writeFileSync as writeFileSync10 } from "fs";
|
|
9097
9225
|
import { join as join29 } from "path";
|
|
@@ -9387,7 +9515,12 @@ var pluginInstallCommand = new Command15("install").description(
|
|
|
9387
9515
|
).argument("[target]", "Plugin name and minor version, e.g. rbac@1.0 (omit when using --local)").option(
|
|
9388
9516
|
"--local <path>",
|
|
9389
9517
|
"Install from a local, unpublished plugin directory instead of the registry"
|
|
9390
|
-
).option("--dry-run", "Resolve the plugin and print planned changes without modifying the repo").option("--cwd <path>", "Project root to install into (defaults to the current directory)").
|
|
9518
|
+
).option("--dry-run", "Resolve the plugin and print planned changes without modifying the repo").option("--cwd <path>", "Project root to install into (defaults to the current directory)").option(
|
|
9519
|
+
"--config <name=value>",
|
|
9520
|
+
"Supply an instance value for one manifest `config:` declaration (biffo-template#1517) \u2014 repeatable. For a 'secret' declaration, value must be an SSM parameter PATH, never the credential itself.",
|
|
9521
|
+
(value, previous) => [...previous, value],
|
|
9522
|
+
[]
|
|
9523
|
+
).action(
|
|
9391
9524
|
async (target, options) => {
|
|
9392
9525
|
const cwd = options.cwd ? resolve12(options.cwd) : process.cwd();
|
|
9393
9526
|
try {
|
|
@@ -9396,7 +9529,8 @@ var pluginInstallCommand = new Command15("install").description(
|
|
|
9396
9529
|
{
|
|
9397
9530
|
...options.local ? { local: resolve12(options.local) } : {},
|
|
9398
9531
|
dryRun: options.dryRun ?? false,
|
|
9399
|
-
cwd
|
|
9532
|
+
cwd,
|
|
9533
|
+
config: parseConfigOptionValues(options.config)
|
|
9400
9534
|
},
|
|
9401
9535
|
{
|
|
9402
9536
|
registry: new RegistryAdapter(),
|
|
@@ -9501,7 +9635,7 @@ async function runPluginInstall(target, options, deps) {
|
|
|
9501
9635
|
);
|
|
9502
9636
|
}
|
|
9503
9637
|
if (options.dryRun) {
|
|
9504
|
-
printDryRun4(entry, source, relTargetDir, inTreeSource);
|
|
9638
|
+
printDryRun4(entry, source, relTargetDir, inTreeSource, options.config ?? {});
|
|
9505
9639
|
return;
|
|
9506
9640
|
}
|
|
9507
9641
|
const isRepo = await deps.git.isGitRepo(options.cwd);
|
|
@@ -9531,6 +9665,14 @@ async function runPluginInstall(target, options, deps) {
|
|
|
9531
9665
|
if (retiredShapeReasons.length > 0) {
|
|
9532
9666
|
throw new Error(retiredFrontendShapeError(pluginName, retiredShapeReasons));
|
|
9533
9667
|
}
|
|
9668
|
+
const configSupply = resolvePluginConfigSupply(
|
|
9669
|
+
pluginName,
|
|
9670
|
+
manifest.config,
|
|
9671
|
+
options.config ?? {}
|
|
9672
|
+
);
|
|
9673
|
+
if (configSupply.missingRequired.length > 0) {
|
|
9674
|
+
throw new Error(missingRequiredConfigMessage(pluginName, configSupply.missingRequired));
|
|
9675
|
+
}
|
|
9534
9676
|
if (inTreeSource) {
|
|
9535
9677
|
log.info(`${relTargetDir}/ is already in this checkout \u2014 installing in place.`);
|
|
9536
9678
|
} else {
|
|
@@ -9589,6 +9731,31 @@ async function runPluginInstall(target, options, deps) {
|
|
|
9589
9731
|
if (seedResult.vendored) {
|
|
9590
9732
|
stagePaths.push(seedResult.stagedPath);
|
|
9591
9733
|
}
|
|
9734
|
+
if (manifest.config.length > 0) {
|
|
9735
|
+
const configFilePath = join33(targetDir, "biffo.plugin-config.json");
|
|
9736
|
+
writeFileSync12(
|
|
9737
|
+
configFilePath,
|
|
9738
|
+
JSON.stringify(
|
|
9739
|
+
{
|
|
9740
|
+
plugin: pluginName,
|
|
9741
|
+
resolved: configSupply.resolved.map(({ name, kind, envName, value }) => ({
|
|
9742
|
+
name,
|
|
9743
|
+
kind,
|
|
9744
|
+
env: envName,
|
|
9745
|
+
// For a secret this is the SSM parameter PATH, not the credential.
|
|
9746
|
+
value
|
|
9747
|
+
}))
|
|
9748
|
+
},
|
|
9749
|
+
null,
|
|
9750
|
+
2
|
|
9751
|
+
) + "\n",
|
|
9752
|
+
"utf8"
|
|
9753
|
+
);
|
|
9754
|
+
stagePaths.push(relative4(options.cwd, configFilePath));
|
|
9755
|
+
log.success(
|
|
9756
|
+
`Recorded ${configSupply.resolved.length}/${manifest.config.length} declared config value(s) at ${relative4(options.cwd, configFilePath)}`
|
|
9757
|
+
);
|
|
9758
|
+
}
|
|
9592
9759
|
const commitMessage = `feat(plugins): install ${pluginName}@${source.version}`;
|
|
9593
9760
|
await deps.git.add(options.cwd, stagePaths);
|
|
9594
9761
|
await deps.git.commit(options.cwd, commitMessage);
|
|
@@ -9599,10 +9766,28 @@ async function runPluginInstall(target, options, deps) {
|
|
|
9599
9766
|
console.log(chalk15.dim(` git push`));
|
|
9600
9767
|
console.log(chalk15.dim(` biffo deploy <environment> --app-only
|
|
9601
9768
|
`));
|
|
9769
|
+
printConfigWiringInstructions(pluginName, configSupply.resolved);
|
|
9602
9770
|
} finally {
|
|
9603
9771
|
source.cleanup();
|
|
9604
9772
|
}
|
|
9605
9773
|
}
|
|
9774
|
+
function printConfigWiringInstructions(pluginName, resolved) {
|
|
9775
|
+
if (resolved.length === 0) return;
|
|
9776
|
+
console.log(chalk15.bold(` ${pluginName} needs these env vars on the shared plugin host:
|
|
9777
|
+
`));
|
|
9778
|
+
const entries = {};
|
|
9779
|
+
for (const r of resolved) entries[r.envName] = r.value;
|
|
9780
|
+
console.log(chalk15.dim(` ${JSON.stringify(entries, null, 2).split("\n").join("\n ")}`));
|
|
9781
|
+
console.log(
|
|
9782
|
+
`
|
|
9783
|
+
Add them to plugin_host_environment in infra/environments/<env>/plugin_host.auto.tfvars.json.`
|
|
9784
|
+
);
|
|
9785
|
+
if (resolved.some((r) => r.kind === "secret")) {
|
|
9786
|
+
console.log(
|
|
9787
|
+
" At least one is a secret reference \u2014 grant the shared plugin host's execution role ssm:GetParameter (and kms:Decrypt if it's a SecureString on a customer-managed key) scoped to that parameter."
|
|
9788
|
+
);
|
|
9789
|
+
}
|
|
9790
|
+
}
|
|
9606
9791
|
function parseManifestFile(path) {
|
|
9607
9792
|
try {
|
|
9608
9793
|
return JSON.parse(readFileSync23(path, "utf8"));
|
|
@@ -9610,7 +9795,7 @@ function parseManifestFile(path) {
|
|
|
9610
9795
|
throw new Error(`Could not parse ${path} as JSON: ${err.message}`);
|
|
9611
9796
|
}
|
|
9612
9797
|
}
|
|
9613
|
-
function printDryRun4(entry, source, relTargetDir, inTreeSource) {
|
|
9798
|
+
function printDryRun4(entry, source, relTargetDir, inTreeSource, suppliedConfig = {}) {
|
|
9614
9799
|
const name = entry ? entry.name : source.name;
|
|
9615
9800
|
const version = entry ? entry.version : source.version;
|
|
9616
9801
|
console.log(chalk15.bold("\n Dry run \u2014 no changes will be made\n"));
|
|
@@ -9642,6 +9827,18 @@ function printDryRun4(entry, source, relTargetDir, inTreeSource) {
|
|
|
9642
9827
|
` Would vendor seed DDL into: ${pluginSeedImportDir(name)}/ (baseline_tables: ${source.manifest.seed.baseline_tables.join(", ") || "none declared"})`
|
|
9643
9828
|
);
|
|
9644
9829
|
}
|
|
9830
|
+
if (source && source.manifest.config.length > 0) {
|
|
9831
|
+
const { resolved, missingRequired } = resolvePluginConfigSupply(
|
|
9832
|
+
name,
|
|
9833
|
+
source.manifest.config,
|
|
9834
|
+
suppliedConfig
|
|
9835
|
+
);
|
|
9836
|
+
console.log(` Declares ${source.manifest.config.length} config need(s):`);
|
|
9837
|
+
for (const c of source.manifest.config) {
|
|
9838
|
+
const status = missingRequired.some((m) => m.name === c.name) ? "MISSING (would fail install)" : resolved.some((r) => r.name === c.name) ? "supplied" : "not supplied (optional)";
|
|
9839
|
+
console.log(` - ${c.name} (${c.kind}${c.required ? ", required" : ""}): ${status}`);
|
|
9840
|
+
}
|
|
9841
|
+
}
|
|
9645
9842
|
console.log(` Would commit: feat(plugins): install ${name}@${version}
|
|
9646
9843
|
`);
|
|
9647
9844
|
}
|
|
@@ -16081,6 +16278,7 @@ function runDoctorChecks(facts) {
|
|
|
16081
16278
|
}
|
|
16082
16279
|
|
|
16083
16280
|
// src/lib/doctor-reaper.ts
|
|
16281
|
+
var FLEET_CLAIM_STALE_AFTER_MS = 4 * 60 * 60 * 1e3;
|
|
16084
16282
|
function classifyReapCandidate(facts) {
|
|
16085
16283
|
if (facts.hasFleetClaim) return { action: "keep", reason: "fleet-worktree-claimed" };
|
|
16086
16284
|
if (facts.isDetached) return { action: "keep", reason: "detached-head" };
|
|
@@ -16108,12 +16306,23 @@ function findReapCandidates(branches, worktrees) {
|
|
|
16108
16306
|
}
|
|
16109
16307
|
async function reapCandidate(cwd, candidate, deps) {
|
|
16110
16308
|
const { git, github } = deps;
|
|
16111
|
-
const [current,
|
|
16309
|
+
const [current, claimExists] = await Promise.all([
|
|
16112
16310
|
git.currentBranch(candidate.worktreePath),
|
|
16113
|
-
git.hasUncommittedChanges(candidate.worktreePath),
|
|
16114
16311
|
git.hasFleetWorktreeClaim(candidate.worktreePath)
|
|
16115
16312
|
]);
|
|
16116
16313
|
const isDetached = current === "HEAD" || current === "";
|
|
16314
|
+
let hasFleetClaim = false;
|
|
16315
|
+
let staleClaimCleared = false;
|
|
16316
|
+
if (claimExists) {
|
|
16317
|
+
const ageMs = await git.fleetWorktreeClaimAgeMs(candidate.worktreePath);
|
|
16318
|
+
if (ageMs === null || ageMs < FLEET_CLAIM_STALE_AFTER_MS) {
|
|
16319
|
+
hasFleetClaim = true;
|
|
16320
|
+
} else {
|
|
16321
|
+
await git.clearStaleFleetWorktreeClaim(candidate.worktreePath);
|
|
16322
|
+
staleClaimCleared = true;
|
|
16323
|
+
}
|
|
16324
|
+
}
|
|
16325
|
+
const isDirty = await git.hasUncommittedChanges(candidate.worktreePath);
|
|
16117
16326
|
const prVerdict = isDetached || isDirty || hasFleetClaim ? "unknown" : await github.prVerdictForBranch(cwd, candidate.branch);
|
|
16118
16327
|
let mergeContainsHead = true;
|
|
16119
16328
|
if (prVerdict === "merged") {
|
|
@@ -16131,10 +16340,10 @@ async function reapCandidate(cwd, candidate, deps) {
|
|
|
16131
16340
|
mergeContainsHead
|
|
16132
16341
|
});
|
|
16133
16342
|
if (verdict.action === "keep") {
|
|
16134
|
-
return { candidate, verdict, worktreeRemoved: null };
|
|
16343
|
+
return { candidate, verdict, worktreeRemoved: null, staleClaimCleared };
|
|
16135
16344
|
}
|
|
16136
16345
|
const worktreeRemoved = await git.removeWorktree(cwd, candidate.worktreePath);
|
|
16137
|
-
return { candidate, verdict, worktreeRemoved };
|
|
16346
|
+
return { candidate, verdict, worktreeRemoved, staleClaimCleared };
|
|
16138
16347
|
}
|
|
16139
16348
|
async function reapAll(cwd, branches, worktrees, currentBranch, deps) {
|
|
16140
16349
|
const candidates = findReapCandidates(branches, worktrees).filter(
|
|
@@ -16335,28 +16544,36 @@ function printReapOutcomes(outcomes) {
|
|
|
16335
16544
|
const kept = outcomes.filter((o) => o.verdict.action === "keep");
|
|
16336
16545
|
const removed = attempted.filter((o) => o.worktreeRemoved === true);
|
|
16337
16546
|
const failed = attempted.filter((o) => o.worktreeRemoved !== true);
|
|
16547
|
+
const staleClaims = outcomes.filter((o) => o.staleClaimCleared);
|
|
16338
16548
|
console.log("");
|
|
16339
16549
|
for (const o of attempted) {
|
|
16550
|
+
const staleNote = o.staleClaimCleared ? " (after clearing a stale fleet-worktree-claim lock)" : "";
|
|
16340
16551
|
if (o.worktreeRemoved === true) {
|
|
16341
|
-
console.log(
|
|
16552
|
+
console.log(
|
|
16553
|
+
chalk21.green(` removed ${o.candidate.worktreePath} (${o.candidate.branch})${staleNote}`)
|
|
16554
|
+
);
|
|
16342
16555
|
} else {
|
|
16343
16556
|
console.log(
|
|
16344
16557
|
chalk21.red(
|
|
16345
|
-
` FAILED ${o.candidate.worktreePath} (${o.candidate.branch}) \u2014 judged safe to remove but 'git worktree remove' did not succeed; left as-is`
|
|
16558
|
+
` FAILED ${o.candidate.worktreePath} (${o.candidate.branch}) \u2014 judged safe to remove but 'git worktree remove' did not succeed; left as-is${staleNote}`
|
|
16346
16559
|
)
|
|
16347
16560
|
);
|
|
16348
16561
|
}
|
|
16349
16562
|
}
|
|
16350
16563
|
for (const o of kept) {
|
|
16351
16564
|
const reason = o.verdict.reason === void 0 ? "unknown" : KEEP_REASON_TEXT[o.verdict.reason];
|
|
16565
|
+
const staleNote = o.staleClaimCleared ? " (its stale fleet-worktree-claim lock was cleared first)" : "";
|
|
16352
16566
|
console.log(
|
|
16353
|
-
chalk21.dim(
|
|
16567
|
+
chalk21.dim(
|
|
16568
|
+
` kept ${o.candidate.worktreePath} (${o.candidate.branch}) \u2014 ${reason}${staleNote}`
|
|
16569
|
+
)
|
|
16354
16570
|
);
|
|
16355
16571
|
}
|
|
16572
|
+
const staleClaimNote = staleClaims.length > 0 ? ` (${String(staleClaims.length)} had a stale fleet-worktree-claim lock cleared)` : "";
|
|
16356
16573
|
console.log(
|
|
16357
16574
|
chalk21.dim(
|
|
16358
16575
|
`
|
|
16359
|
-
--fix: ${String(removed.length)} removed, ${String(failed.length)} failed, ${String(kept.length)} kept, of ${String(outcomes.length)} worktree(s) considered.
|
|
16576
|
+
--fix: ${String(removed.length)} removed, ${String(failed.length)} failed, ${String(kept.length)} kept, of ${String(outcomes.length)} worktree(s) considered${staleClaimNote}.
|
|
16360
16577
|
`
|
|
16361
16578
|
)
|
|
16362
16579
|
);
|