@continuous-excellence/ze-great-dashboard-aws 0.14.3 → 0.14.5

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.
@@ -1,4 +1,5 @@
1
1
  import { type BootstrapConfig, type BootstrapConsistency, type BootstrapKind } from './bootstrap.js';
2
+ import { type BootstrapRemediation } from './remediation.js';
2
3
  export type BootstrapResourceDifference = {
3
4
  path?: string;
4
5
  differenceType?: string;
@@ -32,6 +33,7 @@ export type BootstrapCheck = {
32
33
  ok: boolean;
33
34
  packageVersion: string;
34
35
  stacks: BootstrapStackCheck[];
36
+ remediation: BootstrapRemediation;
35
37
  };
36
38
  export type BootstrapCheckDependencies = {
37
39
  execute(command: string, args: string[]): Promise<string>;
@@ -1,4 +1,5 @@
1
1
  import { type ComputeMode } from './compute-mode.js';
2
+ import { type BootstrapRemediation } from './remediation.js';
2
3
  export { type ComputeMode, computeMode, resolveComputeMode } from './compute-mode.js';
3
4
  export type BootstrapKind = 'core' | 'github-oidc';
4
5
  export type CloudFormationParameterValue = {
@@ -55,6 +56,7 @@ export type BootstrapPlan = {
55
56
  packageTemplates: BootstrapTemplateInspection[];
56
57
  configuration: BootstrapConfig;
57
58
  notes: string[];
59
+ remediation: BootstrapRemediation;
58
60
  };
59
61
  export type BootstrapConsistency = {
60
62
  ok: boolean;
package/dist/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // packages/aws/src/cli.ts
4
+ import { execFile as execFile3 } from "node:child_process";
4
5
  import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
5
6
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7
+ import { promisify as promisify3 } from "node:util";
6
8
  import { parse as parse3 } from "yaml";
7
9
 
8
10
  // packages/aws/src/doctor.ts
@@ -159,6 +161,46 @@ function sha256(value2) {
159
161
  return createHash("sha256").update(value2).digest("hex");
160
162
  }
161
163
 
164
+ // packages/aws/src/remediation.ts
165
+ var safetyNote = "AWS mutations are emitted for explicit administrator review and invocation; this library never executes them.";
166
+ function bootstrapRemediation(config, input = {}) {
167
+ const affectedStacks = input.affectedStacks ?? [
168
+ ["core", config.core?.stackName],
169
+ ["github-oidc", config.githubOidc?.stackName]
170
+ ].filter((entry) => Boolean(entry[1])).map(([kind, stackName2]) => ({ kind, stackName: stackName2 }));
171
+ const configPath = input.configPath ?? "manifest.json";
172
+ const checkCommand = `ze-great-dashboard-aws bootstrap check --config ${configPath} --format json`;
173
+ const issues = input.issues ?? [];
174
+ const summary = input.summary ?? (issues.length ? `Bootstrap validation found ${issues.length} issue${issues.length === 1 ? "" : "s"}.` : "Bootstrap validation is ready for the next reviewed operation.");
175
+ return {
176
+ failureSummary: summary,
177
+ affectedStacks,
178
+ immediateSteps: [
179
+ ...issues.length ? ["Review each reported mismatch, access error, or drift finding."] : [],
180
+ input.nextOperation ?? "Run the next bootstrap operation from the generated handoff.",
181
+ "Capture the resulting CloudFormation stack output for the next validation step."
182
+ ],
183
+ upgradeSteps: [
184
+ "If the contract or template revision is stale, generate parameters from this installed package and create a reviewed UPDATE change set for each affected stack.",
185
+ "Review IAM actions, retained resources, parameters, and the change set before an administrator executes it."
186
+ ],
187
+ revalidateCommand: checkCommand,
188
+ safetyNote
189
+ };
190
+ }
191
+ function formatBootstrapRemediationText(remediation) {
192
+ return [
193
+ `Remediation: ${remediation.failureSummary}`,
194
+ `Affected stacks: ${remediation.affectedStacks.length ? remediation.affectedStacks.map(({ kind, stackName: stackName2, issue }) => `${kind}=${stackName2}${issue ? ` (${issue})` : ""}`).join(", ") : "none identified"}`,
195
+ "Immediate steps:",
196
+ ...remediation.immediateSteps.map((step, index) => ` ${index + 1}. ${step}`),
197
+ "Upgrade steps:",
198
+ ...remediation.upgradeSteps.map((step, index) => ` ${index + 1}. ${step}`),
199
+ `Revalidate: ${remediation.revalidateCommand}`,
200
+ `Safety: ${remediation.safetyNote}`
201
+ ];
202
+ }
203
+
162
204
  // packages/aws/src/bootstrap.ts
163
205
  var templates = {
164
206
  core: {
@@ -231,7 +273,10 @@ async function bootstrapPlan(config) {
231
273
  "Templates are owned by the installed npm package; package.json and package-lock.json pin the source version.",
232
274
  "Generated CloudFormation parameter files and describe-stacks captures are deployment artifacts, not source configuration.",
233
275
  "This plan performs no AWS or GitHub mutations."
234
- ]
276
+ ],
277
+ remediation: bootstrapRemediation(config, {
278
+ nextOperation: "Review the installed templates, then run bootstrap preflight before creating a change set."
279
+ })
235
280
  };
236
281
  }
237
282
  function capturedStack(input) {
@@ -1118,6 +1163,7 @@ function formatBootstrapCheckText(result) {
1118
1163
  );
1119
1164
  }
1120
1165
  }
1166
+ lines.push("", ...formatBootstrapRemediationText(result.remediation));
1121
1167
  return `${lines.join("\n")}
1122
1168
  `;
1123
1169
  }
@@ -1337,12 +1383,30 @@ async function checkBootstrap(config, options, dependencies) {
1337
1383
  return checked;
1338
1384
  })
1339
1385
  );
1386
+ const failed = stacks.filter(
1387
+ ({ consistency, resourceDrift: resourceDrift2 }) => !consistency.ok || Boolean(resourceDrift2 && !resourceDrift2.ok)
1388
+ );
1340
1389
  return {
1341
1390
  ok: stacks.every(
1342
1391
  ({ consistency, resourceDrift: drift }) => consistency.ok && (!drift || drift.ok)
1343
1392
  ),
1344
1393
  packageVersion: plan.packageVersion,
1345
- stacks
1394
+ stacks,
1395
+ remediation: bootstrapRemediation(config, {
1396
+ summary: failed.length ? `Bootstrap validation failed for ${failed.length} stack${failed.length === 1 ? "" : "s"}.` : "Bootstrap stacks are consistent and ready for the deployment check.",
1397
+ affectedStacks: (failed.length ? failed : stacks).map(
1398
+ ({ kind, stackName: stackName2, consistency, resourceDrift: resourceDrift2 }) => ({
1399
+ kind,
1400
+ stackName: stackName2,
1401
+ ...!consistency.ok ? { issue: consistency.mismatches[0] ?? "consistency mismatch" } : resourceDrift2 && !resourceDrift2.ok ? { issue: `resource drift: ${resourceDrift2.status}` } : {}
1402
+ })
1403
+ ),
1404
+ nextOperation: failed.length ? "Apply only reviewed administrator-approved bootstrap updates, then capture both stacks again." : "Run the deployment or release check that consumes these bootstrap outputs.",
1405
+ issues: failed.flatMap(({ consistency, resourceDrift: resourceDrift2 }) => [
1406
+ ...consistency.mismatches,
1407
+ ...resourceDrift2 && !resourceDrift2.ok ? [`resource drift: ${resourceDrift2.status}`] : []
1408
+ ])
1409
+ })
1346
1410
  };
1347
1411
  }
1348
1412
 
@@ -1476,7 +1540,7 @@ function changeSetCommands(input) {
1476
1540
  }
1477
1541
  var githubOidcProvider = {
1478
1542
  name: "github-oidc",
1479
- async prerequisite(config, runner) {
1543
+ async prerequisite(config, runner2) {
1480
1544
  const repository = config.githubOidc?.repository;
1481
1545
  if (!repository)
1482
1546
  return {
@@ -1484,7 +1548,7 @@ var githubOidcProvider = {
1484
1548
  blocking: false,
1485
1549
  detail: "GitHub repository is absent from the manifest."
1486
1550
  };
1487
- if (!runner)
1551
+ if (!runner2)
1488
1552
  return {
1489
1553
  status: "unverified",
1490
1554
  blocking: false,
@@ -1492,7 +1556,7 @@ var githubOidcProvider = {
1492
1556
  };
1493
1557
  try {
1494
1558
  const response = JSON.parse(
1495
- await runner.execute("gh", ["api", `repos/${repository}/actions/oidc/customization/sub`])
1559
+ await runner2.execute("gh", ["api", `repos/${repository}/actions/oidc/customization/sub`])
1496
1560
  );
1497
1561
  const keys = response.include_claim_keys;
1498
1562
  if (response.use_default === false && Array.isArray(keys) && githubSubjectKeys.every((key) => keys.includes(key)))
@@ -1559,6 +1623,10 @@ async function bootstrapHandoff(input) {
1559
1623
  templatePath,
1560
1624
  parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1561
1625
  workDir: input.workDir
1626
+ }),
1627
+ remediation: bootstrapRemediation(input.config, {
1628
+ configPath: input.configPath,
1629
+ nextOperation: "Review and execute the core bootstrap change set, then capture the core stack."
1562
1630
  })
1563
1631
  };
1564
1632
  }
@@ -1602,7 +1670,11 @@ async function bootstrapHandoff(input) {
1602
1670
  prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
1603
1671
  input.config,
1604
1672
  input.runner
1605
- )
1673
+ ),
1674
+ remediation: bootstrapRemediation(input.config, {
1675
+ configPath: input.configPath,
1676
+ nextOperation: "Review and execute the GitHub OIDC change set, then capture the GitHub OIDC stack."
1677
+ })
1606
1678
  };
1607
1679
  }
1608
1680
  const prerequisite = await (input.provider ?? githubOidcProvider).prerequisite(
@@ -1620,7 +1692,12 @@ async function bootstrapHandoff(input) {
1620
1692
  "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
1621
1693
  ],
1622
1694
  commands: [],
1623
- prerequisite
1695
+ prerequisite,
1696
+ remediation: bootstrapRemediation(input.config, {
1697
+ configPath: input.configPath,
1698
+ summary: "The GitHub Environment immutable-subject prerequisite is not verified.",
1699
+ nextOperation: "A GitHub administrator must complete and verify the immutable-subject migration, then rerun bootstrap handoff."
1700
+ })
1624
1701
  };
1625
1702
  return {
1626
1703
  phase: "application-gateway",
@@ -1631,7 +1708,11 @@ async function bootstrapHandoff(input) {
1631
1708
  "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
1632
1709
  ],
1633
1710
  commands: [],
1634
- prerequisite
1711
+ prerequisite,
1712
+ remediation: bootstrapRemediation(input.config, {
1713
+ configPath: input.configPath,
1714
+ nextOperation: "Configure and verify the consumer gateway, then run bootstrap verify and the deployment check."
1715
+ })
1635
1716
  };
1636
1717
  }
1637
1718
  function assertEqual(actual, expected, label) {
@@ -1751,7 +1832,10 @@ async function verifyBootstrap(input) {
1751
1832
  "--body",
1752
1833
  executionRole
1753
1834
  ]
1754
- ]
1835
+ ],
1836
+ remediation: bootstrapRemediation(input.config, {
1837
+ nextOperation: "Set the reviewed GitHub Environment variables, then run bootstrap check before deployment."
1838
+ })
1755
1839
  };
1756
1840
  }
1757
1841
 
@@ -1768,10 +1852,10 @@ function repositoryParts(repository) {
1768
1852
  function accountFromArn(arn) {
1769
1853
  return arn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
1770
1854
  }
1771
- async function optional(runner, command, args2) {
1772
- if (!runner) return void 0;
1855
+ async function optional(runner2, command, args2) {
1856
+ if (!runner2) return void 0;
1773
1857
  try {
1774
- return await runner.execute(command, args2);
1858
+ return await runner2.execute(command, args2);
1775
1859
  } catch {
1776
1860
  return void 0;
1777
1861
  }
@@ -1869,9 +1953,18 @@ async function bootstrapPreflight(input) {
1869
1953
  checks.push(
1870
1954
  missing.length ? { name: "manifest", status: "missing", detail: `Missing: ${missing.join(", ")}` } : { name: "manifest", status: "ready", detail: "Manifest has all bootstrap fields." }
1871
1955
  );
1872
- if (missing.length) return { ready: false, checks };
1873
- const runner = input.runner;
1874
- const identityRaw = await optional(runner, "aws", [
1956
+ if (missing.length)
1957
+ return {
1958
+ ready: false,
1959
+ checks,
1960
+ remediation: bootstrapRemediation(input.config, {
1961
+ summary: "Bootstrap manifest configuration is incomplete.",
1962
+ issues: missing,
1963
+ nextOperation: "Complete the missing manifest fields, then rerun bootstrap preflight."
1964
+ })
1965
+ };
1966
+ const runner2 = input.runner;
1967
+ const identityRaw = await optional(runner2, "aws", [
1875
1968
  "sts",
1876
1969
  "get-caller-identity",
1877
1970
  "--output",
@@ -1897,7 +1990,7 @@ async function bootstrapPreflight(input) {
1897
1990
  detail: `AWS account ${identity.Account} does not match provider account ${providerAccount}.`
1898
1991
  }
1899
1992
  );
1900
- const regionRaw = await optional(runner, "aws", ["configure", "get", "region"]);
1993
+ const regionRaw = await optional(runner2, "aws", ["configure", "get", "region"]);
1901
1994
  if (!regionRaw?.trim())
1902
1995
  checks.push({
1903
1996
  name: "aws-region",
@@ -1912,7 +2005,7 @@ async function bootstrapPreflight(input) {
1912
2005
  detail: `AWS Region ${regionRaw.trim()} does not match manifest ${input.config.region}.`
1913
2006
  }
1914
2007
  );
1915
- const providerRaw = await optional(runner, "aws", [
2008
+ const providerRaw = await optional(runner2, "aws", [
1916
2009
  "iam",
1917
2010
  "get-open-id-connect-provider",
1918
2011
  "--open-id-connect-provider-arn",
@@ -1926,7 +2019,7 @@ async function bootstrapPreflight(input) {
1926
2019
  } : absent(providerRaw) ? { name: "oidc-provider", status: "missing", detail: "OIDC provider does not exist." } : { name: "oidc-provider", status: "ready", detail: "OIDC provider exists." }
1927
2020
  );
1928
2021
  const repository = input.config.githubOidc?.repository ?? "";
1929
- const repoRaw = await optional(runner, "gh", ["api", `repos/${repository}`]);
2022
+ const repoRaw = await optional(runner2, "gh", ["api", `repos/${repository}`]);
1930
2023
  const repo = parseOrUnavailable(repoRaw);
1931
2024
  if (!repoRaw)
1932
2025
  checks.push({
@@ -1952,7 +2045,7 @@ async function bootstrapPreflight(input) {
1952
2045
  status: "ready",
1953
2046
  detail: "GitHub repository identity matches the manifest."
1954
2047
  });
1955
- const environmentRaw = await optional(runner, "gh", [
2048
+ const environmentRaw = await optional(runner2, "gh", [
1956
2049
  "api",
1957
2050
  `repos/${repository}/environments/${input.config.githubOidc?.environment}`
1958
2051
  ]);
@@ -1971,23 +2064,28 @@ async function bootstrapPreflight(input) {
1971
2064
  detail: "GitHub Environment exists; its policy is administrator-owned context."
1972
2065
  }
1973
2066
  );
1974
- const subject = await (input.provider ?? githubOidcProvider).prerequisite(input.config, runner);
2067
+ const subject = await (input.provider ?? githubOidcProvider).prerequisite(input.config, runner2);
1975
2068
  checks.push({
1976
2069
  name: "immutable-subject",
1977
2070
  status: subject.status === "immutable-subject-required" ? "mismatch" : subject.status,
1978
2071
  detail: subject.detail
1979
2072
  });
2073
+ const ready = !checks.some(({ status }) => status === "missing" || status === "mismatch");
1980
2074
  return {
1981
- ready: !checks.some(({ status }) => status === "missing" || status === "mismatch"),
1982
- checks
2075
+ ready,
2076
+ checks,
2077
+ remediation: bootstrapRemediation(input.config, {
2078
+ summary: ready ? "Bootstrap preflight is ready." : "Bootstrap preflight found configuration or identity issues.",
2079
+ issues: checks.filter(({ status }) => status === "missing" || status === "mismatch").map(({ detail }) => detail),
2080
+ nextOperation: ready ? "Run bootstrap plan, then create and review the next bootstrap change set." : "Resolve the reported configuration or identity issues, then rerun bootstrap preflight."
2081
+ })
1983
2082
  };
1984
2083
  }
1985
2084
  function quote(args2) {
1986
2085
  return args2.map((arg) => `'${arg.replaceAll("'", `'\\"'\\"'`)}'`).join(" ");
1987
2086
  }
1988
- async function bootstrapGuide(input) {
1989
- const handoff = await bootstrapHandoff(input);
1990
- const plan = await bootstrapPlan(input.config);
2087
+ async function bootstrapGuideReport(input) {
2088
+ const [handoff, plan] = await Promise.all([bootstrapHandoff(input), bootstrapPlan(input.config)]);
1991
2089
  const lines = [
1992
2090
  `Phase: ${handoff.phase}`,
1993
2091
  `Package version: ${plan.packageVersion}`,
@@ -2024,8 +2122,9 @@ async function bootstrapGuide(input) {
2024
2122
  );
2025
2123
  for (const command of verified.githubEnvironmentInstructions) lines.push(quote(command));
2026
2124
  }
2027
- return `${lines.join("\n")}
2028
- `;
2125
+ lines.push("", ...formatBootstrapRemediationText(plan.remediation));
2126
+ return { handoff, plan, guide: `${lines.join("\n")}
2127
+ ` };
2029
2128
  }
2030
2129
 
2031
2130
  // packages/aws/src/index.ts
@@ -2357,11 +2456,20 @@ async function runDoctor(options, dependencies = actualDependencies) {
2357
2456
  checkResult.detail = `WARNING: ${checkResult.detail}`;
2358
2457
  }
2359
2458
  }
2360
- return checks;
2459
+ const remediation = bootstrapRemediation(
2460
+ {},
2461
+ {
2462
+ summary: checks.some(({ ok }) => !ok) ? "Deployment doctor found issues that require operator review." : "Deployment doctor is healthy; proceed to the deployment check.",
2463
+ issues: checks.filter(({ ok }) => !ok).map(({ detail }) => detail),
2464
+ nextOperation: checks.some(({ ok }) => !ok) ? "Resolve failed checks, then rerun the deployment doctor and bootstrap check." : "Run bootstrap check before deploying the release."
2465
+ }
2466
+ );
2467
+ return checks.map((check2) => ({ ...check2, remediation }));
2361
2468
  }
2362
2469
 
2363
2470
  // packages/aws/src/cli.ts
2364
2471
  var args = process.argv.slice(2);
2472
+ var runCommand = promisify3(execFile3);
2365
2473
  var option = (name, fallback) => {
2366
2474
  const i2 = args.indexOf(name);
2367
2475
  return i2 >= 0 ? args[i2 + 1] : fallback;
@@ -2451,6 +2559,38 @@ function bootstrapStackName(kind, config) {
2451
2559
  function shellCommand(command) {
2452
2560
  return command.map((argument) => `'${argument.replaceAll("'", `'\\"'\\"'`)}'`).join(" ");
2453
2561
  }
2562
+ function outputFormat(defaultFormat = "json") {
2563
+ if (args.includes("--format-shell")) return "shell";
2564
+ const format = option("--format", defaultFormat);
2565
+ if (format !== "json" && format !== "text") throw new Error("--format must be json or text");
2566
+ return format;
2567
+ }
2568
+ function printBootstrap(value2, text, shell, defaultFormat = "json") {
2569
+ const format = outputFormat(defaultFormat);
2570
+ if (format === "shell") {
2571
+ if (!shell) throw new Error("--format-shell is only supported for command handoffs");
2572
+ console.log(shell);
2573
+ } else if (format === "text") console.log(text);
2574
+ else console.log(JSON.stringify(value2));
2575
+ }
2576
+ var runner = {
2577
+ async execute(command, commandArgs) {
2578
+ return (await runCommand(command, commandArgs)).stdout.trim();
2579
+ }
2580
+ };
2581
+ var preflightRunner = {
2582
+ async execute(command, commandArgs) {
2583
+ try {
2584
+ return await runner.execute(command, commandArgs);
2585
+ } catch (error) {
2586
+ if (error && typeof error === "object" && "stderr" in error) {
2587
+ const stderr = error.stderr;
2588
+ if (typeof stderr === "string" && /not found|404|nosuchentity/i.test(stderr)) return stderr;
2589
+ }
2590
+ throw error;
2591
+ }
2592
+ }
2593
+ };
2454
2594
  async function templateParameters(mode = "lambda") {
2455
2595
  const template = await cloudFormationTemplate(mode);
2456
2596
  const block = template.match(/^Parameters:\n[\s\S]*?(?=^[A-Za-z][A-Za-z0-9]*:\s*$)/m)?.[0];
@@ -2574,11 +2714,7 @@ try {
2574
2714
  githubOidcStackPath: option("--github-oidc-stack-json")
2575
2715
  },
2576
2716
  {
2577
- async execute(command, commandArgs) {
2578
- const { execFile: execFile3 } = await import("node:child_process");
2579
- const { promisify: promisify3 } = await import("node:util");
2580
- return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2581
- },
2717
+ execute: runner.execute,
2582
2718
  async fetch(url) {
2583
2719
  return fetch(url);
2584
2720
  },
@@ -2586,10 +2722,14 @@ try {
2586
2722
  packageVersion
2587
2723
  }
2588
2724
  );
2589
- for (const check of checks)
2590
- console.log(
2591
- `${check.warning ? "WARN" : check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`
2592
- );
2725
+ const remediation = checks[0]?.remediation;
2726
+ if (outputFormat("text") === "text") {
2727
+ for (const check of checks)
2728
+ console.log(
2729
+ `${check.warning ? "WARN" : check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`
2730
+ );
2731
+ if (remediation) console.log(formatBootstrapRemediationText(remediation).join("\n"));
2732
+ } else console.log(JSON.stringify({ checks, remediation }));
2593
2733
  if (checks.some(({ ok }) => !ok)) process.exitCode = 1;
2594
2734
  } else if (args[0] === "parameters") {
2595
2735
  const output = option("--output", "aws-dashboard-parameters.json") ?? "aws-dashboard-parameters.json";
@@ -2636,7 +2776,7 @@ try {
2636
2776
  });
2637
2777
  await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
2638
2778
  `);
2639
- console.log(JSON.stringify({ output }));
2779
+ printBootstrap({ output }, `Wrote ${output}. Next: run the generated deployment handoff.`);
2640
2780
  } else if (args[0] === "bootstrap") {
2641
2781
  const action = args[1];
2642
2782
  if (action === "init") {
@@ -2648,13 +2788,6 @@ try {
2648
2788
  if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT"))
2649
2789
  throw error;
2650
2790
  }
2651
- const runner = {
2652
- async execute(command, commandArgs) {
2653
- const { execFile: execFile3 } = await import("node:child_process");
2654
- const { promisify: promisify3 } = await import("node:util");
2655
- return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2656
- }
2657
- };
2658
2791
  const manifest = await scaffoldBootstrapManifest({
2659
2792
  slug: requiredOption("--slug"),
2660
2793
  repository: requiredOption("--repository"),
@@ -2670,109 +2803,105 @@ try {
2670
2803
  });
2671
2804
  await writeFile3(output, `${JSON.stringify(manifest, null, 2)}
2672
2805
  `, { flag: "wx" });
2673
- console.log(JSON.stringify({ output, manifest }));
2806
+ printBootstrap(
2807
+ { output, manifest, remediation: (await bootstrapPlan(manifest)).remediation },
2808
+ `Wrote ${output}. Next: run bootstrap preflight, then bootstrap handoff.`
2809
+ );
2674
2810
  } else {
2675
2811
  const config = await bootstrapConfig();
2676
2812
  if (action === "preflight") {
2677
2813
  requiredOption("--config");
2678
- const runner = {
2679
- async execute(command, commandArgs) {
2680
- const { execFile: execFile3 } = await import("node:child_process");
2681
- const { promisify: promisify3 } = await import("node:util");
2682
- try {
2683
- return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2684
- } catch (error) {
2685
- if (error && typeof error === "object" && "stderr" in error) {
2686
- const stderr = error.stderr;
2687
- if (typeof stderr === "string" && /not found|404|nosuchentity/i.test(stderr))
2688
- return stderr;
2689
- }
2690
- throw error;
2691
- }
2692
- }
2693
- };
2694
- const result = await bootstrapPreflight({ config, runner });
2695
- if (option("--format") === "text")
2696
- for (const check of result.checks)
2697
- console.log(`${check.status.toUpperCase()} ${check.name}: ${check.detail}`);
2698
- else console.log(JSON.stringify(result));
2814
+ const result = await bootstrapPreflight({ config, runner: preflightRunner });
2815
+ printBootstrap(
2816
+ result,
2817
+ [
2818
+ ...result.checks.map(
2819
+ (check) => `${check.status.toUpperCase()} ${check.name}: ${check.detail}`
2820
+ ),
2821
+ "",
2822
+ ...formatBootstrapRemediationText(result.remediation)
2823
+ ].join("\n")
2824
+ );
2699
2825
  if (!result.ready) process.exitCode = 1;
2700
2826
  } else if (action === "guide") {
2701
2827
  const configPath = requiredOption("--config");
2702
2828
  const coreStackPath = option("--core-stack-json");
2703
2829
  const githubStackPath = option("--github-oidc-stack-json");
2704
- const runner = {
2705
- async execute(command, commandArgs) {
2706
- const { execFile: execFile3 } = await import("node:child_process");
2707
- const { promisify: promisify3 } = await import("node:util");
2708
- return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2709
- }
2710
- };
2711
- process.stdout.write(
2712
- await bootstrapGuide({
2713
- config,
2714
- configPath,
2715
- workDir: option("--work-dir"),
2716
- coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2717
- coreStackPath,
2718
- githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2719
- githubOidcStackPath: githubStackPath,
2720
- runner
2721
- })
2830
+ const report = await bootstrapGuideReport({
2831
+ config,
2832
+ configPath,
2833
+ workDir: option("--work-dir"),
2834
+ coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2835
+ coreStackPath,
2836
+ githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2837
+ githubOidcStackPath: githubStackPath,
2838
+ runner
2839
+ });
2840
+ printBootstrap(
2841
+ {
2842
+ handoff: report.handoff,
2843
+ guide: report.guide,
2844
+ remediation: report.handoff.remediation
2845
+ },
2846
+ report.guide,
2847
+ void 0,
2848
+ "text"
2722
2849
  );
2723
2850
  } else if (action === "handoff") {
2724
2851
  const configPath = requiredOption("--config");
2725
2852
  const coreStackPath = option("--core-stack-json");
2726
2853
  const githubStackPath = option("--github-oidc-stack-json");
2727
- const runner = {
2728
- async execute(command, commandArgs) {
2729
- const { execFile: execFile3 } = await import("node:child_process");
2730
- const { promisify: promisify3 } = await import("node:util");
2731
- return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2732
- }
2733
- };
2734
- console.log(
2735
- JSON.stringify(
2736
- await bootstrapHandoff({
2737
- config,
2738
- configPath,
2739
- workDir: option("--work-dir"),
2740
- coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2741
- coreStackPath,
2742
- githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2743
- githubOidcStackPath: githubStackPath,
2744
- runner
2745
- })
2746
- )
2854
+ const handoff = await bootstrapHandoff({
2855
+ config,
2856
+ configPath,
2857
+ workDir: option("--work-dir"),
2858
+ coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2859
+ coreStackPath,
2860
+ githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2861
+ githubOidcStackPath: githubStackPath,
2862
+ runner
2863
+ });
2864
+ printBootstrap(
2865
+ handoff,
2866
+ `${JSON.stringify(handoff, null, 2)}
2867
+ ${formatBootstrapRemediationText(handoff.remediation).join("\n")}`
2747
2868
  );
2748
2869
  } else if (action === "verify") {
2749
2870
  requiredOption("--config");
2750
2871
  const coreStackPath = requiredOption("--core-stack-json");
2751
2872
  const githubStackPath = requiredOption("--github-oidc-stack-json");
2752
- console.log(
2753
- JSON.stringify(
2754
- await verifyBootstrap({
2755
- config,
2756
- coreStack: JSON.parse(await readFile5(coreStackPath, "utf8")),
2757
- githubOidcStack: JSON.parse(await readFile5(githubStackPath, "utf8"))
2758
- })
2759
- )
2873
+ const verified = await verifyBootstrap({
2874
+ config,
2875
+ coreStack: JSON.parse(await readFile5(coreStackPath, "utf8")),
2876
+ githubOidcStack: JSON.parse(await readFile5(githubStackPath, "utf8"))
2877
+ });
2878
+ printBootstrap(
2879
+ verified,
2880
+ `Bootstrap verified.
2881
+ ${formatBootstrapRemediationText(verified.remediation).join("\n")}`
2760
2882
  );
2761
2883
  } else if (action === "template") {
2762
2884
  const kind = bootstrapKind();
2763
2885
  const mode = requireComputeMode(config, option("--mode"));
2764
- console.log(
2765
- JSON.stringify({
2766
- kind,
2767
- mode,
2768
- template: await bootstrapTemplatePath(kind, mode),
2769
- contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind, mode))
2886
+ const template = {
2887
+ kind,
2888
+ mode,
2889
+ template: await bootstrapTemplatePath(kind, mode),
2890
+ contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind, mode)),
2891
+ remediation: bootstrapRemediation(config, {
2892
+ nextOperation: `Review the ${kind} template, then run bootstrap parameters and create a reviewed change set.`
2770
2893
  })
2894
+ };
2895
+ printBootstrap(
2896
+ template,
2897
+ `Template: ${template.template}
2898
+ Contract: ${template.contractVersion}
2899
+ ${formatBootstrapRemediationText(template.remediation).join("\n")}`
2771
2900
  );
2772
2901
  } else if (action === "plan") {
2773
2902
  requiredOption("--config");
2774
2903
  const plan = await bootstrapPlan(config);
2775
- if (option("--format") === "text") {
2904
+ if (outputFormat() === "text") {
2776
2905
  console.log("AWS bootstrap plan (read-only)");
2777
2906
  console.log(`Package version: ${plan.packageVersion}`);
2778
2907
  for (const template of plan.packageTemplates) {
@@ -2788,21 +2917,18 @@ ${template.kind}: ${template.path}`);
2788
2917
  }
2789
2918
  console.log(`
2790
2919
  ${plan.notes.join("\n")}`);
2920
+ console.log(formatBootstrapRemediationText(plan.remediation).join("\n"));
2791
2921
  } else console.log(JSON.stringify(plan));
2792
2922
  } else if (action === "check") {
2793
2923
  requiredOption("--config");
2794
- const { execFile: execFile3 } = await import("node:child_process");
2795
- const { promisify: promisify3 } = await import("node:util");
2796
2924
  const result = await checkBootstrap(
2797
2925
  config,
2798
2926
  { resourceDrift: args.includes("--resource-drift") },
2799
2927
  {
2800
- async execute(command, commandArgs) {
2801
- return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2802
- }
2928
+ execute: runner.execute
2803
2929
  }
2804
2930
  );
2805
- if (option("--format") === "text") process.stdout.write(formatBootstrapCheckText(result));
2931
+ if (outputFormat() === "text") process.stdout.write(formatBootstrapCheckText(result));
2806
2932
  else console.log(JSON.stringify(result));
2807
2933
  if (!result.ok) process.exitCode = 1;
2808
2934
  } else if (action === "parameters") {
@@ -2840,8 +2966,16 @@ ${plan.notes.join("\n")}`);
2840
2966
  }
2841
2967
  await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
2842
2968
  `);
2843
- console.log(
2844
- JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
2969
+ const remediation = (await bootstrapPlan(config)).remediation;
2970
+ printBootstrap(
2971
+ {
2972
+ output,
2973
+ kind,
2974
+ preservedDeployedValues: Boolean(deployedStackPath),
2975
+ remediation
2976
+ },
2977
+ `Wrote ${output}. Next: review and execute the generated ${kind} change set.
2978
+ ${formatBootstrapRemediationText(remediation).join("\n")}`
2845
2979
  );
2846
2980
  } else if (action === "change-set") {
2847
2981
  const kind = bootstrapKind();
@@ -2871,17 +3005,22 @@ ${plan.notes.join("\n")}`);
2871
3005
  ...region2 ? ["--region", region2] : [],
2872
3006
  "--no-cli-pager"
2873
3007
  ];
2874
- console.log(
2875
- JSON.stringify({
2876
- kind,
2877
- packageVersion: plan.packageVersion,
2878
- contractVersion: template?.contractVersion,
2879
- templateRevision: template?.templateRevision,
2880
- templateSha256: template?.sha256,
2881
- reviewRequired: true,
2882
- awsCommand,
2883
- shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
2884
- })
3008
+ const handoff = {
3009
+ kind,
3010
+ packageVersion: plan.packageVersion,
3011
+ contractVersion: template?.contractVersion,
3012
+ templateRevision: template?.templateRevision,
3013
+ templateSha256: template?.sha256,
3014
+ reviewRequired: true,
3015
+ awsCommand,
3016
+ shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0,
3017
+ remediation: plan.remediation
3018
+ };
3019
+ printBootstrap(
3020
+ handoff,
3021
+ `Run the reviewed change set command:
3022
+ ${shellCommand(awsCommand)}`,
3023
+ shellCommand(awsCommand)
2885
3024
  );
2886
3025
  } else {
2887
3026
  throw new Error(
package/dist/doctor.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import { type BootstrapRemediation } from './remediation.js';
1
2
  export type DoctorCheck = {
2
3
  name: string;
3
4
  ok: boolean;
4
5
  detail: string;
5
6
  warning?: boolean;
7
+ remediation?: BootstrapRemediation;
6
8
  };
7
9
  export type DoctorDependencies = {
8
10
  execute(command: string, args: string[]): Promise<string>;
package/dist/guided.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { type BootstrapConfig, type ComputeMode } from './bootstrap.js';
1
+ import { type BootstrapConfig, bootstrapPlan, type ComputeMode } from './bootstrap.js';
2
2
  import { type BootstrapProvider, bootstrapHandoff, type CommandRunner } from './handoff.js';
3
+ import { type BootstrapRemediation } from './remediation.js';
3
4
  export type BootstrapCheckStatus = 'ready' | 'missing' | 'mismatch' | 'unverified';
4
5
  export type BootstrapPreflightCheck = {
5
6
  name: string;
@@ -9,6 +10,12 @@ export type BootstrapPreflightCheck = {
9
10
  export type BootstrapPreflight = {
10
11
  ready: boolean;
11
12
  checks: BootstrapPreflightCheck[];
13
+ remediation: BootstrapRemediation;
14
+ };
15
+ export type BootstrapGuideReport = {
16
+ handoff: Awaited<ReturnType<typeof bootstrapHandoff>>;
17
+ plan: Awaited<ReturnType<typeof bootstrapPlan>>;
18
+ guide: string;
12
19
  };
13
20
  export type BootstrapInitInput = {
14
21
  mode?: ComputeMode;
@@ -31,4 +38,5 @@ export declare function bootstrapPreflight(input: {
31
38
  runner?: CommandRunner;
32
39
  provider?: BootstrapProvider;
33
40
  }): Promise<BootstrapPreflight>;
41
+ export declare function bootstrapGuideReport(input: Parameters<typeof bootstrapHandoff>[0]): Promise<BootstrapGuideReport>;
34
42
  export declare function bootstrapGuide(input: Parameters<typeof bootstrapHandoff>[0]): Promise<string>;
package/dist/handoff.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type BootstrapConfig } from './bootstrap.js';
2
+ import { type BootstrapRemediation } from './remediation.js';
2
3
  export type BootstrapPhase = 'core' | 'github-oidc' | 'github-environment' | 'application-gateway';
3
4
  export type CommandRunner = {
4
5
  execute(command: string, args: string[]): Promise<string>;
@@ -30,6 +31,7 @@ export type BootstrapHandoff = {
30
31
  reviewCheckpoints: string[];
31
32
  commands: HandoffCommand[];
32
33
  prerequisite?: ProviderPrerequisite;
34
+ remediation: BootstrapRemediation;
33
35
  };
34
36
  export type BootstrapVerification = {
35
37
  verified: true;
@@ -43,6 +45,7 @@ export type BootstrapVerification = {
43
45
  cloudFormationExecutionRoleArn: string;
44
46
  };
45
47
  githubEnvironmentInstructions: string[][];
48
+ remediation: BootstrapRemediation;
46
49
  };
47
50
  export declare const githubOidcProvider: BootstrapProvider;
48
51
  export declare function bootstrapHandoff(input: {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { ComputeMode } from './bootstrap.js';
2
2
  export { type BootstrapConfig, type BootstrapConsistency, type BootstrapKind, type BootstrapPlan, type BootstrapTemplateInspection, bootstrapConsistency, bootstrapContractVersion, bootstrapPlan, bootstrapTemplate, bootstrapTemplatePath, bootstrapTemplateRevision, type CloudFormationParameterValue, type ComputeMode, computeMode, coreBootstrapOutputs, type DeployedBootstrapStack, deployedBootstrapStack, mergeBootstrapParameters, requireComputeMode, requiredBootstrapParameters, resolveComputeMode, } from './bootstrap.js';
3
3
  export { type BootstrapCheck, type BootstrapCheckDependencies, type BootstrapResourceDifference, type BootstrapResourceDrift, type BootstrapResourceDriftResult, type BootstrapStackCheck, checkBootstrap, formatBootstrapCheckText, } from './bootstrap-check.js';
4
- export { type BootstrapCheckStatus, type BootstrapInitInput, type BootstrapPreflight, type BootstrapPreflightCheck, bootstrapGuide, bootstrapPreflight, scaffoldBootstrapManifest, } from './guided.js';
4
+ export { type BootstrapCheckStatus, type BootstrapGuideReport, type BootstrapInitInput, type BootstrapPreflight, type BootstrapPreflightCheck, bootstrapGuide, bootstrapGuideReport, bootstrapPreflight, scaffoldBootstrapManifest, } from './guided.js';
5
5
  export { type BootstrapHandoff, type BootstrapPhase, type BootstrapProvider, type BootstrapVerification, bootstrapHandoff, type CommandRunner, githubOidcProvider, verifyBootstrap, } from './handoff.js';
6
+ export { type BootstrapAffectedStack, type BootstrapRemediation, type BootstrapRemediationInput, bootstrapRemediation, formatBootstrapRemediationText, } from './remediation.js';
6
7
  export type ReleaseMetadata = {
7
8
  computeMode: ComputeMode;
8
9
  dashboardVersion: string;
package/dist/index.js CHANGED
@@ -872,6 +872,46 @@ function computeMode(config) {
872
872
  return resolveComputeMode({ persisted: config.mode });
873
873
  }
874
874
 
875
+ // packages/aws/src/remediation.ts
876
+ var safetyNote = "AWS mutations are emitted for explicit administrator review and invocation; this library never executes them.";
877
+ function bootstrapRemediation(config, input = {}) {
878
+ const affectedStacks = input.affectedStacks ?? [
879
+ ["core", config.core?.stackName],
880
+ ["github-oidc", config.githubOidc?.stackName]
881
+ ].filter((entry) => Boolean(entry[1])).map(([kind, stackName2]) => ({ kind, stackName: stackName2 }));
882
+ const configPath = input.configPath ?? "manifest.json";
883
+ const checkCommand = `ze-great-dashboard-aws bootstrap check --config ${configPath} --format json`;
884
+ const issues = input.issues ?? [];
885
+ const summary = input.summary ?? (issues.length ? `Bootstrap validation found ${issues.length} issue${issues.length === 1 ? "" : "s"}.` : "Bootstrap validation is ready for the next reviewed operation.");
886
+ return {
887
+ failureSummary: summary,
888
+ affectedStacks,
889
+ immediateSteps: [
890
+ ...issues.length ? ["Review each reported mismatch, access error, or drift finding."] : [],
891
+ input.nextOperation ?? "Run the next bootstrap operation from the generated handoff.",
892
+ "Capture the resulting CloudFormation stack output for the next validation step."
893
+ ],
894
+ upgradeSteps: [
895
+ "If the contract or template revision is stale, generate parameters from this installed package and create a reviewed UPDATE change set for each affected stack.",
896
+ "Review IAM actions, retained resources, parameters, and the change set before an administrator executes it."
897
+ ],
898
+ revalidateCommand: checkCommand,
899
+ safetyNote
900
+ };
901
+ }
902
+ function formatBootstrapRemediationText(remediation) {
903
+ return [
904
+ `Remediation: ${remediation.failureSummary}`,
905
+ `Affected stacks: ${remediation.affectedStacks.length ? remediation.affectedStacks.map(({ kind, stackName: stackName2, issue }) => `${kind}=${stackName2}${issue ? ` (${issue})` : ""}`).join(", ") : "none identified"}`,
906
+ "Immediate steps:",
907
+ ...remediation.immediateSteps.map((step, index) => ` ${index + 1}. ${step}`),
908
+ "Upgrade steps:",
909
+ ...remediation.upgradeSteps.map((step, index) => ` ${index + 1}. ${step}`),
910
+ `Revalidate: ${remediation.revalidateCommand}`,
911
+ `Safety: ${remediation.safetyNote}`
912
+ ];
913
+ }
914
+
875
915
  // packages/aws/src/bootstrap.ts
876
916
  var templates = {
877
917
  core: {
@@ -944,7 +984,10 @@ async function bootstrapPlan(config) {
944
984
  "Templates are owned by the installed npm package; package.json and package-lock.json pin the source version.",
945
985
  "Generated CloudFormation parameter files and describe-stacks captures are deployment artifacts, not source configuration.",
946
986
  "This plan performs no AWS or GitHub mutations."
947
- ]
987
+ ],
988
+ remediation: bootstrapRemediation(config, {
989
+ nextOperation: "Review the installed templates, then run bootstrap preflight before creating a change set."
990
+ })
948
991
  };
949
992
  }
950
993
  function capturedStack(input) {
@@ -1105,6 +1148,7 @@ function formatBootstrapCheckText(result) {
1105
1148
  );
1106
1149
  }
1107
1150
  }
1151
+ lines.push("", ...formatBootstrapRemediationText(result.remediation));
1108
1152
  return `${lines.join("\n")}
1109
1153
  `;
1110
1154
  }
@@ -1324,12 +1368,30 @@ async function checkBootstrap(config, options, dependencies) {
1324
1368
  return checked;
1325
1369
  })
1326
1370
  );
1371
+ const failed = stacks.filter(
1372
+ ({ consistency, resourceDrift: resourceDrift2 }) => !consistency.ok || Boolean(resourceDrift2 && !resourceDrift2.ok)
1373
+ );
1327
1374
  return {
1328
1375
  ok: stacks.every(
1329
1376
  ({ consistency, resourceDrift: drift }) => consistency.ok && (!drift || drift.ok)
1330
1377
  ),
1331
1378
  packageVersion: plan.packageVersion,
1332
- stacks
1379
+ stacks,
1380
+ remediation: bootstrapRemediation(config, {
1381
+ summary: failed.length ? `Bootstrap validation failed for ${failed.length} stack${failed.length === 1 ? "" : "s"}.` : "Bootstrap stacks are consistent and ready for the deployment check.",
1382
+ affectedStacks: (failed.length ? failed : stacks).map(
1383
+ ({ kind, stackName: stackName2, consistency, resourceDrift: resourceDrift2 }) => ({
1384
+ kind,
1385
+ stackName: stackName2,
1386
+ ...!consistency.ok ? { issue: consistency.mismatches[0] ?? "consistency mismatch" } : resourceDrift2 && !resourceDrift2.ok ? { issue: `resource drift: ${resourceDrift2.status}` } : {}
1387
+ })
1388
+ ),
1389
+ nextOperation: failed.length ? "Apply only reviewed administrator-approved bootstrap updates, then capture both stacks again." : "Run the deployment or release check that consumes these bootstrap outputs.",
1390
+ issues: failed.flatMap(({ consistency, resourceDrift: resourceDrift2 }) => [
1391
+ ...consistency.mismatches,
1392
+ ...resourceDrift2 && !resourceDrift2.ok ? [`resource drift: ${resourceDrift2.status}`] : []
1393
+ ])
1394
+ })
1333
1395
  };
1334
1396
  }
1335
1397
 
@@ -1546,6 +1608,10 @@ async function bootstrapHandoff(input) {
1546
1608
  templatePath,
1547
1609
  parameterPath: workFile(input.workDir, "core-bootstrap.json"),
1548
1610
  workDir: input.workDir
1611
+ }),
1612
+ remediation: bootstrapRemediation(input.config, {
1613
+ configPath: input.configPath,
1614
+ nextOperation: "Review and execute the core bootstrap change set, then capture the core stack."
1549
1615
  })
1550
1616
  };
1551
1617
  }
@@ -1589,7 +1655,11 @@ async function bootstrapHandoff(input) {
1589
1655
  prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
1590
1656
  input.config,
1591
1657
  input.runner
1592
- )
1658
+ ),
1659
+ remediation: bootstrapRemediation(input.config, {
1660
+ configPath: input.configPath,
1661
+ nextOperation: "Review and execute the GitHub OIDC change set, then capture the GitHub OIDC stack."
1662
+ })
1593
1663
  };
1594
1664
  }
1595
1665
  const prerequisite = await (input.provider ?? githubOidcProvider).prerequisite(
@@ -1607,7 +1677,12 @@ async function bootstrapHandoff(input) {
1607
1677
  "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
1608
1678
  ],
1609
1679
  commands: [],
1610
- prerequisite
1680
+ prerequisite,
1681
+ remediation: bootstrapRemediation(input.config, {
1682
+ configPath: input.configPath,
1683
+ summary: "The GitHub Environment immutable-subject prerequisite is not verified.",
1684
+ nextOperation: "A GitHub administrator must complete and verify the immutable-subject migration, then rerun bootstrap handoff."
1685
+ })
1611
1686
  };
1612
1687
  return {
1613
1688
  phase: "application-gateway",
@@ -1618,7 +1693,11 @@ async function bootstrapHandoff(input) {
1618
1693
  "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
1619
1694
  ],
1620
1695
  commands: [],
1621
- prerequisite
1696
+ prerequisite,
1697
+ remediation: bootstrapRemediation(input.config, {
1698
+ configPath: input.configPath,
1699
+ nextOperation: "Configure and verify the consumer gateway, then run bootstrap verify and the deployment check."
1700
+ })
1622
1701
  };
1623
1702
  }
1624
1703
  function assertEqual(actual, expected, label) {
@@ -1738,7 +1817,10 @@ async function verifyBootstrap(input) {
1738
1817
  "--body",
1739
1818
  executionRole
1740
1819
  ]
1741
- ]
1820
+ ],
1821
+ remediation: bootstrapRemediation(input.config, {
1822
+ nextOperation: "Set the reviewed GitHub Environment variables, then run bootstrap check before deployment."
1823
+ })
1742
1824
  };
1743
1825
  }
1744
1826
 
@@ -1856,7 +1938,16 @@ async function bootstrapPreflight(input) {
1856
1938
  checks.push(
1857
1939
  missing.length ? { name: "manifest", status: "missing", detail: `Missing: ${missing.join(", ")}` } : { name: "manifest", status: "ready", detail: "Manifest has all bootstrap fields." }
1858
1940
  );
1859
- if (missing.length) return { ready: false, checks };
1941
+ if (missing.length)
1942
+ return {
1943
+ ready: false,
1944
+ checks,
1945
+ remediation: bootstrapRemediation(input.config, {
1946
+ summary: "Bootstrap manifest configuration is incomplete.",
1947
+ issues: missing,
1948
+ nextOperation: "Complete the missing manifest fields, then rerun bootstrap preflight."
1949
+ })
1950
+ };
1860
1951
  const runner = input.runner;
1861
1952
  const identityRaw = await optional(runner, "aws", [
1862
1953
  "sts",
@@ -1964,17 +2055,22 @@ async function bootstrapPreflight(input) {
1964
2055
  status: subject.status === "immutable-subject-required" ? "mismatch" : subject.status,
1965
2056
  detail: subject.detail
1966
2057
  });
2058
+ const ready = !checks.some(({ status }) => status === "missing" || status === "mismatch");
1967
2059
  return {
1968
- ready: !checks.some(({ status }) => status === "missing" || status === "mismatch"),
1969
- checks
2060
+ ready,
2061
+ checks,
2062
+ remediation: bootstrapRemediation(input.config, {
2063
+ summary: ready ? "Bootstrap preflight is ready." : "Bootstrap preflight found configuration or identity issues.",
2064
+ issues: checks.filter(({ status }) => status === "missing" || status === "mismatch").map(({ detail }) => detail),
2065
+ nextOperation: ready ? "Run bootstrap plan, then create and review the next bootstrap change set." : "Resolve the reported configuration or identity issues, then rerun bootstrap preflight."
2066
+ })
1970
2067
  };
1971
2068
  }
1972
2069
  function quote(args) {
1973
2070
  return args.map((arg) => `'${arg.replaceAll("'", `'\\"'\\"'`)}'`).join(" ");
1974
2071
  }
1975
- async function bootstrapGuide(input) {
1976
- const handoff = await bootstrapHandoff(input);
1977
- const plan = await bootstrapPlan(input.config);
2072
+ async function bootstrapGuideReport(input) {
2073
+ const [handoff, plan] = await Promise.all([bootstrapHandoff(input), bootstrapPlan(input.config)]);
1978
2074
  const lines = [
1979
2075
  `Phase: ${handoff.phase}`,
1980
2076
  `Package version: ${plan.packageVersion}`,
@@ -2011,8 +2107,12 @@ async function bootstrapGuide(input) {
2011
2107
  );
2012
2108
  for (const command of verified.githubEnvironmentInstructions) lines.push(quote(command));
2013
2109
  }
2014
- return `${lines.join("\n")}
2015
- `;
2110
+ lines.push("", ...formatBootstrapRemediationText(plan.remediation));
2111
+ return { handoff, plan, guide: `${lines.join("\n")}
2112
+ ` };
2113
+ }
2114
+ async function bootstrapGuide(input) {
2115
+ return (await bootstrapGuideReport(input)).guide;
2016
2116
  }
2017
2117
 
2018
2118
  // packages/aws/src/index.ts
@@ -2190,9 +2290,11 @@ export {
2190
2290
  bootstrapConsistency,
2191
2291
  bootstrapContractVersion,
2192
2292
  bootstrapGuide,
2293
+ bootstrapGuideReport,
2193
2294
  bootstrapHandoff,
2194
2295
  bootstrapPlan,
2195
2296
  bootstrapPreflight,
2297
+ bootstrapRemediation,
2196
2298
  bootstrapTemplate,
2197
2299
  bootstrapTemplatePath,
2198
2300
  bootstrapTemplateRevision,
@@ -2202,6 +2304,7 @@ export {
2202
2304
  coreBootstrapOutputs,
2203
2305
  deployedBootstrapStack,
2204
2306
  formatBootstrapCheckText,
2307
+ formatBootstrapRemediationText,
2205
2308
  githubOidcProvider,
2206
2309
  mergeBootstrapParameters,
2207
2310
  packageEcs,
@@ -0,0 +1,23 @@
1
+ import type { BootstrapConfig, BootstrapKind } from './bootstrap.js';
2
+ export type BootstrapAffectedStack = {
3
+ kind: BootstrapKind;
4
+ stackName: string;
5
+ issue?: string;
6
+ };
7
+ export type BootstrapRemediation = {
8
+ failureSummary: string;
9
+ affectedStacks: BootstrapAffectedStack[];
10
+ immediateSteps: string[];
11
+ upgradeSteps: string[];
12
+ revalidateCommand: string;
13
+ safetyNote: string;
14
+ };
15
+ export type BootstrapRemediationInput = {
16
+ summary?: string;
17
+ affectedStacks?: BootstrapAffectedStack[];
18
+ configPath?: string;
19
+ nextOperation?: string;
20
+ issues?: string[];
21
+ };
22
+ export declare function bootstrapRemediation(config: BootstrapConfig, input?: BootstrapRemediationInput): BootstrapRemediation;
23
+ export declare function formatBootstrapRemediationText(remediation: BootstrapRemediation): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuous-excellence/ze-great-dashboard-aws",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "type": "module",
5
5
  "description": "AWS Lambda and CloudFormation adapter for Ze Great Dashboard.",
6
6
  "keywords": [