thinkwork-cli 0.12.1 → 0.12.3

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.
Files changed (29) hide show
  1. package/dist/cli.js +1062 -45
  2. package/dist/commands/enterprise/templates/deploy-repo/.github/workflows/deploy.yml +232 -0
  3. package/dist/commands/enterprise/templates/deploy-repo/README.md +31 -0
  4. package/dist/commands/enterprise/templates/deploy-repo/customer/branding/README.md +7 -0
  5. package/dist/commands/enterprise/templates/deploy-repo/customer/deployment.json +6 -0
  6. package/dist/commands/enterprise/templates/deploy-repo/customer/evals/README.md +10 -0
  7. package/dist/commands/enterprise/templates/deploy-repo/customer/seeds/README.md +7 -0
  8. package/dist/commands/enterprise/templates/deploy-repo/customer/skills/README.md +7 -0
  9. package/dist/commands/enterprise/templates/deploy-repo/customer/workspace-defaults/README.md +7 -0
  10. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +606 -0
  11. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +99 -0
  12. package/dist/commands/enterprise/templates/deploy-repo/terraform/backend-dev.hcl +6 -0
  13. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +101 -0
  14. package/dist/commands/enterprise/templates/deploy-repo/terraform/stages/dev.tfvars +9 -0
  15. package/dist/commands/enterprise/templates/deploy-repo/terraform/stages/prod.tfvars +9 -0
  16. package/dist/commands/enterprise/templates/deploy-repo/thinkwork.lock +17 -0
  17. package/dist/terraform/examples/greenfield/main.tf +26 -0
  18. package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -0
  19. package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +7 -7
  20. package/dist/terraform/modules/app/lambda-api/handlers.tf +78 -68
  21. package/dist/terraform/modules/app/lambda-api/outputs.tf +9 -4
  22. package/dist/terraform/modules/app/lambda-api/remote-artifacts.tf +36 -0
  23. package/dist/terraform/modules/app/lambda-api/variables.tf +7 -0
  24. package/dist/terraform/modules/app/lambda-api/workspace-events.tf +1 -1
  25. package/dist/terraform/modules/thinkwork/main.tf +3 -2
  26. package/dist/terraform/modules/thinkwork/outputs.tf +5 -0
  27. package/dist/terraform/modules/thinkwork/variables.tf +6 -0
  28. package/dist/terraform/schema.graphql +10 -40
  29. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -201,7 +201,7 @@ async function ensureWorkspace(cwd, stage) {
201
201
  }
202
202
  }
203
203
  function runTerraformRaw(cwd, args) {
204
- return new Promise((resolve4, reject) => {
204
+ return new Promise((resolve6, reject) => {
205
205
  const proc = spawn("terraform", args, {
206
206
  cwd,
207
207
  stdio: ["pipe", "pipe", "pipe"]
@@ -211,13 +211,13 @@ function runTerraformRaw(cwd, args) {
211
211
  proc.stdout.on("data", (d) => stdout += d);
212
212
  proc.stderr.on("data", (d) => stderr += d);
213
213
  proc.on("close", (code) => {
214
- if (code === 0) resolve4(stdout);
214
+ if (code === 0) resolve6(stdout);
215
215
  else reject(new Error(`terraform ${args.join(" ")} failed (exit ${code}): ${stderr}`));
216
216
  });
217
217
  });
218
218
  }
219
219
  function runTerraform(cwd, args) {
220
- return new Promise((resolve4) => {
220
+ return new Promise((resolve6) => {
221
221
  console.log(`
222
222
  \u2192 terraform ${args.join(" ")}
223
223
  `);
@@ -225,7 +225,7 @@ function runTerraform(cwd, args) {
225
225
  cwd,
226
226
  stdio: "inherit"
227
227
  });
228
- proc.on("close", (code) => resolve4(code ?? 1));
228
+ proc.on("close", (code) => resolve6(code ?? 1));
229
229
  });
230
230
  }
231
231
  async function ensureInit(cwd) {
@@ -469,10 +469,10 @@ async function confirm(message) {
469
469
  input: process.stdin,
470
470
  output: process.stdout
471
471
  });
472
- return new Promise((resolve4) => {
472
+ return new Promise((resolve6) => {
473
473
  rl.question(`${message} [y/N] `, (answer) => {
474
474
  rl.close();
475
- resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
475
+ resolve6(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
476
476
  });
477
477
  });
478
478
  }
@@ -553,7 +553,7 @@ async function runPostDeployProbe(stage) {
553
553
  );
554
554
  return;
555
555
  }
556
- await new Promise((resolve4) => {
556
+ await new Promise((resolve6) => {
557
557
  const proc = spawn2("bash", [scriptPath, "--stage", stage], {
558
558
  stdio: "inherit",
559
559
  env: process.env
@@ -564,11 +564,11 @@ async function runPostDeployProbe(stage) {
564
564
  `post-deploy probe exited ${code} \u2014 deploy not rolled back`
565
565
  );
566
566
  }
567
- resolve4();
567
+ resolve6();
568
568
  });
569
569
  proc.on("error", (err) => {
570
570
  printWarning(`post-deploy probe spawn failed: ${err.message}`);
571
- resolve4();
571
+ resolve6();
572
572
  });
573
573
  });
574
574
  }
@@ -782,11 +782,21 @@ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsS
782
782
  import chalk4 from "chalk";
783
783
 
784
784
  // src/environments.ts
785
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
785
+ import {
786
+ existsSync as existsSync4,
787
+ mkdirSync as mkdirSync2,
788
+ writeFileSync as writeFileSync2,
789
+ readFileSync as readFileSync2,
790
+ readdirSync
791
+ } from "fs";
786
792
  import { join as join2 } from "path";
787
793
  import { homedir as homedir2 } from "os";
788
794
  var THINKWORK_HOME = join2(homedir2(), ".thinkwork");
789
795
  var ENVIRONMENTS_DIR = join2(THINKWORK_HOME, "environments");
796
+ var ENTERPRISE_DEPLOYMENTS_DIR = join2(
797
+ THINKWORK_HOME,
798
+ "enterprise-deployments"
799
+ );
790
800
  function ensureDir(dir) {
791
801
  if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
792
802
  }
@@ -799,6 +809,13 @@ function saveEnvironment(config) {
799
809
  JSON.stringify(config, null, 2) + "\n"
800
810
  );
801
811
  }
812
+ function saveEnterpriseDeployment(config) {
813
+ ensureDir(ENTERPRISE_DEPLOYMENTS_DIR);
814
+ writeFileSync2(
815
+ join2(ENTERPRISE_DEPLOYMENTS_DIR, `${config.customerSlug}.json`),
816
+ JSON.stringify(config, null, 2) + "\n"
817
+ );
818
+ }
802
819
  function loadEnvironment(stage) {
803
820
  const configPath = join2(ENVIRONMENTS_DIR, stage, "config.json");
804
821
  if (!existsSync4(configPath)) return null;
@@ -1008,7 +1025,7 @@ function registerConfigCommand(program2) {
1008
1025
  import { spawn as spawn3 } from "child_process";
1009
1026
  import { resolve } from "path";
1010
1027
  function getTerraformOutput(cwd, key) {
1011
- return new Promise((resolve4, reject) => {
1028
+ return new Promise((resolve6, reject) => {
1012
1029
  const proc = spawn3("terraform", ["output", "-raw", key], {
1013
1030
  cwd,
1014
1031
  stdio: ["pipe", "pipe", "pipe"]
@@ -1016,17 +1033,17 @@ function getTerraformOutput(cwd, key) {
1016
1033
  let stdout = "";
1017
1034
  proc.stdout.on("data", (d) => stdout += d);
1018
1035
  proc.on("close", (code) => {
1019
- if (code === 0) resolve4(stdout.trim());
1036
+ if (code === 0) resolve6(stdout.trim());
1020
1037
  else reject(new Error(`terraform output ${key} failed (exit ${code})`));
1021
1038
  });
1022
1039
  });
1023
1040
  }
1024
1041
  function runScript(scriptPath, args) {
1025
- return new Promise((resolve4) => {
1042
+ return new Promise((resolve6) => {
1026
1043
  const proc = spawn3("bash", [scriptPath, ...args], {
1027
1044
  stdio: "inherit"
1028
1045
  });
1029
- proc.on("close", (code) => resolve4(code ?? 1));
1046
+ proc.on("close", (code) => resolve6(code ?? 1));
1030
1047
  });
1031
1048
  }
1032
1049
  function registerBootstrapCommand(program2) {
@@ -1266,9 +1283,9 @@ async function ensureTerraform() {
1266
1283
  }
1267
1284
  async function ensurePrerequisites() {
1268
1285
  console.log(chalk5.dim(" Checking prerequisites...\n"));
1269
- const awsOk = await ensureAwsCli();
1286
+ const awsOk2 = await ensureAwsCli();
1270
1287
  const tfOk = await ensureTerraform();
1271
- if (awsOk && tfOk) {
1288
+ if (awsOk2 && tfOk) {
1272
1289
  console.log("");
1273
1290
  return true;
1274
1291
  }
@@ -1401,7 +1418,7 @@ function buildAuthorizeUrl(cognito, redirectUri, state) {
1401
1418
  return `${cognito.domainUrl}/oauth2/authorize?${params.toString()}`;
1402
1419
  }
1403
1420
  function waitForCallbackCode(opts) {
1404
- return new Promise((resolve4, reject) => {
1421
+ return new Promise((resolve6, reject) => {
1405
1422
  const server = createServer((req, res) => handleRequest(req, res));
1406
1423
  let finished = false;
1407
1424
  const finish = (err, code) => {
@@ -1412,7 +1429,7 @@ function waitForCallbackCode(opts) {
1412
1429
  closer.closeAllConnections?.();
1413
1430
  server.close(() => {
1414
1431
  if (err) reject(err);
1415
- else resolve4(code);
1432
+ else resolve6(code);
1416
1433
  });
1417
1434
  };
1418
1435
  const timer = setTimeout(() => {
@@ -1603,10 +1620,10 @@ function escapeHtml(s) {
1603
1620
  // src/commands/login.ts
1604
1621
  function ask(prompt) {
1605
1622
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
1606
- return new Promise((resolve4) => {
1623
+ return new Promise((resolve6) => {
1607
1624
  rl.question(prompt, (answer) => {
1608
1625
  rl.close();
1609
- resolve4(answer.trim());
1626
+ resolve6(answer.trim());
1610
1627
  });
1611
1628
  });
1612
1629
  }
@@ -1744,13 +1761,66 @@ function finalizeAws(profile, mode) {
1744
1761
  ` Override per-command with --profile <other>, or unset with \`rm ~/.thinkwork/config.json\`.`
1745
1762
  )
1746
1763
  );
1747
- console.log("");
1748
- console.log(
1749
- ` ${chalk7.bold("Next:")} run ${chalk7.cyan("thinkwork login --stage <stage>")} if you also need`
1750
- );
1751
- console.log(
1752
- ` an API session (required for ${chalk7.cyan("eval")}, ${chalk7.cyan("agent")}, ${chalk7.cyan("thread")}, etc.).`
1764
+ return identity;
1765
+ }
1766
+ async function offerApiLoginChain(opts) {
1767
+ const stages = listDeployedStages(opts.region);
1768
+ const candidates = stages.filter(
1769
+ (stage) => loadStageSession(stage) === null
1753
1770
  );
1771
+ if (candidates.length === 0 || !isInteractive()) {
1772
+ console.log("");
1773
+ console.log(
1774
+ ` ${chalk7.bold("Next:")} run ${chalk7.cyan("thinkwork login --stage <stage>")} if you also need`
1775
+ );
1776
+ console.log(
1777
+ ` an API session (required for ${chalk7.cyan("eval")}, ${chalk7.cyan("agent")}, ${chalk7.cyan("thread")}, etc.).`
1778
+ );
1779
+ return;
1780
+ }
1781
+ console.log("");
1782
+ let chosen;
1783
+ if (candidates.length === 1) {
1784
+ const stage = candidates[0];
1785
+ const answer = await select2({
1786
+ message: `Also sign in to the API for stage "${stage}"? (required for eval / agent / thread)`,
1787
+ choices: [
1788
+ { name: `Yes \u2014 sign in to ${stage}`, value: stage },
1789
+ { name: "No \u2014 skip", value: null }
1790
+ ]
1791
+ });
1792
+ chosen = answer ?? null;
1793
+ } else {
1794
+ const skip = "__skip__";
1795
+ const answer = await select2({
1796
+ message: "Also sign in to an API stage now?",
1797
+ choices: [
1798
+ ...candidates.map((stage) => ({
1799
+ name: `Yes \u2014 ${stage}`,
1800
+ value: stage
1801
+ })),
1802
+ new Separator(),
1803
+ { name: "Skip", value: skip }
1804
+ ]
1805
+ });
1806
+ chosen = answer === skip ? null : answer;
1807
+ }
1808
+ if (!chosen) {
1809
+ console.log("");
1810
+ console.log(
1811
+ chalk7.dim(
1812
+ ` Skipped. Run ${chalk7.cyan("thinkwork login --stage <stage>")} later for an API session.`
1813
+ )
1814
+ );
1815
+ return;
1816
+ }
1817
+ console.log("");
1818
+ await doCognitoLogin({
1819
+ stage: chosen,
1820
+ region: opts.region,
1821
+ port: opts.port,
1822
+ noBrowser: opts.noBrowser
1823
+ });
1754
1824
  }
1755
1825
  async function bootstrapUserAndTenant(stage, region, idToken) {
1756
1826
  const baseUrl = getApiEndpoint(stage, region);
@@ -1879,7 +1949,7 @@ async function doApiKeyLogin(opts) {
1879
1949
  }
1880
1950
  function registerLoginCommand(program2) {
1881
1951
  program2.command("login").description(
1882
- "Sign in. Without --stage: configure AWS credentials (for deploy / destroy). With --stage <s>: sign in to that stack's Cognito / API and cache a session for API-backed commands."
1952
+ "Sign in. Without --stage: configure AWS credentials AND offer to sign in to a deployed stack's API (the natural one-step UX). With --stage <s>: go straight to that stack's Cognito / API login and cache a session for API-backed commands."
1883
1953
  ).option(
1884
1954
  "--profile <name>",
1885
1955
  'AWS profile name to configure (used when entering new keys or SSO). Defaults to "thinkwork" only on the AWS-credentials branch; the Cognito branch leaves AWS_PROFILE alone.'
@@ -1950,33 +2020,41 @@ Registered callback URL:
1950
2020
  });
1951
2021
  return;
1952
2022
  }
1953
- const port = Number.parseInt(opts.port, 10);
1954
- if (!Number.isFinite(port) || port < 1 || port > 65535) {
2023
+ const port2 = Number.parseInt(opts.port, 10);
2024
+ if (!Number.isFinite(port2) || port2 < 1 || port2 > 65535) {
1955
2025
  printError(`Invalid --port value: "${opts.port}".`);
1956
2026
  process.exit(1);
1957
2027
  }
1958
2028
  await doCognitoLogin({
1959
2029
  stage: opts.stage,
1960
2030
  region: opts.region,
1961
- port,
2031
+ port: port2,
1962
2032
  noBrowser: opts.browser === false
1963
2033
  });
1964
2034
  return;
1965
2035
  }
1966
2036
  const targetProfile = opts.profile ?? "thinkwork";
1967
2037
  printHeader("login", targetProfile);
1968
- const awsOk = await ensureAwsCli();
1969
- if (!awsOk) process.exit(1);
2038
+ const awsOk2 = await ensureAwsCli();
2039
+ if (!awsOk2) process.exit(1);
2040
+ const port = Number.parseInt(opts.port, 10);
2041
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
2042
+ printError(`Invalid --port value: "${opts.port}".`);
2043
+ process.exit(1);
2044
+ }
2045
+ const chainOpts = { port, noBrowser: opts.browser === false };
1970
2046
  if (opts.sso) {
1971
2047
  if (!runSsoLogin(targetProfile)) process.exit(1);
1972
2048
  process.env.AWS_PROFILE = targetProfile;
1973
- finalizeAws(targetProfile, "SSO");
2049
+ const { region: region2 } = finalizeAws(targetProfile, "SSO");
2050
+ await offerApiLoginChain({ region: region2, ...chainOpts });
1974
2051
  return;
1975
2052
  }
1976
2053
  if (opts.keys) {
1977
2054
  if (!await runKeyEntry(targetProfile)) process.exit(1);
1978
2055
  process.env.AWS_PROFILE = targetProfile;
1979
- finalizeAws(targetProfile, "access keys");
2056
+ const { region: region2 } = finalizeAws(targetProfile, "access keys");
2057
+ await offerApiLoginChain({ region: region2, ...chainOpts });
1980
2058
  return;
1981
2059
  }
1982
2060
  const profiles = listAwsProfiles();
@@ -1988,7 +2066,8 @@ Registered callback URL:
1988
2066
  );
1989
2067
  if (!await runKeyEntry(targetProfile)) process.exit(1);
1990
2068
  process.env.AWS_PROFILE = targetProfile;
1991
- finalizeAws(targetProfile, "access keys");
2069
+ const { region: region2 } = finalizeAws(targetProfile, "access keys");
2070
+ await offerApiLoginChain({ region: region2, ...chainOpts });
1992
2071
  return;
1993
2072
  }
1994
2073
  const choice = await pickProfile(profiles);
@@ -2000,13 +2079,15 @@ Registered callback URL:
2000
2079
  if (choice.kind === "keys") {
2001
2080
  if (!await runKeyEntry(targetProfile)) process.exit(1);
2002
2081
  process.env.AWS_PROFILE = targetProfile;
2003
- finalizeAws(targetProfile, "access keys");
2082
+ const { region: region2 } = finalizeAws(targetProfile, "access keys");
2083
+ await offerApiLoginChain({ region: region2, ...chainOpts });
2004
2084
  return;
2005
2085
  }
2006
2086
  if (choice.kind === "sso") {
2007
2087
  if (!runSsoLogin(targetProfile)) process.exit(1);
2008
2088
  process.env.AWS_PROFILE = targetProfile;
2009
- finalizeAws(targetProfile, "SSO");
2089
+ const { region: region2 } = finalizeAws(targetProfile, "SSO");
2090
+ await offerApiLoginChain({ region: region2, ...chainOpts });
2010
2091
  return;
2011
2092
  }
2012
2093
  const picked = choice.name;
@@ -2020,7 +2101,8 @@ Registered callback URL:
2020
2101
  process.exit(1);
2021
2102
  }
2022
2103
  process.env.AWS_PROFILE = picked;
2023
- finalizeAws(picked, "existing profile");
2104
+ const { region } = finalizeAws(picked, "existing profile");
2105
+ await offerApiLoginChain({ region, ...chainOpts });
2024
2106
  }
2025
2107
  );
2026
2108
  }
@@ -2102,10 +2184,10 @@ var __dirname = dirname3(fileURLToPath2(import.meta.url));
2102
2184
  function ask2(prompt, defaultVal = "") {
2103
2185
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
2104
2186
  const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
2105
- return new Promise((resolve4) => {
2187
+ return new Promise((resolve6) => {
2106
2188
  rl.question(` ${prompt}${suffix}: `, (answer) => {
2107
2189
  rl.close();
2108
- resolve4(answer.trim() || defaultVal);
2190
+ resolve6(answer.trim() || defaultVal);
2109
2191
  });
2110
2192
  });
2111
2193
  }
@@ -3841,7 +3923,7 @@ function registerUpdateCommand(program2) {
3841
3923
  import { spawn as spawn5 } from "child_process";
3842
3924
  import { input as input2, select as select7 } from "@inquirer/prompts";
3843
3925
  function getTerraformOutput2(cwd, key) {
3844
- return new Promise((resolve4, reject) => {
3926
+ return new Promise((resolve6, reject) => {
3845
3927
  const proc = spawn5("terraform", ["output", "-raw", key], {
3846
3928
  cwd,
3847
3929
  stdio: ["pipe", "pipe", "pipe"]
@@ -3851,7 +3933,7 @@ function getTerraformOutput2(cwd, key) {
3851
3933
  proc.stdout.on("data", (d) => stdout += d);
3852
3934
  proc.stderr.on("data", (d) => stderr += d);
3853
3935
  proc.on("close", (code) => {
3854
- if (code === 0) resolve4(stdout.trim());
3936
+ if (code === 0) resolve6(stdout.trim());
3855
3937
  else
3856
3938
  reject(
3857
3939
  new Error(
@@ -3862,7 +3944,7 @@ function getTerraformOutput2(cwd, key) {
3862
3944
  });
3863
3945
  }
3864
3946
  function runAwsCognitoReset(userPoolId, username, region) {
3865
- return new Promise((resolve4) => {
3947
+ return new Promise((resolve6) => {
3866
3948
  const args = [
3867
3949
  "cognito-idp",
3868
3950
  "admin-reset-user-password",
@@ -3879,7 +3961,7 @@ function runAwsCognitoReset(userPoolId, username, region) {
3879
3961
  let stderr = "";
3880
3962
  proc.stdout.on("data", (d) => stdout += d);
3881
3963
  proc.stderr.on("data", (d) => stderr += d);
3882
- proc.on("close", (code) => resolve4({ code: code ?? 1, stdout, stderr }));
3964
+ proc.on("close", (code) => resolve6({ code: code ?? 1, stdout, stderr }));
3883
3965
  });
3884
3966
  }
3885
3967
  function requireTty2(label) {
@@ -4560,6 +4642,7 @@ var CliRoutineTenantBySlugDocument = { "kind": "Document", "definitions": [{ "ki
4560
4642
  var CliScheduledJobsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliScheduledJobs" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "agentId" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "routineId" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "triggerType" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "enabled" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Boolean" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "limit" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "scheduledJobs" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "agentId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "agentId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "routineId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "routineId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "triggerType" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "triggerType" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "enabled" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "enabled" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "limit" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "limit" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "description" } }, { "kind": "Field", "name": { "kind": "Name", "value": "triggerType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "routineId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "scheduleType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "scheduleExpression" } }, { "kind": "Field", "name": { "kind": "Name", "value": "timezone" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "lastRunAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "nextRunAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
4561
4643
  var CliScheduledJobDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliScheduledJob" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "scheduledJob" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "description" } }, { "kind": "Field", "name": { "kind": "Name", "value": "triggerType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "routineId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "prompt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "scheduleType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "scheduleExpression" } }, { "kind": "Field", "name": { "kind": "Name", "value": "timezone" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "ebScheduleName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "lastRunAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "nextRunAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updatedAt" } }] } }] } }] };
4562
4644
  var CliCreateScheduledJobDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliCreateScheduledJob" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "input" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "CreateScheduledJobInput" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "createScheduledJob" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "input" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "input" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "scheduleExpression" } }, { "kind": "Field", "name": { "kind": "Name", "value": "timezone" } }] } }] } }] };
4645
+ var CliDeleteScheduledJobDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliDeleteScheduledJob" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "deleteScheduledJob" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "ok" } }] } }] } }] };
4563
4646
  var CliSchedJobTenantBySlugDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliSchedJobTenantBySlug" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "tenantBySlug" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "slug" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }] } }] } }] };
4564
4647
  var CliSkillCatalogDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliSkillCatalog" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "skillCatalog" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "skillId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "displayName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "description" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "icon" } }, { "kind": "Field", "name": { "kind": "Name", "value": "source" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }] } }] } }] };
4565
4648
  var CliSkillTenantBySlugDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliSkillTenantBySlug" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "tenantBySlug" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "slug" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }] } }] } }] };
@@ -4735,6 +4818,7 @@ var documents = {
4735
4818
  "\n query CliScheduledJobs(\n $tenantId: ID!\n $agentId: ID\n $routineId: ID\n $triggerType: String\n $enabled: Boolean\n $limit: Int\n ) {\n scheduledJobs(\n tenantId: $tenantId\n agentId: $agentId\n routineId: $routineId\n triggerType: $triggerType\n enabled: $enabled\n limit: $limit\n ) {\n id\n name\n description\n triggerType\n agentId\n routineId\n scheduleType\n scheduleExpression\n timezone\n enabled\n lastRunAt\n nextRunAt\n createdAt\n }\n }\n": CliScheduledJobsDocument,
4736
4819
  "\n query CliScheduledJob($id: ID!) {\n scheduledJob(id: $id) {\n id\n name\n description\n triggerType\n agentId\n routineId\n prompt\n scheduleType\n scheduleExpression\n timezone\n enabled\n ebScheduleName\n lastRunAt\n nextRunAt\n createdAt\n updatedAt\n }\n }\n": CliScheduledJobDocument,
4737
4820
  "\n mutation CliCreateScheduledJob($input: CreateScheduledJobInput!) {\n createScheduledJob(input: $input) {\n id\n name\n enabled\n scheduleExpression\n timezone\n }\n }\n": CliCreateScheduledJobDocument,
4821
+ "\n mutation CliDeleteScheduledJob($id: ID!) {\n deleteScheduledJob(id: $id) {\n id\n ok\n }\n }\n": CliDeleteScheduledJobDocument,
4738
4822
  "\n query CliSchedJobTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n }\n }\n": CliSchedJobTenantBySlugDocument,
4739
4823
  "\n query CliSkillCatalog {\n skillCatalog {\n id\n skillId\n displayName\n description\n category\n icon\n source\n enabled\n }\n }\n": CliSkillCatalogDocument,
4740
4824
  "\n query CliSkillTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n }\n }\n": CliSkillTenantBySlugDocument,
@@ -9847,6 +9931,14 @@ var CreateScheduledJobDoc = graphql(`
9847
9931
  }
9848
9932
  }
9849
9933
  `);
9934
+ var DeleteScheduledJobDoc = graphql(`
9935
+ mutation CliDeleteScheduledJob($id: ID!) {
9936
+ deleteScheduledJob(id: $id) {
9937
+ id
9938
+ ok
9939
+ }
9940
+ }
9941
+ `);
9850
9942
  var SchedJobTenantBySlugDoc = graphql(`
9851
9943
  query CliSchedJobTenantBySlug($slug: String!) {
9852
9944
  tenantBySlug(slug: $slug) {
@@ -10007,6 +10099,39 @@ async function runSchedCreate(name, opts) {
10007
10099
  `Created scheduled job ${data.createScheduledJob.id} \u2014 ${data.createScheduledJob.name} (${data.createScheduledJob.scheduleExpression}, ${data.createScheduledJob.timezone}).`
10008
10100
  );
10009
10101
  }
10102
+ async function runSchedDelete(id, opts) {
10103
+ const ctx = await resolveSchedContext(opts);
10104
+ if (!opts.yes) {
10105
+ if (!isInteractive()) {
10106
+ printError(
10107
+ "Refusing to delete without --yes in non-interactive mode (CI / piped stdin)."
10108
+ );
10109
+ process.exit(1);
10110
+ }
10111
+ requireTty("confirmation");
10112
+ const answer = await promptOrExit(
10113
+ () => input16({
10114
+ message: `Delete scheduled job ${id}? Type "delete" to confirm:`
10115
+ })
10116
+ );
10117
+ if (answer.trim() !== "delete") {
10118
+ console.log(" Cancelled.");
10119
+ return;
10120
+ }
10121
+ }
10122
+ const data = await gqlMutate(ctx.client, DeleteScheduledJobDoc, { id });
10123
+ if (isJsonMode()) {
10124
+ printJson(data.deleteScheduledJob);
10125
+ return;
10126
+ }
10127
+ if (data.deleteScheduledJob.ok) {
10128
+ printSuccess(`Deleted scheduled job ${data.deleteScheduledJob.id}.`);
10129
+ } else {
10130
+ console.log(
10131
+ ` Scheduled job ${id} was already deleted (no row matched).`
10132
+ );
10133
+ }
10134
+ }
10010
10135
  function notYetImplementedAtApi(verb) {
10011
10136
  printError(
10012
10137
  `\`scheduled-job ${verb}\` is not yet implemented at the GraphQL API.
@@ -10031,7 +10156,9 @@ Examples:
10031
10156
  `
10032
10157
  ).action(runSchedCreate);
10033
10158
  job.command("update <id>").description("Update a scheduled job. (API surface pending \u2014 currently a no-op.)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--schedule <expr>").option("--timezone <tz>").option("--payload <json>").option("--enable").option("--disable").action(() => notYetImplementedAtApi("update"));
10034
- job.command("delete <id>").description("Delete a scheduled job. (API surface pending.)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplementedAtApi("delete"));
10159
+ job.command("delete <id>").description(
10160
+ "Delete a scheduled job. Deprovisions the EventBridge schedule first, then removes the row."
10161
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(runSchedDelete);
10035
10162
  job.command("run <id>").description("Trigger a scheduled job immediately. (API surface pending.)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--wait", "Block until the run completes").action(() => notYetImplementedAtApi("run"));
10036
10163
  }
10037
10164
 
@@ -14328,6 +14455,895 @@ Examples:
14328
14455
  });
14329
14456
  }
14330
14457
 
14458
+ // src/commands/enterprise/bootstrap.ts
14459
+ import { resolve as resolve5 } from "path";
14460
+
14461
+ // src/commands/enterprise/template.ts
14462
+ import {
14463
+ existsSync as existsSync10,
14464
+ mkdirSync as mkdirSync5,
14465
+ readFileSync as readFileSync8,
14466
+ readdirSync as readdirSync2,
14467
+ statSync as statSync2,
14468
+ writeFileSync as writeFileSync5
14469
+ } from "fs";
14470
+ import { dirname as dirname4, join as join7, relative as relative2, resolve as resolve4 } from "path";
14471
+ import { fileURLToPath as fileURLToPath3 } from "url";
14472
+ var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
14473
+ var MANAGED_MARKER = "thinkwork-managed: enterprise-deploy-template";
14474
+ var CUSTOMER_SLUG_PATTERN = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
14475
+ function renderEnterpriseDeployRepoTemplate(options) {
14476
+ const customerSlug = validateCustomerSlug(options.customerSlug);
14477
+ const stages = validateStages(options.stages ?? ["dev", "prod"]);
14478
+ const targetDir = resolve4(options.targetDir);
14479
+ const templateRoot = findEnterpriseTemplateRoot();
14480
+ const replacements = buildTemplateReplacements({
14481
+ ...options,
14482
+ customerSlug,
14483
+ stages
14484
+ });
14485
+ const templateFiles = listTemplateFiles(templateRoot).filter(
14486
+ (path2) => !relative2(templateRoot, path2).startsWith("terraform/stages/") && !/^terraform\/backend-[^.]+\.hcl$/.test(
14487
+ relative2(templateRoot, path2).split("\\").join("/")
14488
+ )
14489
+ );
14490
+ const written = [];
14491
+ const preserved = [];
14492
+ for (const templatePath of templateFiles) {
14493
+ const relativePath = relative2(templateRoot, templatePath);
14494
+ const outputPath = join7(targetDir, relativePath);
14495
+ let content = applyTemplate(
14496
+ readFileSync8(templatePath, "utf8"),
14497
+ replacements
14498
+ );
14499
+ if (relativePath.split("\\").join("/") === "customer/deployment.json") {
14500
+ content = renderCustomerDeploymentJson(content, customerSlug, stages);
14501
+ }
14502
+ writeManagedFile(outputPath, content, written, preserved);
14503
+ }
14504
+ const stageTemplateRoot = join7(templateRoot, "terraform", "stages");
14505
+ const backendTemplatePath = join7(
14506
+ templateRoot,
14507
+ "terraform",
14508
+ "backend-dev.hcl"
14509
+ );
14510
+ for (const stage of stages) {
14511
+ const explicitTemplate = join7(stageTemplateRoot, `${stage}.tfvars`);
14512
+ const fallbackTemplate = join7(stageTemplateRoot, "dev.tfvars");
14513
+ const templatePath = existsSync10(explicitTemplate) ? explicitTemplate : fallbackTemplate;
14514
+ const outputPath = join7(
14515
+ targetDir,
14516
+ "terraform",
14517
+ "stages",
14518
+ `${stage}.tfvars`
14519
+ );
14520
+ const content = applyTemplate(readFileSync8(templatePath, "utf8"), {
14521
+ ...replacements,
14522
+ STAGE: stage
14523
+ });
14524
+ writeManagedFile(outputPath, content, written, preserved);
14525
+ const backendPath = join7(targetDir, "terraform", `backend-${stage}.hcl`);
14526
+ const backendContent = applyTemplate(
14527
+ readFileSync8(backendTemplatePath, "utf8"),
14528
+ {
14529
+ ...replacements,
14530
+ STAGE: stage
14531
+ }
14532
+ );
14533
+ writeManagedFile(backendPath, backendContent, written, preserved);
14534
+ }
14535
+ return { targetDir, written, preserved };
14536
+ }
14537
+ function validateCustomerSlug(slug) {
14538
+ const normalized = slug.trim();
14539
+ if (!CUSTOMER_SLUG_PATTERN.test(normalized)) {
14540
+ throw new Error(
14541
+ `Invalid customer slug "${slug}". Must be lowercase alphanumeric + hyphens, 3-40 characters, starting with a letter.`
14542
+ );
14543
+ }
14544
+ return normalized;
14545
+ }
14546
+ function validateStages(stages) {
14547
+ const normalized = [
14548
+ ...new Set(stages.map((stage) => stage.trim()).filter(Boolean))
14549
+ ];
14550
+ if (normalized.length === 0) {
14551
+ throw new Error("At least one deployment stage is required.");
14552
+ }
14553
+ for (const stage of normalized) {
14554
+ const check = validateStage(stage);
14555
+ if (!check.valid) {
14556
+ throw new Error(check.error ?? `Invalid stage name "${stage}".`);
14557
+ }
14558
+ }
14559
+ return normalized;
14560
+ }
14561
+ function findEnterpriseTemplateRoot() {
14562
+ const candidates = [
14563
+ resolve4(__dirname2, "templates/deploy-repo"),
14564
+ resolve4(__dirname2, "commands/enterprise/templates/deploy-repo")
14565
+ ];
14566
+ for (const candidate of candidates) {
14567
+ if (existsSync10(join7(candidate, "thinkwork.lock"))) return candidate;
14568
+ }
14569
+ throw new Error(
14570
+ "Enterprise deployment repo template not found. The CLI package may be incomplete."
14571
+ );
14572
+ }
14573
+ function buildTemplateReplacements(options) {
14574
+ const releaseVersion = options.releaseVersion ?? "v0.0.0";
14575
+ const artifactBucket = options.artifactBucket ?? `${options.customerSlug}-thinkwork-release-artifacts`;
14576
+ return {
14577
+ ACCOUNT_ID: options.accountId ?? "123456789012",
14578
+ ARTIFACT_BUCKET: artifactBucket,
14579
+ CUSTOMER_SLUG: options.customerSlug,
14580
+ LAMBDA_ARTIFACT_PREFIX: `releases/${releaseVersion}/lambdas`,
14581
+ REGION: options.region ?? "us-east-1",
14582
+ RELEASE_MANIFEST_SHA256: options.releaseManifestSha256 ?? "CHANGE_ME",
14583
+ RELEASE_MANIFEST_URL: options.releaseManifestUrl ?? `https://github.com/thinkwork-ai/thinkwork/releases/download/${releaseVersion}/thinkwork-release.json`,
14584
+ RELEASE_VERSION: releaseVersion,
14585
+ TERRAFORM_MODULE_VERSION: options.terraformModuleVersion ?? releaseVersion.replace(/^v/, "")
14586
+ };
14587
+ }
14588
+ function renderCustomerDeploymentJson(content, customerSlug, stages) {
14589
+ const deployment = JSON.parse(content);
14590
+ deployment.stages = Object.fromEntries(
14591
+ stages.map((stage) => [
14592
+ stage,
14593
+ {
14594
+ tenantSlug: stage === "prod" ? customerSlug : `${customerSlug}-${stage}`,
14595
+ evalPacks: [],
14596
+ seedPacks: [],
14597
+ skillPacks: [],
14598
+ workspaceDefaultPacks: [],
14599
+ branding: null
14600
+ }
14601
+ ])
14602
+ );
14603
+ return `${JSON.stringify(deployment, null, 2)}
14604
+ `;
14605
+ }
14606
+ function listTemplateFiles(root) {
14607
+ const out = [];
14608
+ for (const entry of readdirSync2(root)) {
14609
+ const path2 = join7(root, entry);
14610
+ const stat = statSync2(path2);
14611
+ if (stat.isDirectory()) {
14612
+ out.push(...listTemplateFiles(path2));
14613
+ } else if (stat.isFile()) {
14614
+ out.push(path2);
14615
+ }
14616
+ }
14617
+ return out.sort();
14618
+ }
14619
+ function applyTemplate(source, replacements) {
14620
+ return source.replaceAll(/\{\{([A-Z0-9_]+)\}\}/g, (_match, key) => {
14621
+ if (!(key in replacements)) {
14622
+ throw new Error(`Unknown enterprise deployment template token: ${key}`);
14623
+ }
14624
+ return replacements[key];
14625
+ });
14626
+ }
14627
+ function writeManagedFile(path2, content, written, preserved) {
14628
+ mkdirSync5(dirname4(path2), { recursive: true });
14629
+ if (existsSync10(path2)) {
14630
+ const current = readFileSync8(path2, "utf8");
14631
+ if (!current.includes(MANAGED_MARKER)) {
14632
+ preserved.push(path2);
14633
+ return;
14634
+ }
14635
+ }
14636
+ writeFileSync5(path2, content);
14637
+ written.push(path2);
14638
+ }
14639
+
14640
+ // src/commands/enterprise/aws-bootstrap.ts
14641
+ import { execFileSync } from "child_process";
14642
+ import { mkdtempSync, writeFileSync as writeFileSync6 } from "fs";
14643
+ import { tmpdir } from "os";
14644
+ import { join as join8 } from "path";
14645
+ function buildEnterpriseAwsBootstrapPlan(config) {
14646
+ const oidcProviderArn = `arn:aws:iam::${config.accountId}:oidc-provider/token.actions.githubusercontent.com`;
14647
+ return {
14648
+ stateBucket: config.stateBucket,
14649
+ lockTable: config.lockTable,
14650
+ artifactBucket: config.artifactBucket,
14651
+ oidcProviderArn,
14652
+ stageRoles: config.stages.map((stage) => {
14653
+ const roleName = `thinkwork-${config.customerSlug}-${stage}-deploy`;
14654
+ return {
14655
+ stage,
14656
+ roleName,
14657
+ roleArn: `arn:aws:iam::${config.accountId}:role/${roleName}`,
14658
+ trustPolicy: buildGitHubOidcTrustPolicy({
14659
+ oidcProviderArn,
14660
+ repository: config.repository,
14661
+ stage
14662
+ }),
14663
+ deployPolicyName: `thinkwork-${config.customerSlug}-${stage}-deploy`,
14664
+ deployPolicy: buildEnterpriseDeployRolePolicy({
14665
+ accountId: config.accountId,
14666
+ region: config.region,
14667
+ stage,
14668
+ stateBucket: config.stateBucket,
14669
+ lockTable: config.lockTable,
14670
+ artifactBucket: config.artifactBucket
14671
+ })
14672
+ };
14673
+ })
14674
+ };
14675
+ }
14676
+ function buildGitHubOidcTrustPolicy(options) {
14677
+ return {
14678
+ Version: "2012-10-17",
14679
+ Statement: [
14680
+ {
14681
+ Effect: "Allow",
14682
+ Principal: {
14683
+ Federated: options.oidcProviderArn
14684
+ },
14685
+ Action: "sts:AssumeRoleWithWebIdentity",
14686
+ Condition: {
14687
+ StringEquals: {
14688
+ "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
14689
+ "token.actions.githubusercontent.com:sub": `repo:${options.repository}:environment:${options.stage}`
14690
+ }
14691
+ }
14692
+ }
14693
+ ]
14694
+ };
14695
+ }
14696
+ function buildEnterpriseDeployRolePolicy(options) {
14697
+ const thinkworkPrefix = `thinkwork-${options.stage}`;
14698
+ return {
14699
+ Version: "2012-10-17",
14700
+ Statement: [
14701
+ {
14702
+ Sid: "TerraformStateAndReleaseBuckets",
14703
+ Effect: "Allow",
14704
+ Action: [
14705
+ "s3:GetBucketLocation",
14706
+ "s3:GetBucketVersioning",
14707
+ "s3:GetEncryptionConfiguration",
14708
+ "s3:ListBucket",
14709
+ "s3:PutBucketVersioning",
14710
+ "s3:PutEncryptionConfiguration",
14711
+ "s3:PutLifecycleConfiguration",
14712
+ "s3:PutPublicAccessBlock"
14713
+ ],
14714
+ Resource: [
14715
+ `arn:aws:s3:::${options.stateBucket}`,
14716
+ `arn:aws:s3:::${options.artifactBucket}`,
14717
+ `arn:aws:s3:::${thinkworkPrefix}-*`
14718
+ ]
14719
+ },
14720
+ {
14721
+ Sid: "TerraformStateAndReleaseObjects",
14722
+ Effect: "Allow",
14723
+ Action: [
14724
+ "s3:AbortMultipartUpload",
14725
+ "s3:DeleteObject",
14726
+ "s3:GetObject",
14727
+ "s3:PutObject"
14728
+ ],
14729
+ Resource: [
14730
+ `arn:aws:s3:::${options.stateBucket}/*`,
14731
+ `arn:aws:s3:::${options.artifactBucket}/*`,
14732
+ `arn:aws:s3:::${thinkworkPrefix}-*/*`
14733
+ ]
14734
+ },
14735
+ {
14736
+ Sid: "TerraformStateLocks",
14737
+ Effect: "Allow",
14738
+ Action: [
14739
+ "dynamodb:CreateTable",
14740
+ "dynamodb:DescribeTable",
14741
+ "dynamodb:GetItem",
14742
+ "dynamodb:PutItem",
14743
+ "dynamodb:DeleteItem",
14744
+ "dynamodb:UpdateItem"
14745
+ ],
14746
+ Resource: `arn:aws:dynamodb:${options.region}:${options.accountId}:table/${options.lockTable}`
14747
+ },
14748
+ {
14749
+ Sid: "ThinkWorkNamedResources",
14750
+ Effect: "Allow",
14751
+ Action: [
14752
+ "apigateway:*",
14753
+ "appsync:*",
14754
+ "bedrock:*",
14755
+ "bedrock-agentcore:*",
14756
+ "cloudfront:*",
14757
+ "cognito-idp:*",
14758
+ "dynamodb:*",
14759
+ "ec2:*",
14760
+ "ecr:*",
14761
+ "ecs:*",
14762
+ "elasticfilesystem:*",
14763
+ "elasticloadbalancing:*",
14764
+ "events:*",
14765
+ "iam:*",
14766
+ "lambda:*",
14767
+ "logs:*",
14768
+ "rds:*",
14769
+ "scheduler:*",
14770
+ "secretsmanager:*",
14771
+ "ses:*",
14772
+ "sqs:*",
14773
+ "ssm:*",
14774
+ "states:*",
14775
+ "xray:*"
14776
+ ],
14777
+ Resource: "*"
14778
+ }
14779
+ ]
14780
+ };
14781
+ }
14782
+ var AwsCliEnterpriseBootstrapClient = class {
14783
+ async ensureStateBucket(bucket, region) {
14784
+ return ensureBucket(bucket, region, "terraform state bucket");
14785
+ }
14786
+ async ensureLockTable(table, region) {
14787
+ if (awsOk([
14788
+ "dynamodb",
14789
+ "describe-table",
14790
+ "--table-name",
14791
+ table,
14792
+ "--region",
14793
+ region
14794
+ ])) {
14795
+ return {
14796
+ target: table,
14797
+ status: "reused",
14798
+ message: `DynamoDB lock table ${table} already exists.`
14799
+ };
14800
+ }
14801
+ execFileSync("aws", [
14802
+ "dynamodb",
14803
+ "create-table",
14804
+ "--table-name",
14805
+ table,
14806
+ "--attribute-definitions",
14807
+ "AttributeName=LockID,AttributeType=S",
14808
+ "--key-schema",
14809
+ "AttributeName=LockID,KeyType=HASH",
14810
+ "--billing-mode",
14811
+ "PAY_PER_REQUEST",
14812
+ "--region",
14813
+ region
14814
+ ]);
14815
+ return {
14816
+ target: table,
14817
+ status: "created",
14818
+ message: `Created DynamoDB lock table ${table}.`
14819
+ };
14820
+ }
14821
+ async ensureArtifactBucket(bucket, region) {
14822
+ return ensureBucket(bucket, region, "release artifact bucket");
14823
+ }
14824
+ async ensureOidcProvider(accountId) {
14825
+ const arn = `arn:aws:iam::${accountId}:oidc-provider/token.actions.githubusercontent.com`;
14826
+ if (awsOk([
14827
+ "iam",
14828
+ "get-open-id-connect-provider",
14829
+ "--open-id-connect-provider-arn",
14830
+ arn
14831
+ ])) {
14832
+ return {
14833
+ target: arn,
14834
+ status: "reused",
14835
+ message: "GitHub Actions OIDC provider already exists."
14836
+ };
14837
+ }
14838
+ execFileSync("aws", [
14839
+ "iam",
14840
+ "create-open-id-connect-provider",
14841
+ "--url",
14842
+ "https://token.actions.githubusercontent.com",
14843
+ "--client-id-list",
14844
+ "sts.amazonaws.com",
14845
+ "--thumbprint-list",
14846
+ "6938fd4d98bab03faadb97b34396831e3780aea1"
14847
+ ]);
14848
+ return {
14849
+ target: arn,
14850
+ status: "created",
14851
+ message: "Created GitHub Actions OIDC provider."
14852
+ };
14853
+ }
14854
+ async ensureDeployRole(role) {
14855
+ if (awsOk(["iam", "get-role", "--role-name", role.roleName])) {
14856
+ putRolePolicy(role);
14857
+ return {
14858
+ target: role.roleArn,
14859
+ status: "updated",
14860
+ message: `Deploy role ${role.roleName} already exists; updated inline deploy policy ${role.deployPolicyName}.`
14861
+ };
14862
+ }
14863
+ const dir = mkdtempSync(join8(tmpdir(), "thinkwork-enterprise-role-"));
14864
+ const trustPath = join8(dir, "trust.json");
14865
+ writeFileSync6(trustPath, JSON.stringify(role.trustPolicy));
14866
+ execFileSync("aws", [
14867
+ "iam",
14868
+ "create-role",
14869
+ "--role-name",
14870
+ role.roleName,
14871
+ "--assume-role-policy-document",
14872
+ `file://${trustPath}`
14873
+ ]);
14874
+ putRolePolicy(role);
14875
+ return {
14876
+ target: role.roleArn,
14877
+ status: "created",
14878
+ message: `Created deploy role ${role.roleName} and attached inline deploy policy ${role.deployPolicyName}.`
14879
+ };
14880
+ }
14881
+ };
14882
+ function putRolePolicy(role) {
14883
+ const dir = mkdtempSync(join8(tmpdir(), "thinkwork-enterprise-policy-"));
14884
+ const policyPath = join8(dir, "policy.json");
14885
+ writeFileSync6(policyPath, JSON.stringify(role.deployPolicy));
14886
+ execFileSync("aws", [
14887
+ "iam",
14888
+ "put-role-policy",
14889
+ "--role-name",
14890
+ role.roleName,
14891
+ "--policy-name",
14892
+ role.deployPolicyName,
14893
+ "--policy-document",
14894
+ `file://${policyPath}`
14895
+ ]);
14896
+ }
14897
+ function ensureBucket(bucket, region, label) {
14898
+ if (awsOk(["s3api", "head-bucket", "--bucket", bucket])) {
14899
+ return {
14900
+ target: bucket,
14901
+ status: "reused",
14902
+ message: `${label} ${bucket} already exists.`
14903
+ };
14904
+ }
14905
+ const args = region === "us-east-1" ? ["s3api", "create-bucket", "--bucket", bucket, "--region", region] : [
14906
+ "s3api",
14907
+ "create-bucket",
14908
+ "--bucket",
14909
+ bucket,
14910
+ "--region",
14911
+ region,
14912
+ "--create-bucket-configuration",
14913
+ `LocationConstraint=${region}`
14914
+ ];
14915
+ execFileSync("aws", args);
14916
+ execFileSync("aws", [
14917
+ "s3api",
14918
+ "put-public-access-block",
14919
+ "--bucket",
14920
+ bucket,
14921
+ "--public-access-block-configuration",
14922
+ "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
14923
+ ]);
14924
+ return {
14925
+ target: bucket,
14926
+ status: "created",
14927
+ message: `Created ${label} ${bucket}.`
14928
+ };
14929
+ }
14930
+ function awsOk(args) {
14931
+ try {
14932
+ execFileSync("aws", args, { stdio: "ignore" });
14933
+ return true;
14934
+ } catch {
14935
+ return false;
14936
+ }
14937
+ }
14938
+
14939
+ // src/commands/enterprise/github.ts
14940
+ import { execFileSync as execFileSync2 } from "child_process";
14941
+ function parseGitHubRepository(input20) {
14942
+ const trimmed = input20.trim();
14943
+ const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(trimmed);
14944
+ if (!match) {
14945
+ throw new Error(
14946
+ `Invalid GitHub repository "${input20}". Use owner/name, for example acme/thinkwork-deploy.`
14947
+ );
14948
+ }
14949
+ return {
14950
+ owner: match[1],
14951
+ name: match[2],
14952
+ fullName: `${match[1]}/${match[2]}`
14953
+ };
14954
+ }
14955
+ function buildEnterpriseGitHubBootstrapPlan(options) {
14956
+ const repository = parseGitHubRepository(options.repository);
14957
+ return {
14958
+ repository,
14959
+ dispatchWorkflow: options.dispatchWorkflow ?? false,
14960
+ environments: options.stages.map((stage) => {
14961
+ const role = options.stageRoles.find((item) => item.stage === stage);
14962
+ if (!role) {
14963
+ throw new Error(`Missing deploy role for stage "${stage}".`);
14964
+ }
14965
+ return {
14966
+ stage,
14967
+ roleArn: role.roleArn,
14968
+ vars: {
14969
+ AWS_REGION: options.region,
14970
+ AWS_ROLE_ARN: role.roleArn,
14971
+ THINKWORK_ARTIFACT_BUCKET: options.artifactBucket
14972
+ },
14973
+ secretPlaceholders: ["TF_VAR_DB_PASSWORD", "TF_VAR_API_AUTH_SECRET"]
14974
+ };
14975
+ })
14976
+ };
14977
+ }
14978
+ var GhCliEnterpriseBootstrapClient = class {
14979
+ constructor(repository) {
14980
+ this.repository = repository;
14981
+ }
14982
+ repository;
14983
+ async ensureEnvironment(environment) {
14984
+ gh([
14985
+ "api",
14986
+ "--method",
14987
+ "PUT",
14988
+ `repos/${this.repository.fullName}/environments/${environment.stage}`,
14989
+ "--field",
14990
+ "wait_timer=0"
14991
+ ]);
14992
+ return {
14993
+ target: `${this.repository.fullName}:${environment.stage}`,
14994
+ status: "updated",
14995
+ message: `Ensured GitHub Environment ${environment.stage}.`
14996
+ };
14997
+ }
14998
+ async upsertEnvironmentVariables(environment) {
14999
+ for (const [name, value] of Object.entries(environment.vars)) {
15000
+ gh([
15001
+ "variable",
15002
+ "set",
15003
+ name,
15004
+ "--repo",
15005
+ this.repository.fullName,
15006
+ "--env",
15007
+ environment.stage,
15008
+ "--body",
15009
+ value
15010
+ ]);
15011
+ }
15012
+ return {
15013
+ target: `${this.repository.fullName}:${environment.stage}:vars`,
15014
+ status: "updated",
15015
+ message: `Updated non-secret GitHub Environment variables for ${environment.stage}.`
15016
+ };
15017
+ }
15018
+ async writeRepositoryFiles(targetDir) {
15019
+ return {
15020
+ target: targetDir,
15021
+ status: "updated",
15022
+ message: "Repository files were written locally. Commit/push this directory or run from a checked-out deployment repo."
15023
+ };
15024
+ }
15025
+ async dispatchWorkflow(stage) {
15026
+ gh([
15027
+ "workflow",
15028
+ "run",
15029
+ "deploy.yml",
15030
+ "--repo",
15031
+ this.repository.fullName,
15032
+ "--field",
15033
+ `stage=${stage}`
15034
+ ]);
15035
+ return {
15036
+ target: `${this.repository.fullName}:deploy.yml:${stage}`,
15037
+ status: "created",
15038
+ message: `Dispatched deploy workflow for ${stage}.`
15039
+ };
15040
+ }
15041
+ };
15042
+ function gh(args) {
15043
+ return execFileSync2("gh", args, { encoding: "utf8" });
15044
+ }
15045
+
15046
+ // src/commands/enterprise/release.ts
15047
+ function resolveEnterpriseReleasePin(options) {
15048
+ const version = options.releaseVersion ?? `v${VERSION}`;
15049
+ return {
15050
+ version,
15051
+ manifestUrl: options.manifestUrl ?? `https://github.com/thinkwork-ai/thinkwork/releases/download/${version}/thinkwork-release.json`,
15052
+ manifestSha256: options.manifestSha256 ?? "CHANGE_ME",
15053
+ terraformModuleVersion: options.terraformModuleVersion ?? version.replace(/^v/, "")
15054
+ };
15055
+ }
15056
+
15057
+ // src/commands/enterprise/bootstrap.ts
15058
+ function buildEnterpriseBootstrapPlan(options, identity) {
15059
+ const customerSlug = validateCustomerSlug(options.customerSlug);
15060
+ const repository = parseGitHubRepository(options.repository).fullName;
15061
+ const stages = validateStages(options.stages ?? ["dev", "prod"]);
15062
+ const accountId = options.accountId ?? identity?.account;
15063
+ const region = options.region ?? identity?.region;
15064
+ if (!accountId) {
15065
+ throw new Error(
15066
+ "AWS account ID is required. Configure AWS credentials or pass --account-id."
15067
+ );
15068
+ }
15069
+ if (!region || region === "unknown") {
15070
+ throw new Error(
15071
+ "AWS region is required. Configure AWS_REGION or pass --region."
15072
+ );
15073
+ }
15074
+ const release = resolveEnterpriseReleasePin({
15075
+ releaseVersion: options.releaseVersion,
15076
+ manifestUrl: options.manifestUrl,
15077
+ manifestSha256: options.manifestSha256,
15078
+ terraformModuleVersion: options.terraformModuleVersion
15079
+ });
15080
+ const artifactBucket = options.artifactBucket ?? `${customerSlug}-thinkwork-release-artifacts`;
15081
+ const stateBucket = options.stateBucket ?? `${customerSlug}-thinkwork-terraform-state`;
15082
+ const lockTable = options.lockTable ?? `${customerSlug}-thinkwork-terraform-locks`;
15083
+ const aws = buildEnterpriseAwsBootstrapPlan({
15084
+ accountId,
15085
+ region,
15086
+ repository,
15087
+ stages,
15088
+ customerSlug,
15089
+ stateBucket,
15090
+ lockTable,
15091
+ artifactBucket
15092
+ });
15093
+ const github = buildEnterpriseGitHubBootstrapPlan({
15094
+ repository,
15095
+ stages,
15096
+ region,
15097
+ artifactBucket,
15098
+ stageRoles: aws.stageRoles,
15099
+ dispatchWorkflow: options.dispatchWorkflow
15100
+ });
15101
+ return {
15102
+ customerSlug,
15103
+ targetDir: resolve5(options.targetDir),
15104
+ repository,
15105
+ stages,
15106
+ accountId,
15107
+ region,
15108
+ release,
15109
+ aws,
15110
+ github
15111
+ };
15112
+ }
15113
+ async function runEnterpriseBootstrap(options, deps = {}) {
15114
+ const identity = deps.identity === void 0 ? getAwsIdentity() : deps.identity;
15115
+ if (!options.dryRun && !identity && (!options.accountId || !options.region)) {
15116
+ throw new Error(
15117
+ "AWS identity is required before mutating AWS or GitHub resources."
15118
+ );
15119
+ }
15120
+ const plan = buildEnterpriseBootstrapPlan(options, identity);
15121
+ const template = renderEnterpriseDeployRepoTemplate({
15122
+ targetDir: plan.targetDir,
15123
+ customerSlug: plan.customerSlug,
15124
+ stages: plan.stages,
15125
+ region: plan.region,
15126
+ accountId: plan.accountId,
15127
+ releaseVersion: plan.release.version,
15128
+ releaseManifestUrl: plan.release.manifestUrl,
15129
+ releaseManifestSha256: plan.release.manifestSha256,
15130
+ terraformModuleVersion: plan.release.terraformModuleVersion,
15131
+ artifactBucket: plan.aws.artifactBucket
15132
+ });
15133
+ const awsClient = deps.awsClient ?? new AwsCliEnterpriseBootstrapClient();
15134
+ const githubClient = deps.githubClient ?? new GhCliEnterpriseBootstrapClient(parseGitHubRepository(plan.repository));
15135
+ const awsResults = [];
15136
+ const githubResults = [];
15137
+ if (options.dryRun) {
15138
+ awsResults.push(
15139
+ planned(plan.aws.stateBucket, "Would ensure Terraform state bucket."),
15140
+ planned(plan.aws.lockTable, "Would ensure Terraform lock table."),
15141
+ planned(plan.aws.artifactBucket, "Would ensure release artifact bucket."),
15142
+ planned(
15143
+ plan.aws.oidcProviderArn,
15144
+ "Would ensure GitHub Actions OIDC provider."
15145
+ ),
15146
+ ...plan.aws.stageRoles.map(
15147
+ (role) => planned(role.roleArn, `Would ensure deploy role for ${role.stage}.`)
15148
+ )
15149
+ );
15150
+ githubResults.push(
15151
+ planned(plan.targetDir, "Would write deployment repository files."),
15152
+ ...plan.github.environments.flatMap((environment) => [
15153
+ planned(
15154
+ `${plan.repository}:${environment.stage}`,
15155
+ `Would ensure GitHub Environment ${environment.stage}.`
15156
+ ),
15157
+ planned(
15158
+ `${plan.repository}:${environment.stage}:vars`,
15159
+ `Would upsert non-secret GitHub variables for ${environment.stage}.`
15160
+ ),
15161
+ planned(
15162
+ `${plan.repository}:${environment.stage}:secrets`,
15163
+ `Would require GitHub Environment secrets for ${environment.stage}: ${environment.secretPlaceholders.join(", ")}.`
15164
+ ),
15165
+ ...plan.github.dispatchWorkflow ? [
15166
+ planned(
15167
+ `${plan.repository}:deploy.yml:${environment.stage}`,
15168
+ `Would dispatch deploy workflow for ${environment.stage}.`
15169
+ )
15170
+ ] : []
15171
+ ])
15172
+ );
15173
+ } else {
15174
+ awsResults.push(
15175
+ await awsClient.ensureStateBucket(plan.aws.stateBucket, plan.region),
15176
+ await awsClient.ensureLockTable(plan.aws.lockTable, plan.region),
15177
+ await awsClient.ensureArtifactBucket(
15178
+ plan.aws.artifactBucket,
15179
+ plan.region
15180
+ ),
15181
+ await awsClient.ensureOidcProvider(plan.accountId)
15182
+ );
15183
+ for (const role of plan.aws.stageRoles) {
15184
+ awsResults.push(await awsClient.ensureDeployRole(role));
15185
+ }
15186
+ githubResults.push(await githubClient.writeRepositoryFiles(plan.targetDir));
15187
+ for (const environment of plan.github.environments) {
15188
+ githubResults.push(
15189
+ await githubClient.ensureEnvironment(environment),
15190
+ await githubClient.upsertEnvironmentVariables(environment),
15191
+ secretPlaceholderResult(plan.repository, environment)
15192
+ );
15193
+ if (plan.github.dispatchWorkflow) {
15194
+ githubResults.push(
15195
+ await githubClient.dispatchWorkflow(environment.stage)
15196
+ );
15197
+ }
15198
+ }
15199
+ }
15200
+ const metadata = options.dryRun ? planned(
15201
+ plan.customerSlug,
15202
+ "Would record local enterprise deployment metadata without secrets."
15203
+ ) : recordDeploymentMetadata(plan, deps.saveDeployment);
15204
+ return {
15205
+ plan,
15206
+ template,
15207
+ aws: awsResults,
15208
+ github: githubResults,
15209
+ metadata
15210
+ };
15211
+ }
15212
+ function registerEnterpriseBootstrapCommand(program2) {
15213
+ program2.command("bootstrap [targetDir]").description(
15214
+ "Bootstrap a customer-owned ThinkWork deployment repository and CI trust bridge."
15215
+ ).option("--customer <slug>", "Customer slug, e.g. acme").option("--repo <owner/name>", "Customer GitHub deployment repository").option("--stage <stage...>", "Deployment stage(s)", ["dev", "prod"]).option("--region <region>", "AWS region").option("--account-id <id>", "AWS account ID").option("--release-version <version>", "Pinned ThinkWork release version").option("--manifest-url <url>", "Pinned ThinkWork release manifest URL").option(
15216
+ "--manifest-sha256 <sha256>",
15217
+ "Pinned ThinkWork release manifest SHA-256"
15218
+ ).option(
15219
+ "--terraform-module-version <version>",
15220
+ "Pinned Terraform Registry module version"
15221
+ ).option(
15222
+ "--artifact-bucket <bucket>",
15223
+ "Customer-owned release artifact bucket"
15224
+ ).option("--state-bucket <bucket>", "Terraform state bucket").option("--lock-table <table>", "Terraform state lock table").option("--dispatch", "Dispatch deploy workflow after bootstrap").option(
15225
+ "--dry-run",
15226
+ "Render files and print the plan without mutating AWS/GitHub"
15227
+ ).option("-y, --yes", "Skip confirmation for mutating bootstrap").action(
15228
+ async (targetDir, opts) => {
15229
+ try {
15230
+ const customerSlug = await resolveCustomerSlug(opts.customer);
15231
+ const repository = await resolveRepository(opts.repo);
15232
+ const identity = getAwsIdentity();
15233
+ printHeader("enterprise bootstrap", customerSlug, identity);
15234
+ if (!opts.dryRun && !opts.yes) {
15235
+ const prodStages = (opts.stage ?? ["dev", "prod"]).filter(
15236
+ isProdLike
15237
+ );
15238
+ const message = prodStages.length > 0 ? ` Bootstrap deployment authority for ${repository} including production-like stage(s): ${prodStages.join(", ")}?` : ` Bootstrap deployment authority for ${repository}?`;
15239
+ if (!await confirm(message)) {
15240
+ console.log(" Aborted.");
15241
+ return;
15242
+ }
15243
+ }
15244
+ const result = await runEnterpriseBootstrap(
15245
+ {
15246
+ targetDir: targetDir ?? ".",
15247
+ customerSlug,
15248
+ repository,
15249
+ stages: opts.stage,
15250
+ region: opts.region,
15251
+ accountId: opts.accountId,
15252
+ releaseVersion: opts.releaseVersion,
15253
+ manifestUrl: opts.manifestUrl,
15254
+ manifestSha256: opts.manifestSha256,
15255
+ terraformModuleVersion: opts.terraformModuleVersion,
15256
+ artifactBucket: opts.artifactBucket,
15257
+ stateBucket: opts.stateBucket,
15258
+ lockTable: opts.lockTable,
15259
+ dispatchWorkflow: opts.dispatch,
15260
+ dryRun: opts.dryRun
15261
+ },
15262
+ { identity }
15263
+ );
15264
+ printBootstrapSummary(result);
15265
+ printSuccess(
15266
+ opts.dryRun ? "Enterprise bootstrap dry-run complete" : "Enterprise bootstrap complete"
15267
+ );
15268
+ } catch (err) {
15269
+ printError(err.message);
15270
+ process.exit(1);
15271
+ }
15272
+ }
15273
+ );
15274
+ }
15275
+ function planned(target, message) {
15276
+ return { target, status: "planned", message };
15277
+ }
15278
+ function secretPlaceholderResult(repository, environment) {
15279
+ return {
15280
+ target: `${repository}:${environment.stage}:secrets`,
15281
+ status: "planned",
15282
+ message: `Set GitHub Environment secrets for ${environment.stage}: ${environment.secretPlaceholders.join(", ")}.`
15283
+ };
15284
+ }
15285
+ function recordDeploymentMetadata(plan, saveDeployment = saveEnterpriseDeployment) {
15286
+ saveDeployment({
15287
+ customerSlug: plan.customerSlug,
15288
+ repository: plan.repository,
15289
+ targetDir: plan.targetDir,
15290
+ accountId: plan.accountId,
15291
+ region: plan.region,
15292
+ stages: plan.stages,
15293
+ artifactBucket: plan.aws.artifactBucket,
15294
+ stateBucket: plan.aws.stateBucket,
15295
+ lockTable: plan.aws.lockTable,
15296
+ releaseVersion: plan.release.version,
15297
+ releaseManifestUrl: plan.release.manifestUrl,
15298
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15299
+ });
15300
+ return {
15301
+ target: plan.customerSlug,
15302
+ status: "updated",
15303
+ message: "Recorded local enterprise deployment metadata without secrets."
15304
+ };
15305
+ }
15306
+ async function resolveCustomerSlug(flag) {
15307
+ if (flag) return flag;
15308
+ if (!process.stdin.isTTY) {
15309
+ throw new Error("Customer slug is required. Pass --customer <slug>.");
15310
+ }
15311
+ const { input: input20 } = await import("@inquirer/prompts");
15312
+ return input20({ message: "Customer slug:" });
15313
+ }
15314
+ async function resolveRepository(flag) {
15315
+ if (flag) return flag;
15316
+ if (!process.stdin.isTTY) {
15317
+ throw new Error("GitHub repository is required. Pass --repo <owner/name>.");
15318
+ }
15319
+ const { input: input20 } = await import("@inquirer/prompts");
15320
+ return input20({ message: "GitHub deployment repo (owner/name):" });
15321
+ }
15322
+ function printBootstrapSummary(result) {
15323
+ console.log("");
15324
+ console.log(" AWS");
15325
+ for (const step of result.aws) {
15326
+ console.log(` - ${step.status}: ${step.message}`);
15327
+ }
15328
+ console.log(" GitHub");
15329
+ for (const step of result.github) {
15330
+ console.log(` - ${step.status}: ${step.message}`);
15331
+ }
15332
+ if (result.template.preserved.length > 0) {
15333
+ printWarning(
15334
+ `Preserved ${result.template.preserved.length} customer-owned file(s) without the ThinkWork managed marker.`
15335
+ );
15336
+ }
15337
+ }
15338
+
15339
+ // src/commands/enterprise.ts
15340
+ function registerEnterpriseCommand(program2) {
15341
+ const enterprise = program2.command("enterprise").description(
15342
+ "Bootstrap and operate customer-owned ThinkWork enterprise deployment repositories."
15343
+ );
15344
+ registerEnterpriseBootstrapCommand(enterprise);
15345
+ }
15346
+
14331
15347
  // src/cli.ts
14332
15348
  var program = new Command();
14333
15349
  program.name("thinkwork").description(
@@ -14393,6 +15409,7 @@ registerPerformanceCommand(program);
14393
15409
  registerTraceCommand(program);
14394
15410
  registerDashboardCommand(program);
14395
15411
  registerEvalCommand(program);
15412
+ registerEnterpriseCommand(program);
14396
15413
  registerWikiCommand(program);
14397
15414
  for (const cmd of program.commands) {
14398
15415
  if (!cmd.options.some((o) => o.long === "--json")) {