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.
- package/dist/cli.js +1062 -45
- package/dist/commands/enterprise/templates/deploy-repo/.github/workflows/deploy.yml +232 -0
- package/dist/commands/enterprise/templates/deploy-repo/README.md +31 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/branding/README.md +7 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/deployment.json +6 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/evals/README.md +10 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/seeds/README.md +7 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/skills/README.md +7 -0
- package/dist/commands/enterprise/templates/deploy-repo/customer/workspace-defaults/README.md +7 -0
- package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +606 -0
- package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +99 -0
- package/dist/commands/enterprise/templates/deploy-repo/terraform/backend-dev.hcl +6 -0
- package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +101 -0
- package/dist/commands/enterprise/templates/deploy-repo/terraform/stages/dev.tfvars +9 -0
- package/dist/commands/enterprise/templates/deploy-repo/terraform/stages/prod.tfvars +9 -0
- package/dist/commands/enterprise/templates/deploy-repo/thinkwork.lock +17 -0
- package/dist/terraform/examples/greenfield/main.tf +26 -0
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -0
- package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +7 -7
- package/dist/terraform/modules/app/lambda-api/handlers.tf +78 -68
- package/dist/terraform/modules/app/lambda-api/outputs.tf +9 -4
- package/dist/terraform/modules/app/lambda-api/remote-artifacts.tf +36 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +7 -0
- package/dist/terraform/modules/app/lambda-api/workspace-events.tf +1 -1
- package/dist/terraform/modules/thinkwork/main.tf +3 -2
- package/dist/terraform/modules/thinkwork/outputs.tf +5 -0
- package/dist/terraform/modules/thinkwork/variables.tf +6 -0
- package/dist/terraform/schema.graphql +10 -40
- 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((
|
|
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)
|
|
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((
|
|
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) =>
|
|
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((
|
|
472
|
+
return new Promise((resolve6) => {
|
|
473
473
|
rl.question(`${message} [y/N] `, (answer) => {
|
|
474
474
|
rl.close();
|
|
475
|
-
|
|
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((
|
|
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
|
-
|
|
567
|
+
resolve6();
|
|
568
568
|
});
|
|
569
569
|
proc.on("error", (err) => {
|
|
570
570
|
printWarning(`post-deploy probe spawn failed: ${err.message}`);
|
|
571
|
-
|
|
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 {
|
|
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((
|
|
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)
|
|
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((
|
|
1042
|
+
return new Promise((resolve6) => {
|
|
1026
1043
|
const proc = spawn3("bash", [scriptPath, ...args], {
|
|
1027
1044
|
stdio: "inherit"
|
|
1028
1045
|
});
|
|
1029
|
-
proc.on("close", (code) =>
|
|
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
|
|
1286
|
+
const awsOk2 = await ensureAwsCli();
|
|
1270
1287
|
const tfOk = await ensureTerraform();
|
|
1271
|
-
if (
|
|
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((
|
|
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
|
|
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((
|
|
1623
|
+
return new Promise((resolve6) => {
|
|
1607
1624
|
rl.question(prompt, (answer) => {
|
|
1608
1625
|
rl.close();
|
|
1609
|
-
|
|
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
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
);
|
|
1751
|
-
|
|
1752
|
-
|
|
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 (
|
|
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
|
|
1954
|
-
if (!Number.isFinite(
|
|
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
|
|
1969
|
-
if (!
|
|
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((
|
|
2187
|
+
return new Promise((resolve6) => {
|
|
2106
2188
|
rl.question(` ${prompt}${suffix}: `, (answer) => {
|
|
2107
2189
|
rl.close();
|
|
2108
|
-
|
|
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((
|
|
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)
|
|
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((
|
|
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) =>
|
|
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(
|
|
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")) {
|