@biffo/cli 0.166.2 → 0.167.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.
- package/dist/index.js +326 -36
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command26 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/core.ts
|
|
7
7
|
import { Command as Command4 } from "commander";
|
|
@@ -948,6 +948,68 @@ var GitAdapter = class {
|
|
|
948
948
|
async fetchPrune(cwd, remote = "origin") {
|
|
949
949
|
await execa2("git", ["fetch", "--quiet", "--prune", remote], { cwd, reject: false });
|
|
950
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* Is `cwd` the primary checkout, rather than a linked worktree?
|
|
953
|
+
*
|
|
954
|
+
* The distinction decides whether being off the integration branch is a
|
|
955
|
+
* defect or the mandated state: AGENTS.md §1 requires all work to happen in a
|
|
956
|
+
* worktree on its own branch, while §2 requires the primary to stay on `dev`.
|
|
957
|
+
* Reporting the former as a problem is a false positive in the one place
|
|
958
|
+
* everybody works.
|
|
959
|
+
*
|
|
960
|
+
* A linked worktree's git dir points inside `.git/worktrees/<name>`, while the
|
|
961
|
+
* common dir is the shared `.git`. They are equal only in the primary.
|
|
962
|
+
*/
|
|
963
|
+
async isPrimaryWorktree(cwd) {
|
|
964
|
+
const opts = { cwd, reject: false };
|
|
965
|
+
const [dir, common] = await Promise.all([
|
|
966
|
+
execa2("git", ["rev-parse", "--absolute-git-dir"], opts),
|
|
967
|
+
execa2("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], opts)
|
|
968
|
+
]);
|
|
969
|
+
if (dir.exitCode !== 0 || common.exitCode !== 0) return true;
|
|
970
|
+
return dir.stdout.trim() === common.stdout.trim();
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Worktrees other than the primary, with the branch each is on (#797).
|
|
974
|
+
*
|
|
975
|
+
* `--porcelain` rather than the human format: the latter's alignment and
|
|
976
|
+
* annotations vary, and this has to survive paths with spaces.
|
|
977
|
+
*/
|
|
978
|
+
async listWorktrees(cwd) {
|
|
979
|
+
const { stdout, exitCode } = await execa2("git", ["worktree", "list", "--porcelain"], {
|
|
980
|
+
cwd,
|
|
981
|
+
reject: false
|
|
982
|
+
});
|
|
983
|
+
if (exitCode !== 0) return [];
|
|
984
|
+
const out = [];
|
|
985
|
+
let path = "";
|
|
986
|
+
for (const line of stdout.split("\n")) {
|
|
987
|
+
if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
|
|
988
|
+
else if (line.startsWith("branch ")) {
|
|
989
|
+
const branch = line.slice("branch ".length).replace("refs/heads/", "");
|
|
990
|
+
if (out.length > 0 || path !== cwd) out.push({ path, branch });
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return out.filter((w) => w.path !== cwd);
|
|
994
|
+
}
|
|
995
|
+
/** How many commits `branch` is behind `base`; null when it cannot be measured. */
|
|
996
|
+
async countBehind(cwd, branch, base) {
|
|
997
|
+
const { stdout, exitCode } = await execa2("git", ["rev-list", "--count", `${branch}..${base}`], {
|
|
998
|
+
cwd,
|
|
999
|
+
reject: false
|
|
1000
|
+
});
|
|
1001
|
+
if (exitCode !== 0) return null;
|
|
1002
|
+
const n = Number.parseInt(stdout.trim(), 10);
|
|
1003
|
+
return Number.isNaN(n) ? null : n;
|
|
1004
|
+
}
|
|
1005
|
+
/** A file's contents at a ref, or null when it is absent there. */
|
|
1006
|
+
async showFileAtRef(cwd, ref, path) {
|
|
1007
|
+
const { stdout, exitCode } = await execa2("git", ["show", `${ref}:${path}`], {
|
|
1008
|
+
cwd,
|
|
1009
|
+
reject: false
|
|
1010
|
+
});
|
|
1011
|
+
return exitCode === 0 ? stdout : null;
|
|
1012
|
+
}
|
|
951
1013
|
/** Every local branch with its upstream and tracking state (#758). */
|
|
952
1014
|
async listBranchRefs(cwd) {
|
|
953
1015
|
const { stdout, exitCode } = await execa2(
|
|
@@ -1280,7 +1342,7 @@ var GitHubAdapter = class {
|
|
|
1280
1342
|
} catch (err) {
|
|
1281
1343
|
if (err.status !== 404) throw err;
|
|
1282
1344
|
}
|
|
1283
|
-
await new Promise((
|
|
1345
|
+
await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
|
|
1284
1346
|
}
|
|
1285
1347
|
throw new Error(
|
|
1286
1348
|
`Branch "${branch}" not found in ${org}/${repo} after ${timeoutMs / 1e3}s \u2014 GitHub template generation may have stalled. Check the repository and re-run biffo init.`
|
|
@@ -1295,7 +1357,7 @@ var GitHubAdapter = class {
|
|
|
1295
1357
|
} catch (err) {
|
|
1296
1358
|
if (err.status !== 404) throw err;
|
|
1297
1359
|
}
|
|
1298
|
-
await new Promise((
|
|
1360
|
+
await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
|
|
1299
1361
|
}
|
|
1300
1362
|
throw new Error(
|
|
1301
1363
|
`Ref "${ref}" not found in ${org}/${repo} after ${timeoutMs / 1e3}s \u2014 GitHub template generation may have stalled. Check the repository and re-run biffo init.`
|
|
@@ -1500,7 +1562,7 @@ var GitHubAdapter = class {
|
|
|
1500
1562
|
}
|
|
1501
1563
|
if (status !== 404 || Date.now() >= deadline) throw err;
|
|
1502
1564
|
log.info("Branch protection endpoint not yet ready, retrying...");
|
|
1503
|
-
await new Promise((
|
|
1565
|
+
await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
|
|
1504
1566
|
}
|
|
1505
1567
|
}
|
|
1506
1568
|
}
|
|
@@ -1563,7 +1625,7 @@ var GitHubAdapter = class {
|
|
|
1563
1625
|
}
|
|
1564
1626
|
if (status !== 404 || Date.now() >= deadline) throw err;
|
|
1565
1627
|
log.info("Branch protection endpoint not yet ready, retrying...");
|
|
1566
|
-
await new Promise((
|
|
1628
|
+
await new Promise((resolve19) => setTimeout(resolve19, protectionIntervalMs));
|
|
1567
1629
|
}
|
|
1568
1630
|
}
|
|
1569
1631
|
log.success(
|
|
@@ -1818,7 +1880,7 @@ var GitHubAdapter = class {
|
|
|
1818
1880
|
} catch (err) {
|
|
1819
1881
|
if (err.status !== 404 || Date.now() >= deadline) throw err;
|
|
1820
1882
|
log.info(`Workflow ${workflowId} not yet indexed by GitHub Actions, retrying...`);
|
|
1821
|
-
await new Promise((
|
|
1883
|
+
await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
|
|
1822
1884
|
}
|
|
1823
1885
|
}
|
|
1824
1886
|
}
|
|
@@ -1837,7 +1899,7 @@ var GitHubAdapter = class {
|
|
|
1837
1899
|
} catch (err) {
|
|
1838
1900
|
if (err.status !== 404 || Date.now() >= deadline) throw err;
|
|
1839
1901
|
log.info(`Workflow ${workflowId} not yet indexed by GitHub Actions, retrying...`);
|
|
1840
|
-
await new Promise((
|
|
1902
|
+
await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
|
|
1841
1903
|
}
|
|
1842
1904
|
}
|
|
1843
1905
|
}
|
|
@@ -1861,7 +1923,7 @@ var GitHubAdapter = class {
|
|
|
1861
1923
|
} else {
|
|
1862
1924
|
log.info(" Waiting for run to be queued...");
|
|
1863
1925
|
}
|
|
1864
|
-
await new Promise((
|
|
1926
|
+
await new Promise((resolve19) => setTimeout(resolve19, intervalMs));
|
|
1865
1927
|
}
|
|
1866
1928
|
throw new Error(
|
|
1867
1929
|
`Workflow ${workflowId} did not complete within ${timeoutMs / 1e3 / 60} minutes`
|
|
@@ -3332,7 +3394,7 @@ var AwsAdapter = class {
|
|
|
3332
3394
|
const code = err.Code;
|
|
3333
3395
|
if (code === "OperationAborted" && attempt < maxAttempts) {
|
|
3334
3396
|
log.info(` Waiting for S3 to release "${bucketName}"... (${attempt}/${maxAttempts})`);
|
|
3335
|
-
await new Promise((
|
|
3397
|
+
await new Promise((resolve19) => setTimeout(resolve19, retryDelayMs));
|
|
3336
3398
|
} else if (code === "OperationAborted") {
|
|
3337
3399
|
return false;
|
|
3338
3400
|
} else {
|
|
@@ -8679,8 +8741,8 @@ async function runOwnershipCheck(argv) {
|
|
|
8679
8741
|
const { stdout } = await execa6("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
8680
8742
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
8681
8743
|
if (messageFile) {
|
|
8682
|
-
const { readFileSync:
|
|
8683
|
-
if (
|
|
8744
|
+
const { readFileSync: readFileSync26, existsSync: existsSync35 } = await import("fs");
|
|
8745
|
+
if (existsSync35(messageFile)) commitMessage = readFileSync26(messageFile, "utf8");
|
|
8684
8746
|
}
|
|
8685
8747
|
} else {
|
|
8686
8748
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -9072,20 +9134,247 @@ function rawArgsAfter(subcommand) {
|
|
|
9072
9134
|
return at === -1 ? [] : process.argv.slice(at + 1);
|
|
9073
9135
|
}
|
|
9074
9136
|
|
|
9137
|
+
// src/commands/doctor.ts
|
|
9138
|
+
import { existsSync as existsSync34, readFileSync as readFileSync25 } from "fs";
|
|
9139
|
+
import { join as join34, resolve as resolve18 } from "path";
|
|
9140
|
+
import chalk21 from "chalk";
|
|
9141
|
+
import { Command as Command24 } from "commander";
|
|
9142
|
+
|
|
9143
|
+
// src/lib/doctor.ts
|
|
9144
|
+
function checkCheckoutCurrency(facts) {
|
|
9145
|
+
const findings = [];
|
|
9146
|
+
if (facts.currentBranch === "HEAD" || facts.currentBranch === "") {
|
|
9147
|
+
findings.push({
|
|
9148
|
+
check: "checkout-detached",
|
|
9149
|
+
severity: "error",
|
|
9150
|
+
detail: "The primary checkout has a detached HEAD, so nothing read from it can be attributed to a branch.",
|
|
9151
|
+
remedy: `git switch ${facts.integrationBranch}`
|
|
9152
|
+
});
|
|
9153
|
+
return findings;
|
|
9154
|
+
}
|
|
9155
|
+
if (facts.isPrimary && facts.currentBranch !== facts.integrationBranch) {
|
|
9156
|
+
findings.push({
|
|
9157
|
+
check: "checkout-off-integration",
|
|
9158
|
+
severity: "error",
|
|
9159
|
+
detail: `The primary checkout is on '${facts.currentBranch}', not '${facts.integrationBranch}'. Anything read from it \u2014 versions, migrations, whether a feature exists \u2014 describes that branch, not the repo. This is how an audit gets run against dead code (AGENTS.md \xA72).`,
|
|
9160
|
+
remedy: `git switch ${facts.integrationBranch} && git pull (do the work in a worktree instead)`
|
|
9161
|
+
});
|
|
9162
|
+
}
|
|
9163
|
+
if (facts.isPrimary && facts.isDirty) {
|
|
9164
|
+
findings.push({
|
|
9165
|
+
check: "checkout-dirty",
|
|
9166
|
+
severity: "warn",
|
|
9167
|
+
detail: "The primary checkout has uncommitted changes, so what it contains is neither the integration branch nor anything reviewed. Editing the primary directly is what AGENTS.md \xA71 exists to prevent; work belongs in a worktree.",
|
|
9168
|
+
remedy: 'git stash push -m "<what this is>" or commit it on a branch, then work in a worktree'
|
|
9169
|
+
});
|
|
9170
|
+
}
|
|
9171
|
+
if (facts.hasUpstream && facts.behind > 0) {
|
|
9172
|
+
const diverged = facts.ahead > 0 ? `, and ${String(facts.ahead)} ahead (diverged)` : "";
|
|
9173
|
+
const where = facts.isPrimary ? "The primary checkout" : "This worktree";
|
|
9174
|
+
findings.push({
|
|
9175
|
+
check: "checkout-behind",
|
|
9176
|
+
// In a worktree, behind-its-own-upstream means someone else pushed to the
|
|
9177
|
+
// branch — worth knowing, but not a reason to distrust everything read
|
|
9178
|
+
// from it the way a stale primary is.
|
|
9179
|
+
severity: facts.isPrimary ? "error" : "warn",
|
|
9180
|
+
detail: `${where} is ${String(facts.behind)} commit(s) behind its upstream${diverged}. Every file read from it may be stale, including the ones that look like authoritative state.`,
|
|
9181
|
+
remedy: "git pull --ff-only"
|
|
9182
|
+
});
|
|
9183
|
+
}
|
|
9184
|
+
return findings;
|
|
9185
|
+
}
|
|
9186
|
+
function checkCoreVersionCurrency(facts) {
|
|
9187
|
+
if (facts.localCoreVersion === null || facts.remoteCoreVersion === null) return [];
|
|
9188
|
+
if (facts.localCoreVersion === facts.remoteCoreVersion) return [];
|
|
9189
|
+
if (!facts.isPrimary) return [];
|
|
9190
|
+
return [
|
|
9191
|
+
{
|
|
9192
|
+
check: "core-version-stale",
|
|
9193
|
+
severity: "error",
|
|
9194
|
+
detail: `This checkout records core ${facts.localCoreVersion}, but ${facts.integrationBranch} carries ${facts.remoteCoreVersion}. Sizing an upgrade or reasoning about which fixes this instance has from the local number will be wrong.`,
|
|
9195
|
+
remedy: "git pull --ff-only, then re-read biffo.core.json"
|
|
9196
|
+
}
|
|
9197
|
+
];
|
|
9198
|
+
}
|
|
9199
|
+
function checkFossilCoreVersion(facts) {
|
|
9200
|
+
if (facts.fossilCoreVersion === null || facts.localCoreVersion === null) return [];
|
|
9201
|
+
if (facts.fossilCoreVersion === facts.localCoreVersion) return [];
|
|
9202
|
+
return [
|
|
9203
|
+
{
|
|
9204
|
+
check: "fossil-core-version",
|
|
9205
|
+
severity: "warn",
|
|
9206
|
+
detail: `core.version says ${facts.fossilCoreVersion} while biffo.core.json says ${facts.localCoreVersion}. core.version is inherited from \`biffo init\` and has not been maintained since; biffo.core.json is the authority. Reading the wrong one is #788.`,
|
|
9207
|
+
remedy: "Ignore core.version. `biffo core upgrade` removes it when it can prove it is inherited, and deliberately keeps it when it may have been repurposed."
|
|
9208
|
+
}
|
|
9209
|
+
];
|
|
9210
|
+
}
|
|
9211
|
+
function checkStaleBranches(facts) {
|
|
9212
|
+
const gone = facts.branches.filter((b) => b.name !== facts.currentBranch && b.track.includes("gone")).map((b) => b.name);
|
|
9213
|
+
if (gone.length === 0) return [];
|
|
9214
|
+
const upgradeOnes = gone.filter((n) => n.startsWith(UPGRADE_BRANCH_PREFIX)).length;
|
|
9215
|
+
const note = upgradeOnes > 0 ? ` (${String(upgradeOnes)} from core upgrades)` : "";
|
|
9216
|
+
return [
|
|
9217
|
+
{
|
|
9218
|
+
check: "stale-branches",
|
|
9219
|
+
severity: "warn",
|
|
9220
|
+
detail: `${String(gone.length)} local branch(es) have a gone upstream${note} \u2014 their remote copy was deleted, which happens on merge. Squash merges mean \`git branch -d\` refuses them and \`--merged\` never lists them, so they accumulate silently (#758).`,
|
|
9221
|
+
remedy: `git branch -D ${gone.slice(0, 3).join(" ")}${gone.length > 3 ? " \u2026" : ""}`
|
|
9222
|
+
}
|
|
9223
|
+
];
|
|
9224
|
+
}
|
|
9225
|
+
function checkWorktrees(facts, behindThreshold = 50) {
|
|
9226
|
+
const findings = [];
|
|
9227
|
+
const goneBranches = new Set(
|
|
9228
|
+
facts.branches.filter((b) => b.track.includes("gone")).map((b) => b.name)
|
|
9229
|
+
);
|
|
9230
|
+
const merged = facts.worktrees.filter((w) => goneBranches.has(w.branch));
|
|
9231
|
+
if (merged.length > 0) {
|
|
9232
|
+
findings.push({
|
|
9233
|
+
check: "worktree-merged",
|
|
9234
|
+
severity: "warn",
|
|
9235
|
+
detail: `${String(merged.length)} worktree(s) sit on a branch whose remote copy is gone, i.e. whose PR has merged: ${merged.map((w) => w.path).join(", ")}.`,
|
|
9236
|
+
remedy: "git worktree remove <path>"
|
|
9237
|
+
});
|
|
9238
|
+
}
|
|
9239
|
+
const ancient = facts.worktrees.filter(
|
|
9240
|
+
(w) => !goneBranches.has(w.branch) && w.behind !== null && w.behind >= behindThreshold
|
|
9241
|
+
);
|
|
9242
|
+
if (ancient.length > 0) {
|
|
9243
|
+
findings.push({
|
|
9244
|
+
check: "worktree-stale",
|
|
9245
|
+
severity: "warn",
|
|
9246
|
+
detail: `${String(ancient.length)} worktree(s) are more than ${String(behindThreshold)} commits behind ${facts.integrationBranch}: ${ancient.map((w) => `${w.path} (${String(w.behind)})`).join(", ")}. Reading one of these describes the repo as it was, not as it is.`,
|
|
9247
|
+
remedy: "git worktree remove <path>, or rebase it onto the integration branch"
|
|
9248
|
+
});
|
|
9249
|
+
}
|
|
9250
|
+
return findings;
|
|
9251
|
+
}
|
|
9252
|
+
function runDoctorChecks(facts) {
|
|
9253
|
+
return [
|
|
9254
|
+
...checkCheckoutCurrency(facts),
|
|
9255
|
+
...checkCoreVersionCurrency(facts),
|
|
9256
|
+
...checkFossilCoreVersion(facts),
|
|
9257
|
+
...checkStaleBranches(facts),
|
|
9258
|
+
...checkWorktrees(facts)
|
|
9259
|
+
];
|
|
9260
|
+
}
|
|
9261
|
+
|
|
9262
|
+
// src/commands/doctor.ts
|
|
9263
|
+
var INTEGRATION_BRANCH = "dev";
|
|
9264
|
+
var doctorCommand = new Command24("doctor").description(
|
|
9265
|
+
"Report repo-state conditions that make everything read from this checkout unreliable"
|
|
9266
|
+
).option("--cwd <path>", "Repo root to inspect (defaults to the current directory)").option("--no-fetch", "Skip the fetch; report against refs as they already are locally").action(async (options) => {
|
|
9267
|
+
const cwd = options.cwd ? resolve18(options.cwd) : process.cwd();
|
|
9268
|
+
try {
|
|
9269
|
+
const findings = await runDoctor({ cwd, fetch: options.fetch !== false });
|
|
9270
|
+
printFindings(findings);
|
|
9271
|
+
if (findings.some((f) => f.severity === "error")) process.exit(1);
|
|
9272
|
+
} catch (err) {
|
|
9273
|
+
log.error(err.message);
|
|
9274
|
+
process.exit(1);
|
|
9275
|
+
}
|
|
9276
|
+
});
|
|
9277
|
+
async function runDoctor(options, deps = { git: new GitAdapter() }) {
|
|
9278
|
+
const { git } = deps;
|
|
9279
|
+
if (!await git.isGitRepo(options.cwd)) {
|
|
9280
|
+
throw new Error(`${options.cwd} is not a git repository.`);
|
|
9281
|
+
}
|
|
9282
|
+
if (options.fetch) await git.fetchPrune(options.cwd);
|
|
9283
|
+
const currentBranch = await git.currentBranch(options.cwd);
|
|
9284
|
+
const isPrimary = await git.isPrimaryWorktree(options.cwd);
|
|
9285
|
+
const isDirty = await git.hasUncommittedChanges(options.cwd);
|
|
9286
|
+
const { ahead, behind, hasUpstream } = await git.aheadBehind(options.cwd);
|
|
9287
|
+
const branches = await git.listBranchRefs(options.cwd);
|
|
9288
|
+
const worktreePaths = await git.listWorktrees(options.cwd);
|
|
9289
|
+
const worktrees = await Promise.all(
|
|
9290
|
+
worktreePaths.map(async (w) => ({
|
|
9291
|
+
...w,
|
|
9292
|
+
behind: await git.countBehind(options.cwd, w.branch, `origin/${INTEGRATION_BRANCH}`)
|
|
9293
|
+
}))
|
|
9294
|
+
);
|
|
9295
|
+
const facts = {
|
|
9296
|
+
currentBranch,
|
|
9297
|
+
isPrimary,
|
|
9298
|
+
integrationBranch: INTEGRATION_BRANCH,
|
|
9299
|
+
ahead,
|
|
9300
|
+
behind,
|
|
9301
|
+
hasUpstream,
|
|
9302
|
+
isDirty,
|
|
9303
|
+
localCoreVersion: readLocalCoreVersion(options.cwd),
|
|
9304
|
+
remoteCoreVersion: parseCoreRecord(
|
|
9305
|
+
await git.showFileAtRef(options.cwd, `origin/${INTEGRATION_BRANCH}`, INSTANCE_CORE_FILE)
|
|
9306
|
+
),
|
|
9307
|
+
fossilCoreVersion: readFossil(options.cwd),
|
|
9308
|
+
branches,
|
|
9309
|
+
worktrees
|
|
9310
|
+
};
|
|
9311
|
+
return runDoctorChecks(facts);
|
|
9312
|
+
}
|
|
9313
|
+
function readLocalCoreVersion(cwd) {
|
|
9314
|
+
const path = join34(cwd, INSTANCE_CORE_FILE);
|
|
9315
|
+
if (!existsSync34(path)) return null;
|
|
9316
|
+
try {
|
|
9317
|
+
return parseCoreRecord(readFileSync25(path, "utf8"));
|
|
9318
|
+
} catch {
|
|
9319
|
+
return null;
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9322
|
+
function parseCoreRecord(contents) {
|
|
9323
|
+
if (contents === null) return null;
|
|
9324
|
+
try {
|
|
9325
|
+
const parsed = JSON.parse(contents);
|
|
9326
|
+
return typeof parsed.version === "string" ? parsed.version : null;
|
|
9327
|
+
} catch {
|
|
9328
|
+
return null;
|
|
9329
|
+
}
|
|
9330
|
+
}
|
|
9331
|
+
function readFossil(cwd) {
|
|
9332
|
+
const path = join34(cwd, CORE_VERSION_FILE);
|
|
9333
|
+
if (!existsSync34(path)) return null;
|
|
9334
|
+
try {
|
|
9335
|
+
const value = readFileSync25(path, "utf8").trim();
|
|
9336
|
+
return value === "" ? null : value;
|
|
9337
|
+
} catch {
|
|
9338
|
+
return null;
|
|
9339
|
+
}
|
|
9340
|
+
}
|
|
9341
|
+
function printFindings(findings) {
|
|
9342
|
+
if (findings.length === 0) {
|
|
9343
|
+
log.success("No findings \u2014 this checkout can be trusted.");
|
|
9344
|
+
return;
|
|
9345
|
+
}
|
|
9346
|
+
const errors = findings.filter((f) => f.severity === "error");
|
|
9347
|
+
const warnings = findings.filter((f) => f.severity === "warn");
|
|
9348
|
+
console.log("");
|
|
9349
|
+
for (const f of findings) {
|
|
9350
|
+
const tag = f.severity === "error" ? chalk21.red("error") : chalk21.yellow(" warn");
|
|
9351
|
+
console.log(` ${tag} ${chalk21.bold(f.check)}`);
|
|
9352
|
+
console.log(` ${f.detail}`);
|
|
9353
|
+
console.log(chalk21.dim(` fix: ${f.remedy}`));
|
|
9354
|
+
console.log("");
|
|
9355
|
+
}
|
|
9356
|
+
console.log(
|
|
9357
|
+
chalk21.dim(
|
|
9358
|
+
` ${String(errors.length)} error(s), ${String(warnings.length)} warning(s). Errors mean values read from this checkout may be wrong.
|
|
9359
|
+
`
|
|
9360
|
+
)
|
|
9361
|
+
);
|
|
9362
|
+
}
|
|
9363
|
+
|
|
9075
9364
|
// src/commands/teardown.ts
|
|
9076
9365
|
import { execSync as execSync7 } from "child_process";
|
|
9077
9366
|
import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STSClient3 } from "@aws-sdk/client-sts";
|
|
9078
|
-
import
|
|
9079
|
-
import { Command as
|
|
9367
|
+
import chalk22 from "chalk";
|
|
9368
|
+
import { Command as Command25 } from "commander";
|
|
9080
9369
|
import inquirer8 from "inquirer";
|
|
9081
|
-
var teardownCommand = new
|
|
9370
|
+
var teardownCommand = new Command25("teardown").description(
|
|
9082
9371
|
"Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
|
|
9083
9372
|
).option("--project <name>", "Project name to tear down (reads session if omitted)").option("--skip-destroy", "Skip terraform destroy (only use if infrastructure is already gone)").option(
|
|
9084
9373
|
"--confirm <name>",
|
|
9085
9374
|
"Pre-confirm by supplying the project name (the scriptable form of the typed confirmation)"
|
|
9086
9375
|
).option("-y, --yes", "Skip the typed project-name confirmation entirely").action(
|
|
9087
9376
|
async (options) => {
|
|
9088
|
-
console.log(
|
|
9377
|
+
console.log(chalk22.bold("\n Biffo \u2014 Teardown\n"));
|
|
9089
9378
|
const githubToken = resolveGithubToken4();
|
|
9090
9379
|
const sts = new STSClient3({});
|
|
9091
9380
|
const { Account: accountId } = await sts.send(new GetCallerIdentityCommand3({}));
|
|
@@ -9107,7 +9396,7 @@ var teardownCommand = new Command24("teardown").description(
|
|
|
9107
9396
|
adminEmail = session.config.admin?.email ?? "noop@example.com";
|
|
9108
9397
|
adminUsername = session.config.admin?.username ?? "noop";
|
|
9109
9398
|
domain = session.config.project.domain ?? "";
|
|
9110
|
-
console.log(
|
|
9399
|
+
console.log(chalk22.yellow(" Loaded session for: ") + chalk22.bold(projectName));
|
|
9111
9400
|
if (org && repo) console.log(` Repository: ${org}/${repo}`);
|
|
9112
9401
|
console.log();
|
|
9113
9402
|
} else if (savedConfig) {
|
|
@@ -9120,7 +9409,7 @@ var teardownCommand = new Command24("teardown").description(
|
|
|
9120
9409
|
adminEmail = savedConfig.admin.email;
|
|
9121
9410
|
adminUsername = savedConfig.admin.username;
|
|
9122
9411
|
domain = resolveDnsConfig(savedConfig).domain;
|
|
9123
|
-
console.log(
|
|
9412
|
+
console.log(chalk22.yellow(" Loaded config for: ") + chalk22.bold(projectName));
|
|
9124
9413
|
console.log(` Repository: ${org}/${repo}`);
|
|
9125
9414
|
console.log();
|
|
9126
9415
|
} else {
|
|
@@ -9191,35 +9480,35 @@ var teardownCommand = new Command24("teardown").description(
|
|
|
9191
9480
|
throw err;
|
|
9192
9481
|
}
|
|
9193
9482
|
}
|
|
9194
|
-
console.log(
|
|
9483
|
+
console.log(chalk22.red.bold(" This will permanently delete:\n"));
|
|
9195
9484
|
if (deployedEnvs.length > 0 || hasGlobal) {
|
|
9196
|
-
console.log(
|
|
9485
|
+
console.log(chalk22.red(" Infrastructure (via GitHub Actions terraform destroy):"));
|
|
9197
9486
|
for (const env of deployedEnvs) {
|
|
9198
9487
|
console.log(
|
|
9199
|
-
` ${
|
|
9488
|
+
` ${chalk22.red("\u2717")} ${env} \u2014 VPC, RDS, Lambda, Cognito, CloudFront, EventBridge`
|
|
9200
9489
|
);
|
|
9201
9490
|
}
|
|
9202
9491
|
if (hasGlobal) {
|
|
9203
|
-
console.log(` ${
|
|
9492
|
+
console.log(` ${chalk22.red("\u2717")} global \u2014 Route 53 hosted zone, ACM certificate`);
|
|
9204
9493
|
}
|
|
9205
9494
|
console.log();
|
|
9206
9495
|
}
|
|
9207
9496
|
for (const line of formatSiblingPlan(siblings, options.skipDestroy === true)) {
|
|
9208
9497
|
console.log(line);
|
|
9209
9498
|
}
|
|
9210
|
-
console.log(
|
|
9211
|
-
console.log(` ${
|
|
9499
|
+
console.log(chalk22.red(" Biffo resources:"));
|
|
9500
|
+
console.log(` ${chalk22.red("\u2717")} GitHub repository ${chalk22.bold(`${org}/${repo}`)}`);
|
|
9212
9501
|
console.log(
|
|
9213
|
-
` ${
|
|
9502
|
+
` ${chalk22.red("\u2717")} IAM role ${chalk22.bold(`biffo-github-actions-${projectName}`)}`
|
|
9214
9503
|
);
|
|
9215
9504
|
console.log(
|
|
9216
|
-
` ${
|
|
9505
|
+
` ${chalk22.red("\u2717")} S3 bucket ${chalk22.bold(stateBucket)} (all versions)`
|
|
9217
9506
|
);
|
|
9218
|
-
console.log(` ${
|
|
9507
|
+
console.log(` ${chalk22.red("\u2717")} Local session file`);
|
|
9219
9508
|
if (options.skipDestroy) {
|
|
9220
9509
|
console.log();
|
|
9221
9510
|
console.log(
|
|
9222
|
-
|
|
9511
|
+
chalk22.yellow(
|
|
9223
9512
|
" --skip-destroy: NO terraform destroy runs, for this project or any sibling.\n Everything above is deleted, but any infrastructure still standing is left\n standing \u2014 and, with the state buckets gone, orphaned."
|
|
9224
9513
|
)
|
|
9225
9514
|
);
|
|
@@ -9407,30 +9696,30 @@ async function assertSiblingsAreDestroyable(github, siblings) {
|
|
|
9407
9696
|
}
|
|
9408
9697
|
function formatSiblingPlan(siblings, skipDestroy) {
|
|
9409
9698
|
if (siblings.length === 0) return [];
|
|
9410
|
-
const lines = [
|
|
9699
|
+
const lines = [chalk22.red(` Sibling apps (${siblings.length}) \u2014 ADR-0007:`)];
|
|
9411
9700
|
for (const s of siblings) {
|
|
9412
9701
|
const envs = s.environments.join(", ");
|
|
9413
9702
|
if (s.repoState === "gone") {
|
|
9414
9703
|
lines.push(
|
|
9415
|
-
` ${
|
|
9704
|
+
` ${chalk22.yellow("!")} ${chalk22.bold(`${s.org}/${s.repo}`)} \u2014 repo already deleted; its ${envs} infrastructure CANNOT be destroyed and will be left standing`
|
|
9416
9705
|
);
|
|
9417
9706
|
continue;
|
|
9418
9707
|
}
|
|
9419
9708
|
lines.push(
|
|
9420
|
-
` ${
|
|
9709
|
+
` ${chalk22.red("\u2717")} GitHub repository ${chalk22.bold(`${s.org}/${s.repo}`)} ` + (s.pathPrefix === ROOT_SIBLING_NAME ? "(the application, routed at /)" : `(routed at /${s.pathPrefix})`)
|
|
9421
9710
|
);
|
|
9422
9711
|
lines.push(
|
|
9423
|
-
` ${
|
|
9712
|
+
` ${chalk22.red("\u2717")} ${skipDestroy ? "infrastructure NOT destroyed (--skip-destroy)" : `${envs} infrastructure \u2014 S3 site bucket, Lambda, API Gateway`}`
|
|
9424
9713
|
);
|
|
9425
9714
|
lines.push(
|
|
9426
|
-
` ${
|
|
9715
|
+
` ${chalk22.red("\u2717")} IAM role ${chalk22.bold(`biffo-github-actions-${s.projectName}`)}`
|
|
9427
9716
|
);
|
|
9428
9717
|
lines.push(
|
|
9429
|
-
` ${
|
|
9718
|
+
` ${chalk22.red("\u2717")} S3 bucket ${chalk22.bold(`${s.projectName}-terraform-state-${s.accountId}`)}`
|
|
9430
9719
|
);
|
|
9431
9720
|
if (s.pendingRegistrationPr !== void 0) {
|
|
9432
9721
|
lines.push(
|
|
9433
|
-
|
|
9722
|
+
chalk22.dim(` registration PR #${s.pendingRegistrationPr} is still open \u2014 never routed`)
|
|
9434
9723
|
);
|
|
9435
9724
|
}
|
|
9436
9725
|
}
|
|
@@ -9468,7 +9757,7 @@ async function confirmTeardown(projectName, options) {
|
|
|
9468
9757
|
{
|
|
9469
9758
|
type: "input",
|
|
9470
9759
|
name: "confirm",
|
|
9471
|
-
message: `Type ${
|
|
9760
|
+
message: `Type ${chalk22.bold(projectName)} to confirm:`
|
|
9472
9761
|
}
|
|
9473
9762
|
]);
|
|
9474
9763
|
return confirm === projectName;
|
|
@@ -9486,7 +9775,7 @@ function resolveGithubToken4() {
|
|
|
9486
9775
|
}
|
|
9487
9776
|
|
|
9488
9777
|
// src/index.ts
|
|
9489
|
-
var program = new
|
|
9778
|
+
var program = new Command26();
|
|
9490
9779
|
function cliVersion() {
|
|
9491
9780
|
try {
|
|
9492
9781
|
return getLatestCoreVersion();
|
|
@@ -9504,6 +9793,7 @@ program.addCommand(dataCommand);
|
|
|
9504
9793
|
program.addCommand(coreCommand);
|
|
9505
9794
|
program.addCommand(siblingCommand);
|
|
9506
9795
|
program.addCommand(checkCommand);
|
|
9796
|
+
program.addCommand(doctorCommand);
|
|
9507
9797
|
registerNonInteractive(program);
|
|
9508
9798
|
program.parseAsync().catch((err) => {
|
|
9509
9799
|
if (err instanceof NonInteractiveError) {
|