thinkwork-cli 0.12.2 → 0.12.4
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 +1586 -32
- package/dist/commands/enterprise/templates/deploy-repo/.github/workflows/deploy.yml +236 -0
- package/dist/commands/enterprise/templates/deploy-repo/README.md +33 -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/docs/runbook.md +79 -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/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((resolve7, 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) resolve7(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((resolve7) => {
|
|
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) => resolve7(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((resolve7) => {
|
|
473
473
|
rl.question(`${message} [y/N] `, (answer) => {
|
|
474
474
|
rl.close();
|
|
475
|
-
|
|
475
|
+
resolve7(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((resolve7) => {
|
|
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
|
+
resolve7();
|
|
568
568
|
});
|
|
569
569
|
proc.on("error", (err) => {
|
|
570
570
|
printWarning(`post-deploy probe spawn failed: ${err.message}`);
|
|
571
|
-
|
|
571
|
+
resolve7();
|
|
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((resolve7, 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) resolve7(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((resolve7) => {
|
|
1026
1043
|
const proc = spawn3("bash", [scriptPath, ...args], {
|
|
1027
1044
|
stdio: "inherit"
|
|
1028
1045
|
});
|
|
1029
|
-
proc.on("close", (code) =>
|
|
1046
|
+
proc.on("close", (code) => resolve7(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((resolve7, 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 resolve7(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((resolve7) => {
|
|
1607
1624
|
rl.question(prompt, (answer) => {
|
|
1608
1625
|
rl.close();
|
|
1609
|
-
|
|
1626
|
+
resolve7(answer.trim());
|
|
1610
1627
|
});
|
|
1611
1628
|
});
|
|
1612
1629
|
}
|
|
@@ -2018,8 +2035,8 @@ Registered callback URL:
|
|
|
2018
2035
|
}
|
|
2019
2036
|
const targetProfile = opts.profile ?? "thinkwork";
|
|
2020
2037
|
printHeader("login", targetProfile);
|
|
2021
|
-
const
|
|
2022
|
-
if (!
|
|
2038
|
+
const awsOk2 = await ensureAwsCli();
|
|
2039
|
+
if (!awsOk2) process.exit(1);
|
|
2023
2040
|
const port = Number.parseInt(opts.port, 10);
|
|
2024
2041
|
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
2025
2042
|
printError(`Invalid --port value: "${opts.port}".`);
|
|
@@ -2167,10 +2184,10 @@ var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
|
2167
2184
|
function ask2(prompt, defaultVal = "") {
|
|
2168
2185
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2169
2186
|
const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
|
|
2170
|
-
return new Promise((
|
|
2187
|
+
return new Promise((resolve7) => {
|
|
2171
2188
|
rl.question(` ${prompt}${suffix}: `, (answer) => {
|
|
2172
2189
|
rl.close();
|
|
2173
|
-
|
|
2190
|
+
resolve7(answer.trim() || defaultVal);
|
|
2174
2191
|
});
|
|
2175
2192
|
});
|
|
2176
2193
|
}
|
|
@@ -3906,7 +3923,7 @@ function registerUpdateCommand(program2) {
|
|
|
3906
3923
|
import { spawn as spawn5 } from "child_process";
|
|
3907
3924
|
import { input as input2, select as select7 } from "@inquirer/prompts";
|
|
3908
3925
|
function getTerraformOutput2(cwd, key) {
|
|
3909
|
-
return new Promise((
|
|
3926
|
+
return new Promise((resolve7, reject) => {
|
|
3910
3927
|
const proc = spawn5("terraform", ["output", "-raw", key], {
|
|
3911
3928
|
cwd,
|
|
3912
3929
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3916,7 +3933,7 @@ function getTerraformOutput2(cwd, key) {
|
|
|
3916
3933
|
proc.stdout.on("data", (d) => stdout += d);
|
|
3917
3934
|
proc.stderr.on("data", (d) => stderr += d);
|
|
3918
3935
|
proc.on("close", (code) => {
|
|
3919
|
-
if (code === 0)
|
|
3936
|
+
if (code === 0) resolve7(stdout.trim());
|
|
3920
3937
|
else
|
|
3921
3938
|
reject(
|
|
3922
3939
|
new Error(
|
|
@@ -3927,7 +3944,7 @@ function getTerraformOutput2(cwd, key) {
|
|
|
3927
3944
|
});
|
|
3928
3945
|
}
|
|
3929
3946
|
function runAwsCognitoReset(userPoolId, username, region) {
|
|
3930
|
-
return new Promise((
|
|
3947
|
+
return new Promise((resolve7) => {
|
|
3931
3948
|
const args = [
|
|
3932
3949
|
"cognito-idp",
|
|
3933
3950
|
"admin-reset-user-password",
|
|
@@ -3944,7 +3961,7 @@ function runAwsCognitoReset(userPoolId, username, region) {
|
|
|
3944
3961
|
let stderr = "";
|
|
3945
3962
|
proc.stdout.on("data", (d) => stdout += d);
|
|
3946
3963
|
proc.stderr.on("data", (d) => stderr += d);
|
|
3947
|
-
proc.on("close", (code) =>
|
|
3964
|
+
proc.on("close", (code) => resolve7({ code: code ?? 1, stdout, stderr }));
|
|
3948
3965
|
});
|
|
3949
3966
|
}
|
|
3950
3967
|
function requireTty2(label) {
|
|
@@ -4625,6 +4642,7 @@ var CliRoutineTenantBySlugDocument = { "kind": "Document", "definitions": [{ "ki
|
|
|
4625
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" } }] } }] } }] };
|
|
4626
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" } }] } }] } }] };
|
|
4627
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" } }] } }] } }] };
|
|
4628
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" } }] } }] } }] };
|
|
4629
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" } }] } }] } }] };
|
|
4630
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" } }] } }] } }] };
|
|
@@ -4683,6 +4701,7 @@ var CliCreateWebhookDocument = { "kind": "Document", "definitions": [{ "kind": "
|
|
|
4683
4701
|
var CliUpdateWebhookDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliUpdateWebhook" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "input" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "UpdateWebhookInput" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "updateWebhook" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }, { "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": "targetType" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "rateLimit" } }] } }] } }] };
|
|
4684
4702
|
var CliDeleteWebhookDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliDeleteWebhook" }, "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": "deleteWebhook" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }] }] } }] };
|
|
4685
4703
|
var CliRegenerateWebhookTokenDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliRegenerateWebhookToken" }, "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": "regenerateWebhookToken" }, "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": "token" } }] } }] } }] };
|
|
4704
|
+
var CliWebhookDeliveriesDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliWebhookDeliveries" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "webhookId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }, { "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": "webhookDeliveries" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "webhookId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "webhookId" } } }, { "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": "providerName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "providerEventId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "normalizedKind" } }, { "kind": "Field", "name": { "kind": "Name", "value": "receivedAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "signatureStatus" } }, { "kind": "Field", "name": { "kind": "Name", "value": "resolutionStatus" } }, { "kind": "Field", "name": { "kind": "Name", "value": "statusCode" } }, { "kind": "Field", "name": { "kind": "Name", "value": "durationMs" } }, { "kind": "Field", "name": { "kind": "Name", "value": "threadId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "threadCreated" } }, { "kind": "Field", "name": { "kind": "Name", "value": "retryCount" } }, { "kind": "Field", "name": { "kind": "Name", "value": "isReplay" } }, { "kind": "Field", "name": { "kind": "Name", "value": "errorMessage" } }] } }] } }] };
|
|
4686
4705
|
var CliWebhookTenantBySlugDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliWebhookTenantBySlug" }, "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" } }] } }] } }] };
|
|
4687
4706
|
var CliWikiTenantBySlugDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliWikiTenantBySlug" }, "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" } }, { "kind": "Field", "name": { "kind": "Name", "value": "slug" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }] } }] } }] };
|
|
4688
4707
|
var CliAllTenantAgentsForWikiDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliAllTenantAgentsForWiki" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "allTenantAgents" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "includeSystem" }, "value": { "kind": "BooleanValue", "value": false } }, { "kind": "Argument", "name": { "kind": "Name", "value": "includeSubAgents" }, "value": { "kind": "BooleanValue", "value": false } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "slug" } }, { "kind": "Field", "name": { "kind": "Name", "value": "type" } }, { "kind": "Field", "name": { "kind": "Name", "value": "status" } }] } }] } }] };
|
|
@@ -4800,6 +4819,7 @@ var documents = {
|
|
|
4800
4819
|
"\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,
|
|
4801
4820
|
"\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,
|
|
4802
4821
|
"\n mutation CliCreateScheduledJob($input: CreateScheduledJobInput!) {\n createScheduledJob(input: $input) {\n id\n name\n enabled\n scheduleExpression\n timezone\n }\n }\n": CliCreateScheduledJobDocument,
|
|
4822
|
+
"\n mutation CliDeleteScheduledJob($id: ID!) {\n deleteScheduledJob(id: $id) {\n id\n ok\n }\n }\n": CliDeleteScheduledJobDocument,
|
|
4803
4823
|
"\n query CliSchedJobTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n }\n }\n": CliSchedJobTenantBySlugDocument,
|
|
4804
4824
|
"\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,
|
|
4805
4825
|
"\n query CliSkillTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n }\n }\n": CliSkillTenantBySlugDocument,
|
|
@@ -4858,6 +4878,7 @@ var documents = {
|
|
|
4858
4878
|
"\n mutation CliUpdateWebhook($id: ID!, $input: UpdateWebhookInput!) {\n updateWebhook(id: $id, input: $input) {\n id\n name\n targetType\n enabled\n rateLimit\n }\n }\n": CliUpdateWebhookDocument,
|
|
4859
4879
|
"\n mutation CliDeleteWebhook($id: ID!) {\n deleteWebhook(id: $id)\n }\n": CliDeleteWebhookDocument,
|
|
4860
4880
|
"\n mutation CliRegenerateWebhookToken($id: ID!) {\n regenerateWebhookToken(id: $id) {\n id\n token\n }\n }\n": CliRegenerateWebhookTokenDocument,
|
|
4881
|
+
"\n query CliWebhookDeliveries($webhookId: ID!, $limit: Int) {\n webhookDeliveries(webhookId: $webhookId, limit: $limit) {\n id\n providerName\n providerEventId\n normalizedKind\n receivedAt\n signatureStatus\n resolutionStatus\n statusCode\n durationMs\n threadId\n threadCreated\n retryCount\n isReplay\n errorMessage\n }\n }\n": CliWebhookDeliveriesDocument,
|
|
4861
4882
|
"\n query CliWebhookTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n }\n }\n": CliWebhookTenantBySlugDocument,
|
|
4862
4883
|
"\n query CliWikiTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n slug\n name\n }\n }\n": CliWikiTenantBySlugDocument,
|
|
4863
4884
|
"\n query CliAllTenantAgentsForWiki($tenantId: ID!) {\n allTenantAgents(tenantId: $tenantId, includeSystem: false, includeSubAgents: false) {\n id\n name\n slug\n type\n status\n }\n }\n": CliAllTenantAgentsForWikiDocument,
|
|
@@ -9912,6 +9933,14 @@ var CreateScheduledJobDoc = graphql(`
|
|
|
9912
9933
|
}
|
|
9913
9934
|
}
|
|
9914
9935
|
`);
|
|
9936
|
+
var DeleteScheduledJobDoc = graphql(`
|
|
9937
|
+
mutation CliDeleteScheduledJob($id: ID!) {
|
|
9938
|
+
deleteScheduledJob(id: $id) {
|
|
9939
|
+
id
|
|
9940
|
+
ok
|
|
9941
|
+
}
|
|
9942
|
+
}
|
|
9943
|
+
`);
|
|
9915
9944
|
var SchedJobTenantBySlugDoc = graphql(`
|
|
9916
9945
|
query CliSchedJobTenantBySlug($slug: String!) {
|
|
9917
9946
|
tenantBySlug(slug: $slug) {
|
|
@@ -10072,6 +10101,39 @@ async function runSchedCreate(name, opts) {
|
|
|
10072
10101
|
`Created scheduled job ${data.createScheduledJob.id} \u2014 ${data.createScheduledJob.name} (${data.createScheduledJob.scheduleExpression}, ${data.createScheduledJob.timezone}).`
|
|
10073
10102
|
);
|
|
10074
10103
|
}
|
|
10104
|
+
async function runSchedDelete(id, opts) {
|
|
10105
|
+
const ctx = await resolveSchedContext(opts);
|
|
10106
|
+
if (!opts.yes) {
|
|
10107
|
+
if (!isInteractive()) {
|
|
10108
|
+
printError(
|
|
10109
|
+
"Refusing to delete without --yes in non-interactive mode (CI / piped stdin)."
|
|
10110
|
+
);
|
|
10111
|
+
process.exit(1);
|
|
10112
|
+
}
|
|
10113
|
+
requireTty("confirmation");
|
|
10114
|
+
const answer = await promptOrExit(
|
|
10115
|
+
() => input16({
|
|
10116
|
+
message: `Delete scheduled job ${id}? Type "delete" to confirm:`
|
|
10117
|
+
})
|
|
10118
|
+
);
|
|
10119
|
+
if (answer.trim() !== "delete") {
|
|
10120
|
+
console.log(" Cancelled.");
|
|
10121
|
+
return;
|
|
10122
|
+
}
|
|
10123
|
+
}
|
|
10124
|
+
const data = await gqlMutate(ctx.client, DeleteScheduledJobDoc, { id });
|
|
10125
|
+
if (isJsonMode()) {
|
|
10126
|
+
printJson(data.deleteScheduledJob);
|
|
10127
|
+
return;
|
|
10128
|
+
}
|
|
10129
|
+
if (data.deleteScheduledJob.ok) {
|
|
10130
|
+
printSuccess(`Deleted scheduled job ${data.deleteScheduledJob.id}.`);
|
|
10131
|
+
} else {
|
|
10132
|
+
console.log(
|
|
10133
|
+
` Scheduled job ${id} was already deleted (no row matched).`
|
|
10134
|
+
);
|
|
10135
|
+
}
|
|
10136
|
+
}
|
|
10075
10137
|
function notYetImplementedAtApi(verb) {
|
|
10076
10138
|
printError(
|
|
10077
10139
|
`\`scheduled-job ${verb}\` is not yet implemented at the GraphQL API.
|
|
@@ -10096,7 +10158,9 @@ Examples:
|
|
|
10096
10158
|
`
|
|
10097
10159
|
).action(runSchedCreate);
|
|
10098
10160
|
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"));
|
|
10099
|
-
job.command("delete <id>").description(
|
|
10161
|
+
job.command("delete <id>").description(
|
|
10162
|
+
"Delete a scheduled job. Deprovisions the EventBridge schedule first, then removes the row."
|
|
10163
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(runSchedDelete);
|
|
10100
10164
|
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"));
|
|
10101
10165
|
}
|
|
10102
10166
|
|
|
@@ -10557,6 +10621,26 @@ var RegenerateWebhookTokenDoc = graphql(`
|
|
|
10557
10621
|
}
|
|
10558
10622
|
}
|
|
10559
10623
|
`);
|
|
10624
|
+
var WebhookDeliveriesDoc = graphql(`
|
|
10625
|
+
query CliWebhookDeliveries($webhookId: ID!, $limit: Int) {
|
|
10626
|
+
webhookDeliveries(webhookId: $webhookId, limit: $limit) {
|
|
10627
|
+
id
|
|
10628
|
+
providerName
|
|
10629
|
+
providerEventId
|
|
10630
|
+
normalizedKind
|
|
10631
|
+
receivedAt
|
|
10632
|
+
signatureStatus
|
|
10633
|
+
resolutionStatus
|
|
10634
|
+
statusCode
|
|
10635
|
+
durationMs
|
|
10636
|
+
threadId
|
|
10637
|
+
threadCreated
|
|
10638
|
+
retryCount
|
|
10639
|
+
isReplay
|
|
10640
|
+
errorMessage
|
|
10641
|
+
}
|
|
10642
|
+
}
|
|
10643
|
+
`);
|
|
10560
10644
|
var WebhookTenantBySlugDoc = graphql(`
|
|
10561
10645
|
query CliWebhookTenantBySlug($slug: String!) {
|
|
10562
10646
|
tenantBySlug(slug: $slug) {
|
|
@@ -10790,6 +10874,50 @@ async function runWebhookRotate(id, opts) {
|
|
|
10790
10874
|
console.log(" New token (SAVE THIS):");
|
|
10791
10875
|
console.log(` ${wh.token}`);
|
|
10792
10876
|
}
|
|
10877
|
+
async function runWebhookDeliveries(id, opts) {
|
|
10878
|
+
const ctx = await resolveWebhookContext(opts);
|
|
10879
|
+
const limit = Math.min(
|
|
10880
|
+
Math.max(Number.parseInt(opts.limit ?? "25", 10) || 25, 1),
|
|
10881
|
+
500
|
|
10882
|
+
);
|
|
10883
|
+
const data = await gqlQuery(ctx.client, WebhookDeliveriesDoc, {
|
|
10884
|
+
webhookId: id,
|
|
10885
|
+
limit
|
|
10886
|
+
});
|
|
10887
|
+
const rows = data.webhookDeliveries;
|
|
10888
|
+
if (isJsonMode()) {
|
|
10889
|
+
printJson({ items: rows });
|
|
10890
|
+
return;
|
|
10891
|
+
}
|
|
10892
|
+
if (rows.length === 0) {
|
|
10893
|
+
logStderr(`No deliveries recorded for webhook ${id}.`);
|
|
10894
|
+
return;
|
|
10895
|
+
}
|
|
10896
|
+
printTable(
|
|
10897
|
+
rows.map((r) => ({
|
|
10898
|
+
received: r.receivedAt ?? "",
|
|
10899
|
+
provider: r.providerName ?? "\u2014",
|
|
10900
|
+
event: r.normalizedKind ?? r.providerEventId ?? "\u2014",
|
|
10901
|
+
sig: r.signatureStatus,
|
|
10902
|
+
resolution: r.resolutionStatus,
|
|
10903
|
+
status: r.statusCode != null ? String(r.statusCode) : "\u2014",
|
|
10904
|
+
durMs: r.durationMs != null ? String(r.durationMs) : "\u2014",
|
|
10905
|
+
retry: r.retryCount != null ? String(r.retryCount) : "\u2014",
|
|
10906
|
+
thread: r.threadId ?? "\u2014"
|
|
10907
|
+
})),
|
|
10908
|
+
[
|
|
10909
|
+
{ key: "received", header: "Received" },
|
|
10910
|
+
{ key: "provider", header: "Provider" },
|
|
10911
|
+
{ key: "event", header: "Event" },
|
|
10912
|
+
{ key: "sig", header: "Sig" },
|
|
10913
|
+
{ key: "resolution", header: "Resolution" },
|
|
10914
|
+
{ key: "status", header: "Status" },
|
|
10915
|
+
{ key: "durMs", header: "Dur(ms)" },
|
|
10916
|
+
{ key: "retry", header: "Retry" },
|
|
10917
|
+
{ key: "thread", header: "Thread" }
|
|
10918
|
+
]
|
|
10919
|
+
);
|
|
10920
|
+
}
|
|
10793
10921
|
function notYetImplementedAtApi2(verb) {
|
|
10794
10922
|
printError(
|
|
10795
10923
|
`\`webhook ${verb}\` is not yet implemented at the GraphQL API.
|
|
@@ -10812,7 +10940,9 @@ Examples:
|
|
|
10812
10940
|
wh.command("delete <id>").description("Delete a webhook (its URL stops working immediately).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(runWebhookDelete);
|
|
10813
10941
|
wh.command("test <id>").description("Send a synthetic payload to the webhook. (API surface pending.)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--payload <json>").action(() => notYetImplementedAtApi2("test"));
|
|
10814
10942
|
wh.command("rotate <id>").description("Generate a new token for an existing webhook.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(runWebhookRotate);
|
|
10815
|
-
wh.command("deliveries <id>").description(
|
|
10943
|
+
wh.command("deliveries <id>").description(
|
|
10944
|
+
"Show recent delivery attempts for a webhook (newest first). Default 25, max 500."
|
|
10945
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max rows (1-500)", "25").action(runWebhookDeliveries);
|
|
10816
10946
|
}
|
|
10817
10947
|
|
|
10818
10948
|
// src/lib/plugin-zip.ts
|
|
@@ -13205,10 +13335,15 @@ async function runEvalSeed(opts) {
|
|
|
13205
13335
|
categories: opts.category && opts.category.length > 0 ? opts.category : null
|
|
13206
13336
|
});
|
|
13207
13337
|
if (isJsonMode()) {
|
|
13208
|
-
printJson({
|
|
13338
|
+
printJson({
|
|
13339
|
+
source: "built-in-yaml-seed",
|
|
13340
|
+
inserted: data.seedEvalTestCases
|
|
13341
|
+
});
|
|
13209
13342
|
return;
|
|
13210
13343
|
}
|
|
13211
|
-
printSuccess(
|
|
13344
|
+
printSuccess(
|
|
13345
|
+
`Seeded ${data.seedEvalTestCases} new test case(s). (Duplicates were skipped.)`
|
|
13346
|
+
);
|
|
13212
13347
|
}
|
|
13213
13348
|
|
|
13214
13349
|
// src/commands/eval/test-case/list.ts
|
|
@@ -14393,6 +14528,1424 @@ Examples:
|
|
|
14393
14528
|
});
|
|
14394
14529
|
}
|
|
14395
14530
|
|
|
14531
|
+
// src/commands/enterprise/bootstrap.ts
|
|
14532
|
+
import { resolve as resolve5 } from "path";
|
|
14533
|
+
|
|
14534
|
+
// src/commands/enterprise/template.ts
|
|
14535
|
+
import {
|
|
14536
|
+
existsSync as existsSync10,
|
|
14537
|
+
mkdirSync as mkdirSync5,
|
|
14538
|
+
readFileSync as readFileSync8,
|
|
14539
|
+
readdirSync as readdirSync2,
|
|
14540
|
+
statSync as statSync2,
|
|
14541
|
+
writeFileSync as writeFileSync5
|
|
14542
|
+
} from "fs";
|
|
14543
|
+
import { dirname as dirname4, join as join7, relative as relative2, resolve as resolve4 } from "path";
|
|
14544
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
14545
|
+
var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
|
|
14546
|
+
var MANAGED_MARKER = "thinkwork-managed: enterprise-deploy-template";
|
|
14547
|
+
var CUSTOMER_SLUG_PATTERN = /^[a-z][a-z0-9-]{1,38}[a-z0-9]$/;
|
|
14548
|
+
function renderEnterpriseDeployRepoTemplate(options) {
|
|
14549
|
+
const customerSlug = validateCustomerSlug(options.customerSlug);
|
|
14550
|
+
const stages = validateStages(options.stages ?? ["dev", "prod"]);
|
|
14551
|
+
const targetDir = resolve4(options.targetDir);
|
|
14552
|
+
const templateRoot = findEnterpriseTemplateRoot();
|
|
14553
|
+
const replacements = buildTemplateReplacements({
|
|
14554
|
+
...options,
|
|
14555
|
+
customerSlug,
|
|
14556
|
+
stages
|
|
14557
|
+
});
|
|
14558
|
+
const templateFiles = listTemplateFiles(templateRoot).filter(
|
|
14559
|
+
(path2) => !relative2(templateRoot, path2).startsWith("terraform/stages/") && !/^terraform\/backend-[^.]+\.hcl$/.test(
|
|
14560
|
+
relative2(templateRoot, path2).split("\\").join("/")
|
|
14561
|
+
)
|
|
14562
|
+
);
|
|
14563
|
+
const written = [];
|
|
14564
|
+
const preserved = [];
|
|
14565
|
+
for (const templatePath of templateFiles) {
|
|
14566
|
+
const relativePath = relative2(templateRoot, templatePath);
|
|
14567
|
+
const outputPath = join7(targetDir, relativePath);
|
|
14568
|
+
let content = applyTemplate(
|
|
14569
|
+
readFileSync8(templatePath, "utf8"),
|
|
14570
|
+
replacements
|
|
14571
|
+
);
|
|
14572
|
+
if (relativePath.split("\\").join("/") === "customer/deployment.json") {
|
|
14573
|
+
content = renderCustomerDeploymentJson(content, customerSlug, stages);
|
|
14574
|
+
}
|
|
14575
|
+
writeManagedFile(outputPath, content, written, preserved);
|
|
14576
|
+
}
|
|
14577
|
+
const stageTemplateRoot = join7(templateRoot, "terraform", "stages");
|
|
14578
|
+
const backendTemplatePath = join7(
|
|
14579
|
+
templateRoot,
|
|
14580
|
+
"terraform",
|
|
14581
|
+
"backend-dev.hcl"
|
|
14582
|
+
);
|
|
14583
|
+
for (const stage of stages) {
|
|
14584
|
+
const explicitTemplate = join7(stageTemplateRoot, `${stage}.tfvars`);
|
|
14585
|
+
const fallbackTemplate = join7(stageTemplateRoot, "dev.tfvars");
|
|
14586
|
+
const templatePath = existsSync10(explicitTemplate) ? explicitTemplate : fallbackTemplate;
|
|
14587
|
+
const outputPath = join7(
|
|
14588
|
+
targetDir,
|
|
14589
|
+
"terraform",
|
|
14590
|
+
"stages",
|
|
14591
|
+
`${stage}.tfvars`
|
|
14592
|
+
);
|
|
14593
|
+
const content = applyTemplate(readFileSync8(templatePath, "utf8"), {
|
|
14594
|
+
...replacements,
|
|
14595
|
+
STAGE: stage
|
|
14596
|
+
});
|
|
14597
|
+
writeManagedFile(outputPath, content, written, preserved);
|
|
14598
|
+
const backendPath = join7(targetDir, "terraform", `backend-${stage}.hcl`);
|
|
14599
|
+
const backendContent = applyTemplate(
|
|
14600
|
+
readFileSync8(backendTemplatePath, "utf8"),
|
|
14601
|
+
{
|
|
14602
|
+
...replacements,
|
|
14603
|
+
STAGE: stage
|
|
14604
|
+
}
|
|
14605
|
+
);
|
|
14606
|
+
writeManagedFile(backendPath, backendContent, written, preserved);
|
|
14607
|
+
}
|
|
14608
|
+
return { targetDir, written, preserved };
|
|
14609
|
+
}
|
|
14610
|
+
function validateCustomerSlug(slug) {
|
|
14611
|
+
const normalized = slug.trim();
|
|
14612
|
+
if (!CUSTOMER_SLUG_PATTERN.test(normalized)) {
|
|
14613
|
+
throw new Error(
|
|
14614
|
+
`Invalid customer slug "${slug}". Must be lowercase alphanumeric + hyphens, 3-40 characters, starting with a letter.`
|
|
14615
|
+
);
|
|
14616
|
+
}
|
|
14617
|
+
return normalized;
|
|
14618
|
+
}
|
|
14619
|
+
function validateStages(stages) {
|
|
14620
|
+
const normalized = [
|
|
14621
|
+
...new Set(stages.map((stage) => stage.trim()).filter(Boolean))
|
|
14622
|
+
];
|
|
14623
|
+
if (normalized.length === 0) {
|
|
14624
|
+
throw new Error("At least one deployment stage is required.");
|
|
14625
|
+
}
|
|
14626
|
+
for (const stage of normalized) {
|
|
14627
|
+
const check = validateStage(stage);
|
|
14628
|
+
if (!check.valid) {
|
|
14629
|
+
throw new Error(check.error ?? `Invalid stage name "${stage}".`);
|
|
14630
|
+
}
|
|
14631
|
+
}
|
|
14632
|
+
return normalized;
|
|
14633
|
+
}
|
|
14634
|
+
function findEnterpriseTemplateRoot() {
|
|
14635
|
+
const candidates = [
|
|
14636
|
+
resolve4(__dirname2, "templates/deploy-repo"),
|
|
14637
|
+
resolve4(__dirname2, "commands/enterprise/templates/deploy-repo")
|
|
14638
|
+
];
|
|
14639
|
+
for (const candidate of candidates) {
|
|
14640
|
+
if (existsSync10(join7(candidate, "thinkwork.lock"))) return candidate;
|
|
14641
|
+
}
|
|
14642
|
+
throw new Error(
|
|
14643
|
+
"Enterprise deployment repo template not found. The CLI package may be incomplete."
|
|
14644
|
+
);
|
|
14645
|
+
}
|
|
14646
|
+
function buildTemplateReplacements(options) {
|
|
14647
|
+
const releaseVersion = options.releaseVersion ?? "v0.0.0";
|
|
14648
|
+
const artifactBucket = options.artifactBucket ?? `${options.customerSlug}-thinkwork-release-artifacts`;
|
|
14649
|
+
return {
|
|
14650
|
+
ACCOUNT_ID: options.accountId ?? "123456789012",
|
|
14651
|
+
ARTIFACT_BUCKET: artifactBucket,
|
|
14652
|
+
CUSTOMER_SLUG: options.customerSlug,
|
|
14653
|
+
LAMBDA_ARTIFACT_PREFIX: `releases/${releaseVersion}/lambdas`,
|
|
14654
|
+
REGION: options.region ?? "us-east-1",
|
|
14655
|
+
RELEASE_MANIFEST_SHA256: options.releaseManifestSha256 ?? "CHANGE_ME",
|
|
14656
|
+
RELEASE_MANIFEST_URL: options.releaseManifestUrl ?? `https://github.com/thinkwork-ai/thinkwork/releases/download/${releaseVersion}/thinkwork-release.json`,
|
|
14657
|
+
RELEASE_VERSION: releaseVersion,
|
|
14658
|
+
TERRAFORM_MODULE_VERSION: options.terraformModuleVersion ?? releaseVersion.replace(/^v/, "")
|
|
14659
|
+
};
|
|
14660
|
+
}
|
|
14661
|
+
function renderCustomerDeploymentJson(content, customerSlug, stages) {
|
|
14662
|
+
const deployment = JSON.parse(content);
|
|
14663
|
+
deployment.stages = Object.fromEntries(
|
|
14664
|
+
stages.map((stage) => [
|
|
14665
|
+
stage,
|
|
14666
|
+
{
|
|
14667
|
+
tenantSlug: stage === "prod" ? customerSlug : `${customerSlug}-${stage}`,
|
|
14668
|
+
evalPacks: [],
|
|
14669
|
+
seedPacks: [],
|
|
14670
|
+
skillPacks: [],
|
|
14671
|
+
workspaceDefaultPacks: [],
|
|
14672
|
+
branding: null
|
|
14673
|
+
}
|
|
14674
|
+
])
|
|
14675
|
+
);
|
|
14676
|
+
return `${JSON.stringify(deployment, null, 2)}
|
|
14677
|
+
`;
|
|
14678
|
+
}
|
|
14679
|
+
function listTemplateFiles(root) {
|
|
14680
|
+
const out = [];
|
|
14681
|
+
for (const entry of readdirSync2(root)) {
|
|
14682
|
+
const path2 = join7(root, entry);
|
|
14683
|
+
const stat = statSync2(path2);
|
|
14684
|
+
if (stat.isDirectory()) {
|
|
14685
|
+
out.push(...listTemplateFiles(path2));
|
|
14686
|
+
} else if (stat.isFile()) {
|
|
14687
|
+
out.push(path2);
|
|
14688
|
+
}
|
|
14689
|
+
}
|
|
14690
|
+
return out.sort();
|
|
14691
|
+
}
|
|
14692
|
+
function applyTemplate(source, replacements) {
|
|
14693
|
+
return source.replaceAll(/\{\{([A-Z0-9_]+)\}\}/g, (_match, key) => {
|
|
14694
|
+
if (!(key in replacements)) {
|
|
14695
|
+
throw new Error(`Unknown enterprise deployment template token: ${key}`);
|
|
14696
|
+
}
|
|
14697
|
+
return replacements[key];
|
|
14698
|
+
});
|
|
14699
|
+
}
|
|
14700
|
+
function writeManagedFile(path2, content, written, preserved) {
|
|
14701
|
+
mkdirSync5(dirname4(path2), { recursive: true });
|
|
14702
|
+
if (existsSync10(path2)) {
|
|
14703
|
+
const current = readFileSync8(path2, "utf8");
|
|
14704
|
+
if (!current.includes(MANAGED_MARKER)) {
|
|
14705
|
+
preserved.push(path2);
|
|
14706
|
+
return;
|
|
14707
|
+
}
|
|
14708
|
+
}
|
|
14709
|
+
writeFileSync5(path2, content);
|
|
14710
|
+
written.push(path2);
|
|
14711
|
+
}
|
|
14712
|
+
|
|
14713
|
+
// src/commands/enterprise/aws-bootstrap.ts
|
|
14714
|
+
import { execFileSync } from "child_process";
|
|
14715
|
+
import { mkdtempSync, writeFileSync as writeFileSync6 } from "fs";
|
|
14716
|
+
import { tmpdir } from "os";
|
|
14717
|
+
import { join as join8 } from "path";
|
|
14718
|
+
function buildEnterpriseAwsBootstrapPlan(config) {
|
|
14719
|
+
const oidcProviderArn = `arn:aws:iam::${config.accountId}:oidc-provider/token.actions.githubusercontent.com`;
|
|
14720
|
+
return {
|
|
14721
|
+
stateBucket: config.stateBucket,
|
|
14722
|
+
lockTable: config.lockTable,
|
|
14723
|
+
artifactBucket: config.artifactBucket,
|
|
14724
|
+
oidcProviderArn,
|
|
14725
|
+
stageRoles: config.stages.map((stage) => {
|
|
14726
|
+
const roleName = `thinkwork-${config.customerSlug}-${stage}-deploy`;
|
|
14727
|
+
return {
|
|
14728
|
+
stage,
|
|
14729
|
+
roleName,
|
|
14730
|
+
roleArn: `arn:aws:iam::${config.accountId}:role/${roleName}`,
|
|
14731
|
+
trustPolicy: buildGitHubOidcTrustPolicy({
|
|
14732
|
+
oidcProviderArn,
|
|
14733
|
+
repository: config.repository,
|
|
14734
|
+
stage
|
|
14735
|
+
}),
|
|
14736
|
+
deployPolicyName: `thinkwork-${config.customerSlug}-${stage}-deploy`,
|
|
14737
|
+
deployPolicy: buildEnterpriseDeployRolePolicy({
|
|
14738
|
+
accountId: config.accountId,
|
|
14739
|
+
region: config.region,
|
|
14740
|
+
stage,
|
|
14741
|
+
stateBucket: config.stateBucket,
|
|
14742
|
+
lockTable: config.lockTable,
|
|
14743
|
+
artifactBucket: config.artifactBucket
|
|
14744
|
+
})
|
|
14745
|
+
};
|
|
14746
|
+
})
|
|
14747
|
+
};
|
|
14748
|
+
}
|
|
14749
|
+
function buildGitHubOidcTrustPolicy(options) {
|
|
14750
|
+
return {
|
|
14751
|
+
Version: "2012-10-17",
|
|
14752
|
+
Statement: [
|
|
14753
|
+
{
|
|
14754
|
+
Effect: "Allow",
|
|
14755
|
+
Principal: {
|
|
14756
|
+
Federated: options.oidcProviderArn
|
|
14757
|
+
},
|
|
14758
|
+
Action: "sts:AssumeRoleWithWebIdentity",
|
|
14759
|
+
Condition: {
|
|
14760
|
+
StringEquals: {
|
|
14761
|
+
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
|
|
14762
|
+
"token.actions.githubusercontent.com:sub": `repo:${options.repository}:environment:${options.stage}`
|
|
14763
|
+
}
|
|
14764
|
+
}
|
|
14765
|
+
}
|
|
14766
|
+
]
|
|
14767
|
+
};
|
|
14768
|
+
}
|
|
14769
|
+
function buildEnterpriseDeployRolePolicy(options) {
|
|
14770
|
+
const thinkworkPrefix = `thinkwork-${options.stage}`;
|
|
14771
|
+
return {
|
|
14772
|
+
Version: "2012-10-17",
|
|
14773
|
+
Statement: [
|
|
14774
|
+
{
|
|
14775
|
+
Sid: "TerraformStateAndReleaseBuckets",
|
|
14776
|
+
Effect: "Allow",
|
|
14777
|
+
Action: [
|
|
14778
|
+
"s3:GetBucketLocation",
|
|
14779
|
+
"s3:GetBucketVersioning",
|
|
14780
|
+
"s3:GetEncryptionConfiguration",
|
|
14781
|
+
"s3:ListBucket",
|
|
14782
|
+
"s3:PutBucketVersioning",
|
|
14783
|
+
"s3:PutEncryptionConfiguration",
|
|
14784
|
+
"s3:PutLifecycleConfiguration",
|
|
14785
|
+
"s3:PutPublicAccessBlock"
|
|
14786
|
+
],
|
|
14787
|
+
Resource: [
|
|
14788
|
+
`arn:aws:s3:::${options.stateBucket}`,
|
|
14789
|
+
`arn:aws:s3:::${options.artifactBucket}`,
|
|
14790
|
+
`arn:aws:s3:::${thinkworkPrefix}-*`
|
|
14791
|
+
]
|
|
14792
|
+
},
|
|
14793
|
+
{
|
|
14794
|
+
Sid: "TerraformStateAndReleaseObjects",
|
|
14795
|
+
Effect: "Allow",
|
|
14796
|
+
Action: [
|
|
14797
|
+
"s3:AbortMultipartUpload",
|
|
14798
|
+
"s3:DeleteObject",
|
|
14799
|
+
"s3:GetObject",
|
|
14800
|
+
"s3:PutObject"
|
|
14801
|
+
],
|
|
14802
|
+
Resource: [
|
|
14803
|
+
`arn:aws:s3:::${options.stateBucket}/*`,
|
|
14804
|
+
`arn:aws:s3:::${options.artifactBucket}/*`,
|
|
14805
|
+
`arn:aws:s3:::${thinkworkPrefix}-*/*`
|
|
14806
|
+
]
|
|
14807
|
+
},
|
|
14808
|
+
{
|
|
14809
|
+
Sid: "TerraformStateLocks",
|
|
14810
|
+
Effect: "Allow",
|
|
14811
|
+
Action: [
|
|
14812
|
+
"dynamodb:CreateTable",
|
|
14813
|
+
"dynamodb:DescribeTable",
|
|
14814
|
+
"dynamodb:GetItem",
|
|
14815
|
+
"dynamodb:PutItem",
|
|
14816
|
+
"dynamodb:DeleteItem",
|
|
14817
|
+
"dynamodb:UpdateItem"
|
|
14818
|
+
],
|
|
14819
|
+
Resource: `arn:aws:dynamodb:${options.region}:${options.accountId}:table/${options.lockTable}`
|
|
14820
|
+
},
|
|
14821
|
+
{
|
|
14822
|
+
Sid: "ThinkWorkNamedResources",
|
|
14823
|
+
Effect: "Allow",
|
|
14824
|
+
Action: [
|
|
14825
|
+
"apigateway:*",
|
|
14826
|
+
"appsync:*",
|
|
14827
|
+
"bedrock:*",
|
|
14828
|
+
"bedrock-agentcore:*",
|
|
14829
|
+
"cloudfront:*",
|
|
14830
|
+
"cognito-idp:*",
|
|
14831
|
+
"dynamodb:*",
|
|
14832
|
+
"ec2:*",
|
|
14833
|
+
"ecr:*",
|
|
14834
|
+
"ecs:*",
|
|
14835
|
+
"elasticfilesystem:*",
|
|
14836
|
+
"elasticloadbalancing:*",
|
|
14837
|
+
"events:*",
|
|
14838
|
+
"iam:*",
|
|
14839
|
+
"lambda:*",
|
|
14840
|
+
"logs:*",
|
|
14841
|
+
"rds:*",
|
|
14842
|
+
"scheduler:*",
|
|
14843
|
+
"secretsmanager:*",
|
|
14844
|
+
"ses:*",
|
|
14845
|
+
"sqs:*",
|
|
14846
|
+
"ssm:*",
|
|
14847
|
+
"states:*",
|
|
14848
|
+
"xray:*"
|
|
14849
|
+
],
|
|
14850
|
+
Resource: "*"
|
|
14851
|
+
}
|
|
14852
|
+
]
|
|
14853
|
+
};
|
|
14854
|
+
}
|
|
14855
|
+
var AwsCliEnterpriseBootstrapClient = class {
|
|
14856
|
+
async ensureStateBucket(bucket, region) {
|
|
14857
|
+
return ensureBucket(bucket, region, "terraform state bucket");
|
|
14858
|
+
}
|
|
14859
|
+
async ensureLockTable(table, region) {
|
|
14860
|
+
if (awsOk([
|
|
14861
|
+
"dynamodb",
|
|
14862
|
+
"describe-table",
|
|
14863
|
+
"--table-name",
|
|
14864
|
+
table,
|
|
14865
|
+
"--region",
|
|
14866
|
+
region
|
|
14867
|
+
])) {
|
|
14868
|
+
return {
|
|
14869
|
+
target: table,
|
|
14870
|
+
status: "reused",
|
|
14871
|
+
message: `DynamoDB lock table ${table} already exists.`
|
|
14872
|
+
};
|
|
14873
|
+
}
|
|
14874
|
+
execFileSync("aws", [
|
|
14875
|
+
"dynamodb",
|
|
14876
|
+
"create-table",
|
|
14877
|
+
"--table-name",
|
|
14878
|
+
table,
|
|
14879
|
+
"--attribute-definitions",
|
|
14880
|
+
"AttributeName=LockID,AttributeType=S",
|
|
14881
|
+
"--key-schema",
|
|
14882
|
+
"AttributeName=LockID,KeyType=HASH",
|
|
14883
|
+
"--billing-mode",
|
|
14884
|
+
"PAY_PER_REQUEST",
|
|
14885
|
+
"--region",
|
|
14886
|
+
region
|
|
14887
|
+
]);
|
|
14888
|
+
return {
|
|
14889
|
+
target: table,
|
|
14890
|
+
status: "created",
|
|
14891
|
+
message: `Created DynamoDB lock table ${table}.`
|
|
14892
|
+
};
|
|
14893
|
+
}
|
|
14894
|
+
async ensureArtifactBucket(bucket, region) {
|
|
14895
|
+
return ensureBucket(bucket, region, "release artifact bucket");
|
|
14896
|
+
}
|
|
14897
|
+
async ensureOidcProvider(accountId) {
|
|
14898
|
+
const arn = `arn:aws:iam::${accountId}:oidc-provider/token.actions.githubusercontent.com`;
|
|
14899
|
+
if (awsOk([
|
|
14900
|
+
"iam",
|
|
14901
|
+
"get-open-id-connect-provider",
|
|
14902
|
+
"--open-id-connect-provider-arn",
|
|
14903
|
+
arn
|
|
14904
|
+
])) {
|
|
14905
|
+
return {
|
|
14906
|
+
target: arn,
|
|
14907
|
+
status: "reused",
|
|
14908
|
+
message: "GitHub Actions OIDC provider already exists."
|
|
14909
|
+
};
|
|
14910
|
+
}
|
|
14911
|
+
execFileSync("aws", [
|
|
14912
|
+
"iam",
|
|
14913
|
+
"create-open-id-connect-provider",
|
|
14914
|
+
"--url",
|
|
14915
|
+
"https://token.actions.githubusercontent.com",
|
|
14916
|
+
"--client-id-list",
|
|
14917
|
+
"sts.amazonaws.com",
|
|
14918
|
+
"--thumbprint-list",
|
|
14919
|
+
"6938fd4d98bab03faadb97b34396831e3780aea1"
|
|
14920
|
+
]);
|
|
14921
|
+
return {
|
|
14922
|
+
target: arn,
|
|
14923
|
+
status: "created",
|
|
14924
|
+
message: "Created GitHub Actions OIDC provider."
|
|
14925
|
+
};
|
|
14926
|
+
}
|
|
14927
|
+
async ensureDeployRole(role) {
|
|
14928
|
+
if (awsOk(["iam", "get-role", "--role-name", role.roleName])) {
|
|
14929
|
+
putRolePolicy(role);
|
|
14930
|
+
return {
|
|
14931
|
+
target: role.roleArn,
|
|
14932
|
+
status: "updated",
|
|
14933
|
+
message: `Deploy role ${role.roleName} already exists; updated inline deploy policy ${role.deployPolicyName}.`
|
|
14934
|
+
};
|
|
14935
|
+
}
|
|
14936
|
+
const dir = mkdtempSync(join8(tmpdir(), "thinkwork-enterprise-role-"));
|
|
14937
|
+
const trustPath = join8(dir, "trust.json");
|
|
14938
|
+
writeFileSync6(trustPath, JSON.stringify(role.trustPolicy));
|
|
14939
|
+
execFileSync("aws", [
|
|
14940
|
+
"iam",
|
|
14941
|
+
"create-role",
|
|
14942
|
+
"--role-name",
|
|
14943
|
+
role.roleName,
|
|
14944
|
+
"--assume-role-policy-document",
|
|
14945
|
+
`file://${trustPath}`
|
|
14946
|
+
]);
|
|
14947
|
+
putRolePolicy(role);
|
|
14948
|
+
return {
|
|
14949
|
+
target: role.roleArn,
|
|
14950
|
+
status: "created",
|
|
14951
|
+
message: `Created deploy role ${role.roleName} and attached inline deploy policy ${role.deployPolicyName}.`
|
|
14952
|
+
};
|
|
14953
|
+
}
|
|
14954
|
+
};
|
|
14955
|
+
function putRolePolicy(role) {
|
|
14956
|
+
const dir = mkdtempSync(join8(tmpdir(), "thinkwork-enterprise-policy-"));
|
|
14957
|
+
const policyPath = join8(dir, "policy.json");
|
|
14958
|
+
writeFileSync6(policyPath, JSON.stringify(role.deployPolicy));
|
|
14959
|
+
execFileSync("aws", [
|
|
14960
|
+
"iam",
|
|
14961
|
+
"put-role-policy",
|
|
14962
|
+
"--role-name",
|
|
14963
|
+
role.roleName,
|
|
14964
|
+
"--policy-name",
|
|
14965
|
+
role.deployPolicyName,
|
|
14966
|
+
"--policy-document",
|
|
14967
|
+
`file://${policyPath}`
|
|
14968
|
+
]);
|
|
14969
|
+
}
|
|
14970
|
+
function ensureBucket(bucket, region, label) {
|
|
14971
|
+
if (awsOk(["s3api", "head-bucket", "--bucket", bucket])) {
|
|
14972
|
+
return {
|
|
14973
|
+
target: bucket,
|
|
14974
|
+
status: "reused",
|
|
14975
|
+
message: `${label} ${bucket} already exists.`
|
|
14976
|
+
};
|
|
14977
|
+
}
|
|
14978
|
+
const args = region === "us-east-1" ? ["s3api", "create-bucket", "--bucket", bucket, "--region", region] : [
|
|
14979
|
+
"s3api",
|
|
14980
|
+
"create-bucket",
|
|
14981
|
+
"--bucket",
|
|
14982
|
+
bucket,
|
|
14983
|
+
"--region",
|
|
14984
|
+
region,
|
|
14985
|
+
"--create-bucket-configuration",
|
|
14986
|
+
`LocationConstraint=${region}`
|
|
14987
|
+
];
|
|
14988
|
+
execFileSync("aws", args);
|
|
14989
|
+
execFileSync("aws", [
|
|
14990
|
+
"s3api",
|
|
14991
|
+
"put-public-access-block",
|
|
14992
|
+
"--bucket",
|
|
14993
|
+
bucket,
|
|
14994
|
+
"--public-access-block-configuration",
|
|
14995
|
+
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
|
|
14996
|
+
]);
|
|
14997
|
+
return {
|
|
14998
|
+
target: bucket,
|
|
14999
|
+
status: "created",
|
|
15000
|
+
message: `Created ${label} ${bucket}.`
|
|
15001
|
+
};
|
|
15002
|
+
}
|
|
15003
|
+
function awsOk(args) {
|
|
15004
|
+
try {
|
|
15005
|
+
execFileSync("aws", args, { stdio: "ignore" });
|
|
15006
|
+
return true;
|
|
15007
|
+
} catch {
|
|
15008
|
+
return false;
|
|
15009
|
+
}
|
|
15010
|
+
}
|
|
15011
|
+
|
|
15012
|
+
// src/commands/enterprise/github.ts
|
|
15013
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
15014
|
+
function parseGitHubRepository(input20) {
|
|
15015
|
+
const trimmed = input20.trim();
|
|
15016
|
+
const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(trimmed);
|
|
15017
|
+
if (!match) {
|
|
15018
|
+
throw new Error(
|
|
15019
|
+
`Invalid GitHub repository "${input20}". Use owner/name, for example acme/thinkwork-deploy.`
|
|
15020
|
+
);
|
|
15021
|
+
}
|
|
15022
|
+
return {
|
|
15023
|
+
owner: match[1],
|
|
15024
|
+
name: match[2],
|
|
15025
|
+
fullName: `${match[1]}/${match[2]}`
|
|
15026
|
+
};
|
|
15027
|
+
}
|
|
15028
|
+
function buildEnterpriseGitHubBootstrapPlan(options) {
|
|
15029
|
+
const repository = parseGitHubRepository(options.repository);
|
|
15030
|
+
return {
|
|
15031
|
+
repository,
|
|
15032
|
+
dispatchWorkflow: options.dispatchWorkflow ?? false,
|
|
15033
|
+
environments: options.stages.map((stage) => {
|
|
15034
|
+
const role = options.stageRoles.find((item) => item.stage === stage);
|
|
15035
|
+
if (!role) {
|
|
15036
|
+
throw new Error(`Missing deploy role for stage "${stage}".`);
|
|
15037
|
+
}
|
|
15038
|
+
return {
|
|
15039
|
+
stage,
|
|
15040
|
+
roleArn: role.roleArn,
|
|
15041
|
+
vars: {
|
|
15042
|
+
AWS_REGION: options.region,
|
|
15043
|
+
AWS_ROLE_ARN: role.roleArn,
|
|
15044
|
+
THINKWORK_ARTIFACT_BUCKET: options.artifactBucket
|
|
15045
|
+
},
|
|
15046
|
+
secretPlaceholders: ["TF_VAR_DB_PASSWORD", "TF_VAR_API_AUTH_SECRET"]
|
|
15047
|
+
};
|
|
15048
|
+
})
|
|
15049
|
+
};
|
|
15050
|
+
}
|
|
15051
|
+
var GhCliEnterpriseBootstrapClient = class {
|
|
15052
|
+
constructor(repository) {
|
|
15053
|
+
this.repository = repository;
|
|
15054
|
+
}
|
|
15055
|
+
repository;
|
|
15056
|
+
async ensureEnvironment(environment) {
|
|
15057
|
+
gh([
|
|
15058
|
+
"api",
|
|
15059
|
+
"--method",
|
|
15060
|
+
"PUT",
|
|
15061
|
+
`repos/${this.repository.fullName}/environments/${environment.stage}`,
|
|
15062
|
+
"--field",
|
|
15063
|
+
"wait_timer=0"
|
|
15064
|
+
]);
|
|
15065
|
+
return {
|
|
15066
|
+
target: `${this.repository.fullName}:${environment.stage}`,
|
|
15067
|
+
status: "updated",
|
|
15068
|
+
message: `Ensured GitHub Environment ${environment.stage}.`
|
|
15069
|
+
};
|
|
15070
|
+
}
|
|
15071
|
+
async upsertEnvironmentVariables(environment) {
|
|
15072
|
+
for (const [name, value] of Object.entries(environment.vars)) {
|
|
15073
|
+
gh([
|
|
15074
|
+
"variable",
|
|
15075
|
+
"set",
|
|
15076
|
+
name,
|
|
15077
|
+
"--repo",
|
|
15078
|
+
this.repository.fullName,
|
|
15079
|
+
"--env",
|
|
15080
|
+
environment.stage,
|
|
15081
|
+
"--body",
|
|
15082
|
+
value
|
|
15083
|
+
]);
|
|
15084
|
+
}
|
|
15085
|
+
return {
|
|
15086
|
+
target: `${this.repository.fullName}:${environment.stage}:vars`,
|
|
15087
|
+
status: "updated",
|
|
15088
|
+
message: `Updated non-secret GitHub Environment variables for ${environment.stage}.`
|
|
15089
|
+
};
|
|
15090
|
+
}
|
|
15091
|
+
async writeRepositoryFiles(targetDir) {
|
|
15092
|
+
return {
|
|
15093
|
+
target: targetDir,
|
|
15094
|
+
status: "updated",
|
|
15095
|
+
message: "Repository files were written locally. Commit/push this directory or run from a checked-out deployment repo."
|
|
15096
|
+
};
|
|
15097
|
+
}
|
|
15098
|
+
async dispatchWorkflow(stage) {
|
|
15099
|
+
gh([
|
|
15100
|
+
"workflow",
|
|
15101
|
+
"run",
|
|
15102
|
+
"deploy.yml",
|
|
15103
|
+
"--repo",
|
|
15104
|
+
this.repository.fullName,
|
|
15105
|
+
"--field",
|
|
15106
|
+
`stage=${stage}`
|
|
15107
|
+
]);
|
|
15108
|
+
return {
|
|
15109
|
+
target: `${this.repository.fullName}:deploy.yml:${stage}`,
|
|
15110
|
+
status: "created",
|
|
15111
|
+
message: `Dispatched deploy workflow for ${stage}.`
|
|
15112
|
+
};
|
|
15113
|
+
}
|
|
15114
|
+
};
|
|
15115
|
+
function gh(args) {
|
|
15116
|
+
return execFileSync2("gh", args, { encoding: "utf8" });
|
|
15117
|
+
}
|
|
15118
|
+
|
|
15119
|
+
// src/commands/enterprise/release.ts
|
|
15120
|
+
function resolveEnterpriseReleasePin(options) {
|
|
15121
|
+
const version = options.releaseVersion ?? `v${VERSION}`;
|
|
15122
|
+
return {
|
|
15123
|
+
version,
|
|
15124
|
+
manifestUrl: options.manifestUrl ?? `https://github.com/thinkwork-ai/thinkwork/releases/download/${version}/thinkwork-release.json`,
|
|
15125
|
+
manifestSha256: options.manifestSha256 ?? "CHANGE_ME",
|
|
15126
|
+
terraformModuleVersion: options.terraformModuleVersion ?? version.replace(/^v/, "")
|
|
15127
|
+
};
|
|
15128
|
+
}
|
|
15129
|
+
|
|
15130
|
+
// src/commands/enterprise/bootstrap.ts
|
|
15131
|
+
function buildEnterpriseBootstrapPlan(options, identity) {
|
|
15132
|
+
const customerSlug = validateCustomerSlug(options.customerSlug);
|
|
15133
|
+
const repository = parseGitHubRepository(options.repository).fullName;
|
|
15134
|
+
const stages = validateStages(options.stages ?? ["dev", "prod"]);
|
|
15135
|
+
const accountId = options.accountId ?? identity?.account;
|
|
15136
|
+
const region = options.region ?? identity?.region;
|
|
15137
|
+
if (!accountId) {
|
|
15138
|
+
throw new Error(
|
|
15139
|
+
"AWS account ID is required. Configure AWS credentials or pass --account-id."
|
|
15140
|
+
);
|
|
15141
|
+
}
|
|
15142
|
+
if (!region || region === "unknown") {
|
|
15143
|
+
throw new Error(
|
|
15144
|
+
"AWS region is required. Configure AWS_REGION or pass --region."
|
|
15145
|
+
);
|
|
15146
|
+
}
|
|
15147
|
+
const release = resolveEnterpriseReleasePin({
|
|
15148
|
+
releaseVersion: options.releaseVersion,
|
|
15149
|
+
manifestUrl: options.manifestUrl,
|
|
15150
|
+
manifestSha256: options.manifestSha256,
|
|
15151
|
+
terraformModuleVersion: options.terraformModuleVersion
|
|
15152
|
+
});
|
|
15153
|
+
const artifactBucket = options.artifactBucket ?? `${customerSlug}-thinkwork-release-artifacts`;
|
|
15154
|
+
const stateBucket = options.stateBucket ?? `${customerSlug}-thinkwork-terraform-state`;
|
|
15155
|
+
const lockTable = options.lockTable ?? `${customerSlug}-thinkwork-terraform-locks`;
|
|
15156
|
+
const aws = buildEnterpriseAwsBootstrapPlan({
|
|
15157
|
+
accountId,
|
|
15158
|
+
region,
|
|
15159
|
+
repository,
|
|
15160
|
+
stages,
|
|
15161
|
+
customerSlug,
|
|
15162
|
+
stateBucket,
|
|
15163
|
+
lockTable,
|
|
15164
|
+
artifactBucket
|
|
15165
|
+
});
|
|
15166
|
+
const github = buildEnterpriseGitHubBootstrapPlan({
|
|
15167
|
+
repository,
|
|
15168
|
+
stages,
|
|
15169
|
+
region,
|
|
15170
|
+
artifactBucket,
|
|
15171
|
+
stageRoles: aws.stageRoles,
|
|
15172
|
+
dispatchWorkflow: options.dispatchWorkflow
|
|
15173
|
+
});
|
|
15174
|
+
return {
|
|
15175
|
+
customerSlug,
|
|
15176
|
+
targetDir: resolve5(options.targetDir),
|
|
15177
|
+
repository,
|
|
15178
|
+
stages,
|
|
15179
|
+
accountId,
|
|
15180
|
+
region,
|
|
15181
|
+
release,
|
|
15182
|
+
aws,
|
|
15183
|
+
github
|
|
15184
|
+
};
|
|
15185
|
+
}
|
|
15186
|
+
async function runEnterpriseBootstrap(options, deps = {}) {
|
|
15187
|
+
const identity = deps.identity === void 0 ? getAwsIdentity() : deps.identity;
|
|
15188
|
+
if (!options.dryRun && !identity && (!options.accountId || !options.region)) {
|
|
15189
|
+
throw new Error(
|
|
15190
|
+
"AWS identity is required before mutating AWS or GitHub resources."
|
|
15191
|
+
);
|
|
15192
|
+
}
|
|
15193
|
+
const plan = buildEnterpriseBootstrapPlan(options, identity);
|
|
15194
|
+
const template = renderEnterpriseDeployRepoTemplate({
|
|
15195
|
+
targetDir: plan.targetDir,
|
|
15196
|
+
customerSlug: plan.customerSlug,
|
|
15197
|
+
stages: plan.stages,
|
|
15198
|
+
region: plan.region,
|
|
15199
|
+
accountId: plan.accountId,
|
|
15200
|
+
releaseVersion: plan.release.version,
|
|
15201
|
+
releaseManifestUrl: plan.release.manifestUrl,
|
|
15202
|
+
releaseManifestSha256: plan.release.manifestSha256,
|
|
15203
|
+
terraformModuleVersion: plan.release.terraformModuleVersion,
|
|
15204
|
+
artifactBucket: plan.aws.artifactBucket
|
|
15205
|
+
});
|
|
15206
|
+
const awsClient = deps.awsClient ?? new AwsCliEnterpriseBootstrapClient();
|
|
15207
|
+
const githubClient = deps.githubClient ?? new GhCliEnterpriseBootstrapClient(parseGitHubRepository(plan.repository));
|
|
15208
|
+
const awsResults = [];
|
|
15209
|
+
const githubResults = [];
|
|
15210
|
+
if (options.dryRun) {
|
|
15211
|
+
awsResults.push(
|
|
15212
|
+
planned(plan.aws.stateBucket, "Would ensure Terraform state bucket."),
|
|
15213
|
+
planned(plan.aws.lockTable, "Would ensure Terraform lock table."),
|
|
15214
|
+
planned(plan.aws.artifactBucket, "Would ensure release artifact bucket."),
|
|
15215
|
+
planned(
|
|
15216
|
+
plan.aws.oidcProviderArn,
|
|
15217
|
+
"Would ensure GitHub Actions OIDC provider."
|
|
15218
|
+
),
|
|
15219
|
+
...plan.aws.stageRoles.map(
|
|
15220
|
+
(role) => planned(role.roleArn, `Would ensure deploy role for ${role.stage}.`)
|
|
15221
|
+
)
|
|
15222
|
+
);
|
|
15223
|
+
githubResults.push(
|
|
15224
|
+
planned(plan.targetDir, "Would write deployment repository files."),
|
|
15225
|
+
...plan.github.environments.flatMap((environment) => [
|
|
15226
|
+
planned(
|
|
15227
|
+
`${plan.repository}:${environment.stage}`,
|
|
15228
|
+
`Would ensure GitHub Environment ${environment.stage}.`
|
|
15229
|
+
),
|
|
15230
|
+
planned(
|
|
15231
|
+
`${plan.repository}:${environment.stage}:vars`,
|
|
15232
|
+
`Would upsert non-secret GitHub variables for ${environment.stage}.`
|
|
15233
|
+
),
|
|
15234
|
+
planned(
|
|
15235
|
+
`${plan.repository}:${environment.stage}:secrets`,
|
|
15236
|
+
`Would require GitHub Environment secrets for ${environment.stage}: ${environment.secretPlaceholders.join(", ")}.`
|
|
15237
|
+
),
|
|
15238
|
+
...plan.github.dispatchWorkflow ? [
|
|
15239
|
+
planned(
|
|
15240
|
+
`${plan.repository}:deploy.yml:${environment.stage}`,
|
|
15241
|
+
`Would dispatch deploy workflow for ${environment.stage}.`
|
|
15242
|
+
)
|
|
15243
|
+
] : []
|
|
15244
|
+
])
|
|
15245
|
+
);
|
|
15246
|
+
} else {
|
|
15247
|
+
awsResults.push(
|
|
15248
|
+
await awsClient.ensureStateBucket(plan.aws.stateBucket, plan.region),
|
|
15249
|
+
await awsClient.ensureLockTable(plan.aws.lockTable, plan.region),
|
|
15250
|
+
await awsClient.ensureArtifactBucket(
|
|
15251
|
+
plan.aws.artifactBucket,
|
|
15252
|
+
plan.region
|
|
15253
|
+
),
|
|
15254
|
+
await awsClient.ensureOidcProvider(plan.accountId)
|
|
15255
|
+
);
|
|
15256
|
+
for (const role of plan.aws.stageRoles) {
|
|
15257
|
+
awsResults.push(await awsClient.ensureDeployRole(role));
|
|
15258
|
+
}
|
|
15259
|
+
githubResults.push(await githubClient.writeRepositoryFiles(plan.targetDir));
|
|
15260
|
+
for (const environment of plan.github.environments) {
|
|
15261
|
+
githubResults.push(
|
|
15262
|
+
await githubClient.ensureEnvironment(environment),
|
|
15263
|
+
await githubClient.upsertEnvironmentVariables(environment),
|
|
15264
|
+
secretPlaceholderResult(plan.repository, environment)
|
|
15265
|
+
);
|
|
15266
|
+
if (plan.github.dispatchWorkflow) {
|
|
15267
|
+
githubResults.push(
|
|
15268
|
+
await githubClient.dispatchWorkflow(environment.stage)
|
|
15269
|
+
);
|
|
15270
|
+
}
|
|
15271
|
+
}
|
|
15272
|
+
}
|
|
15273
|
+
const metadata = options.dryRun ? planned(
|
|
15274
|
+
plan.customerSlug,
|
|
15275
|
+
"Would record local enterprise deployment metadata without secrets."
|
|
15276
|
+
) : recordDeploymentMetadata(plan, deps.saveDeployment);
|
|
15277
|
+
return {
|
|
15278
|
+
plan,
|
|
15279
|
+
template,
|
|
15280
|
+
aws: awsResults,
|
|
15281
|
+
github: githubResults,
|
|
15282
|
+
metadata
|
|
15283
|
+
};
|
|
15284
|
+
}
|
|
15285
|
+
function registerEnterpriseBootstrapCommand(program2) {
|
|
15286
|
+
program2.command("bootstrap [targetDir]").description(
|
|
15287
|
+
"Bootstrap a customer-owned ThinkWork deployment repository and CI trust bridge."
|
|
15288
|
+
).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(
|
|
15289
|
+
"--manifest-sha256 <sha256>",
|
|
15290
|
+
"Pinned ThinkWork release manifest SHA-256"
|
|
15291
|
+
).option(
|
|
15292
|
+
"--terraform-module-version <version>",
|
|
15293
|
+
"Pinned Terraform Registry module version"
|
|
15294
|
+
).option(
|
|
15295
|
+
"--artifact-bucket <bucket>",
|
|
15296
|
+
"Customer-owned release artifact bucket"
|
|
15297
|
+
).option("--state-bucket <bucket>", "Terraform state bucket").option("--lock-table <table>", "Terraform state lock table").option("--dispatch", "Dispatch deploy workflow after bootstrap").option(
|
|
15298
|
+
"--dry-run",
|
|
15299
|
+
"Render files and print the plan without mutating AWS/GitHub"
|
|
15300
|
+
).option("-y, --yes", "Skip confirmation for mutating bootstrap").action(
|
|
15301
|
+
async (targetDir, opts) => {
|
|
15302
|
+
try {
|
|
15303
|
+
const customerSlug = await resolveCustomerSlug(opts.customer);
|
|
15304
|
+
const repository = await resolveRepository(opts.repo);
|
|
15305
|
+
const identity = getAwsIdentity();
|
|
15306
|
+
printHeader("enterprise bootstrap", customerSlug, identity);
|
|
15307
|
+
if (!opts.dryRun && !opts.yes) {
|
|
15308
|
+
const prodStages = (opts.stage ?? ["dev", "prod"]).filter(
|
|
15309
|
+
isProdLike
|
|
15310
|
+
);
|
|
15311
|
+
const message = prodStages.length > 0 ? ` Bootstrap deployment authority for ${repository} including production-like stage(s): ${prodStages.join(", ")}?` : ` Bootstrap deployment authority for ${repository}?`;
|
|
15312
|
+
if (!await confirm(message)) {
|
|
15313
|
+
console.log(" Aborted.");
|
|
15314
|
+
return;
|
|
15315
|
+
}
|
|
15316
|
+
}
|
|
15317
|
+
const result = await runEnterpriseBootstrap(
|
|
15318
|
+
{
|
|
15319
|
+
targetDir: targetDir ?? ".",
|
|
15320
|
+
customerSlug,
|
|
15321
|
+
repository,
|
|
15322
|
+
stages: opts.stage,
|
|
15323
|
+
region: opts.region,
|
|
15324
|
+
accountId: opts.accountId,
|
|
15325
|
+
releaseVersion: opts.releaseVersion,
|
|
15326
|
+
manifestUrl: opts.manifestUrl,
|
|
15327
|
+
manifestSha256: opts.manifestSha256,
|
|
15328
|
+
terraformModuleVersion: opts.terraformModuleVersion,
|
|
15329
|
+
artifactBucket: opts.artifactBucket,
|
|
15330
|
+
stateBucket: opts.stateBucket,
|
|
15331
|
+
lockTable: opts.lockTable,
|
|
15332
|
+
dispatchWorkflow: opts.dispatch,
|
|
15333
|
+
dryRun: opts.dryRun
|
|
15334
|
+
},
|
|
15335
|
+
{ identity }
|
|
15336
|
+
);
|
|
15337
|
+
printBootstrapSummary(result);
|
|
15338
|
+
printSuccess(
|
|
15339
|
+
opts.dryRun ? "Enterprise bootstrap dry-run complete" : "Enterprise bootstrap complete"
|
|
15340
|
+
);
|
|
15341
|
+
} catch (err) {
|
|
15342
|
+
printError(err.message);
|
|
15343
|
+
process.exit(1);
|
|
15344
|
+
}
|
|
15345
|
+
}
|
|
15346
|
+
);
|
|
15347
|
+
}
|
|
15348
|
+
function planned(target, message) {
|
|
15349
|
+
return { target, status: "planned", message };
|
|
15350
|
+
}
|
|
15351
|
+
function secretPlaceholderResult(repository, environment) {
|
|
15352
|
+
return {
|
|
15353
|
+
target: `${repository}:${environment.stage}:secrets`,
|
|
15354
|
+
status: "planned",
|
|
15355
|
+
message: `Set GitHub Environment secrets for ${environment.stage}: ${environment.secretPlaceholders.join(", ")}.`
|
|
15356
|
+
};
|
|
15357
|
+
}
|
|
15358
|
+
function recordDeploymentMetadata(plan, saveDeployment = saveEnterpriseDeployment) {
|
|
15359
|
+
saveDeployment({
|
|
15360
|
+
customerSlug: plan.customerSlug,
|
|
15361
|
+
repository: plan.repository,
|
|
15362
|
+
targetDir: plan.targetDir,
|
|
15363
|
+
accountId: plan.accountId,
|
|
15364
|
+
region: plan.region,
|
|
15365
|
+
stages: plan.stages,
|
|
15366
|
+
artifactBucket: plan.aws.artifactBucket,
|
|
15367
|
+
stateBucket: plan.aws.stateBucket,
|
|
15368
|
+
lockTable: plan.aws.lockTable,
|
|
15369
|
+
releaseVersion: plan.release.version,
|
|
15370
|
+
releaseManifestUrl: plan.release.manifestUrl,
|
|
15371
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15372
|
+
});
|
|
15373
|
+
return {
|
|
15374
|
+
target: plan.customerSlug,
|
|
15375
|
+
status: "updated",
|
|
15376
|
+
message: "Recorded local enterprise deployment metadata without secrets."
|
|
15377
|
+
};
|
|
15378
|
+
}
|
|
15379
|
+
async function resolveCustomerSlug(flag) {
|
|
15380
|
+
if (flag) return flag;
|
|
15381
|
+
if (!process.stdin.isTTY) {
|
|
15382
|
+
throw new Error("Customer slug is required. Pass --customer <slug>.");
|
|
15383
|
+
}
|
|
15384
|
+
const { input: input20 } = await import("@inquirer/prompts");
|
|
15385
|
+
return input20({ message: "Customer slug:" });
|
|
15386
|
+
}
|
|
15387
|
+
async function resolveRepository(flag) {
|
|
15388
|
+
if (flag) return flag;
|
|
15389
|
+
if (!process.stdin.isTTY) {
|
|
15390
|
+
throw new Error("GitHub repository is required. Pass --repo <owner/name>.");
|
|
15391
|
+
}
|
|
15392
|
+
const { input: input20 } = await import("@inquirer/prompts");
|
|
15393
|
+
return input20({ message: "GitHub deployment repo (owner/name):" });
|
|
15394
|
+
}
|
|
15395
|
+
function printBootstrapSummary(result) {
|
|
15396
|
+
console.log("");
|
|
15397
|
+
console.log(" AWS");
|
|
15398
|
+
for (const step of result.aws) {
|
|
15399
|
+
console.log(` - ${step.status}: ${step.message}`);
|
|
15400
|
+
}
|
|
15401
|
+
console.log(" GitHub");
|
|
15402
|
+
for (const step of result.github) {
|
|
15403
|
+
console.log(` - ${step.status}: ${step.message}`);
|
|
15404
|
+
}
|
|
15405
|
+
if (result.template.preserved.length > 0) {
|
|
15406
|
+
printWarning(
|
|
15407
|
+
`Preserved ${result.template.preserved.length} customer-owned file(s) without the ThinkWork managed marker.`
|
|
15408
|
+
);
|
|
15409
|
+
}
|
|
15410
|
+
}
|
|
15411
|
+
|
|
15412
|
+
// src/commands/enterprise/overlay.ts
|
|
15413
|
+
import { resolve as resolve6 } from "path";
|
|
15414
|
+
|
|
15415
|
+
// src/commands/enterprise/overlay-schema.ts
|
|
15416
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
|
|
15417
|
+
import { join as join9, relative as relative3, sep as sep2 } from "path";
|
|
15418
|
+
function loadEnterpriseOverlayDefinition(repoRoot) {
|
|
15419
|
+
const path2 = join9(repoRoot, "customer", "deployment.json");
|
|
15420
|
+
if (!existsSync11(path2)) {
|
|
15421
|
+
throw new Error(`Missing customer overlay definition: ${path2}`);
|
|
15422
|
+
}
|
|
15423
|
+
const parsed = JSON.parse(readFileSync9(path2, "utf8"));
|
|
15424
|
+
if (parsed.schemaVersion !== 1) {
|
|
15425
|
+
throw new Error(
|
|
15426
|
+
`Unsupported customer/deployment.json schemaVersion ${parsed.schemaVersion}`
|
|
15427
|
+
);
|
|
15428
|
+
}
|
|
15429
|
+
if (!isNonEmptyString(parsed.customerSlug)) {
|
|
15430
|
+
throw new Error("customer/deployment.json requires customerSlug");
|
|
15431
|
+
}
|
|
15432
|
+
if (!isRecord(parsed.stages)) {
|
|
15433
|
+
throw new Error("customer/deployment.json requires stages");
|
|
15434
|
+
}
|
|
15435
|
+
return {
|
|
15436
|
+
schemaVersion: 1,
|
|
15437
|
+
customerSlug: parsed.customerSlug,
|
|
15438
|
+
stages: Object.fromEntries(
|
|
15439
|
+
Object.entries(parsed.stages).map(([stage, value]) => [
|
|
15440
|
+
stage,
|
|
15441
|
+
normalizeStage(stage, value)
|
|
15442
|
+
])
|
|
15443
|
+
)
|
|
15444
|
+
};
|
|
15445
|
+
}
|
|
15446
|
+
function stageOverlay(definition, stage) {
|
|
15447
|
+
const config = definition.stages[stage];
|
|
15448
|
+
if (!config) {
|
|
15449
|
+
throw new Error(`customer/deployment.json does not define stage ${stage}`);
|
|
15450
|
+
}
|
|
15451
|
+
return config;
|
|
15452
|
+
}
|
|
15453
|
+
function readCustomerEvalPack(repoRoot, packName) {
|
|
15454
|
+
assertPackName(packName, "eval");
|
|
15455
|
+
const path2 = join9(repoRoot, "customer", "evals", `${packName}.json`);
|
|
15456
|
+
if (!existsSync11(path2)) {
|
|
15457
|
+
throw new Error(`Eval pack "${packName}" is missing: ${path2}`);
|
|
15458
|
+
}
|
|
15459
|
+
const parsed = JSON.parse(readFileSync9(path2, "utf8"));
|
|
15460
|
+
if (!Array.isArray(parsed)) {
|
|
15461
|
+
throw new Error(`Eval pack "${packName}" must be a JSON array`);
|
|
15462
|
+
}
|
|
15463
|
+
return parsed.map(
|
|
15464
|
+
(value, index) => normalizeEvalSeed(value, `customer/evals/${packName}.json[${index}]`)
|
|
15465
|
+
);
|
|
15466
|
+
}
|
|
15467
|
+
function readJsonSeedPack(repoRoot, packName) {
|
|
15468
|
+
assertPackName(packName, "seed");
|
|
15469
|
+
const path2 = join9(repoRoot, "customer", "seeds", `${packName}.json`);
|
|
15470
|
+
if (!existsSync11(path2)) {
|
|
15471
|
+
throw new Error(`Seed pack "${packName}" is missing: ${path2}`);
|
|
15472
|
+
}
|
|
15473
|
+
return JSON.parse(readFileSync9(path2, "utf8"));
|
|
15474
|
+
}
|
|
15475
|
+
function collectOverlayFiles(repoRoot, family, packName) {
|
|
15476
|
+
assertPackName(packName, family);
|
|
15477
|
+
const root = join9(repoRoot, "customer", family, packName);
|
|
15478
|
+
if (!existsSync11(root) || !statSync3(root).isDirectory()) {
|
|
15479
|
+
throw new Error(`Overlay pack "${family}/${packName}" is missing: ${root}`);
|
|
15480
|
+
}
|
|
15481
|
+
const files = [];
|
|
15482
|
+
walkFiles(root, (path2) => {
|
|
15483
|
+
const relativePath = relative3(root, path2).split(sep2).join("/");
|
|
15484
|
+
if (relativePath === ".DS_Store" || relativePath.endsWith("/.DS_Store")) {
|
|
15485
|
+
return;
|
|
15486
|
+
}
|
|
15487
|
+
files.push({
|
|
15488
|
+
relativePath,
|
|
15489
|
+
content: readFileSync9(path2, "utf8")
|
|
15490
|
+
});
|
|
15491
|
+
});
|
|
15492
|
+
if (family === "skills" && !files.some((file) => file.relativePath === "SKILL.md")) {
|
|
15493
|
+
throw new Error(`Skill pack "${packName}" must include SKILL.md`);
|
|
15494
|
+
}
|
|
15495
|
+
if (files.length === 0) {
|
|
15496
|
+
throw new Error(`Overlay pack "${family}/${packName}" contains no files`);
|
|
15497
|
+
}
|
|
15498
|
+
return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
15499
|
+
}
|
|
15500
|
+
function normalizeStage(stage, value) {
|
|
15501
|
+
if (!isRecord(value)) {
|
|
15502
|
+
throw new Error(`Stage ${stage} must be an object`);
|
|
15503
|
+
}
|
|
15504
|
+
if (!isNonEmptyString(value.tenantSlug)) {
|
|
15505
|
+
throw new Error(`Stage ${stage} requires tenantSlug`);
|
|
15506
|
+
}
|
|
15507
|
+
const branding = value.branding;
|
|
15508
|
+
if (branding !== null && branding !== void 0 && !isRecord(branding)) {
|
|
15509
|
+
throw new Error(`Stage ${stage} branding must be an object or null`);
|
|
15510
|
+
}
|
|
15511
|
+
return {
|
|
15512
|
+
tenantSlug: value.tenantSlug,
|
|
15513
|
+
evalPacks: normalizePackArray(value.evalPacks, `${stage}.evalPacks`),
|
|
15514
|
+
seedPacks: normalizePackArray(value.seedPacks, `${stage}.seedPacks`),
|
|
15515
|
+
skillPacks: normalizePackArray(value.skillPacks, `${stage}.skillPacks`),
|
|
15516
|
+
workspaceDefaultPacks: normalizePackArray(
|
|
15517
|
+
value.workspaceDefaultPacks,
|
|
15518
|
+
`${stage}.workspaceDefaultPacks`
|
|
15519
|
+
),
|
|
15520
|
+
branding: branding ?? null,
|
|
15521
|
+
defaultAgentTemplateSlug: isNonEmptyString(value.defaultAgentTemplateSlug) ? value.defaultAgentTemplateSlug : "default"
|
|
15522
|
+
};
|
|
15523
|
+
}
|
|
15524
|
+
function normalizeEvalSeed(value, label) {
|
|
15525
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
15526
|
+
if (!isNonEmptyString(value.name))
|
|
15527
|
+
throw new Error(`${label}.name is required`);
|
|
15528
|
+
if (!isNonEmptyString(value.category))
|
|
15529
|
+
throw new Error(`${label}.category is required`);
|
|
15530
|
+
if (!isNonEmptyString(value.query))
|
|
15531
|
+
throw new Error(`${label}.query is required`);
|
|
15532
|
+
const assertions = value.assertions;
|
|
15533
|
+
if (!Array.isArray(assertions)) {
|
|
15534
|
+
throw new Error(`${label}.assertions must be an array`);
|
|
15535
|
+
}
|
|
15536
|
+
return {
|
|
15537
|
+
name: value.name,
|
|
15538
|
+
category: value.category,
|
|
15539
|
+
query: value.query,
|
|
15540
|
+
systemPrompt: typeof value.systemPrompt === "string" ? value.systemPrompt : null,
|
|
15541
|
+
agentTemplateId: typeof value.agentTemplateId === "string" ? value.agentTemplateId : null,
|
|
15542
|
+
assertions: assertions.map(
|
|
15543
|
+
(assertion, index) => normalizeAssertion(assertion, `${label}.assertions[${index}]`)
|
|
15544
|
+
),
|
|
15545
|
+
agentcoreEvaluatorIds: normalizeOptionalStringArray(
|
|
15546
|
+
value.agentcoreEvaluatorIds,
|
|
15547
|
+
`${label}.agentcoreEvaluatorIds`
|
|
15548
|
+
),
|
|
15549
|
+
tags: normalizeOptionalStringArray(value.tags, `${label}.tags`),
|
|
15550
|
+
enabled: typeof value.enabled === "boolean" ? value.enabled : true
|
|
15551
|
+
};
|
|
15552
|
+
}
|
|
15553
|
+
function normalizeAssertion(value, label) {
|
|
15554
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
15555
|
+
if (!isNonEmptyString(value.type))
|
|
15556
|
+
throw new Error(`${label}.type is required`);
|
|
15557
|
+
return {
|
|
15558
|
+
type: value.type,
|
|
15559
|
+
value: typeof value.value === "string" || value.value === null ? value.value : void 0,
|
|
15560
|
+
path: typeof value.path === "string" || value.path === null ? value.path : void 0
|
|
15561
|
+
};
|
|
15562
|
+
}
|
|
15563
|
+
function normalizePackArray(value, label) {
|
|
15564
|
+
const items = normalizeStringArray(value, label);
|
|
15565
|
+
for (const item of items) assertPackName(item, label);
|
|
15566
|
+
return items;
|
|
15567
|
+
}
|
|
15568
|
+
function normalizeStringArray(value, label) {
|
|
15569
|
+
if (value === void 0) return [];
|
|
15570
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
15571
|
+
return value.map((item, index) => {
|
|
15572
|
+
if (!isNonEmptyString(item)) {
|
|
15573
|
+
throw new Error(`${label}[${index}] must be a non-empty string`);
|
|
15574
|
+
}
|
|
15575
|
+
return item;
|
|
15576
|
+
});
|
|
15577
|
+
}
|
|
15578
|
+
function normalizeOptionalStringArray(value, label) {
|
|
15579
|
+
if (value === void 0 || value === null) return [];
|
|
15580
|
+
return normalizeStringArray(value, label);
|
|
15581
|
+
}
|
|
15582
|
+
function assertPackName(value, label) {
|
|
15583
|
+
if (!/^[a-z0-9][a-z0-9_.-]*$/.test(value)) {
|
|
15584
|
+
throw new Error(
|
|
15585
|
+
`${label} pack "${value}" must use lowercase letters, numbers, dot, underscore, or hyphen`
|
|
15586
|
+
);
|
|
15587
|
+
}
|
|
15588
|
+
}
|
|
15589
|
+
function walkFiles(root, visit) {
|
|
15590
|
+
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
15591
|
+
const path2 = join9(root, entry.name);
|
|
15592
|
+
if (entry.isDirectory()) {
|
|
15593
|
+
walkFiles(path2, visit);
|
|
15594
|
+
} else if (entry.isFile()) {
|
|
15595
|
+
visit(path2);
|
|
15596
|
+
}
|
|
15597
|
+
}
|
|
15598
|
+
}
|
|
15599
|
+
function isRecord(value) {
|
|
15600
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15601
|
+
}
|
|
15602
|
+
function isNonEmptyString(value) {
|
|
15603
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
15604
|
+
}
|
|
15605
|
+
|
|
15606
|
+
// src/commands/enterprise/overlay-apply.ts
|
|
15607
|
+
function buildEnterpriseOverlayPlan(options) {
|
|
15608
|
+
const definition = loadEnterpriseOverlayDefinition(options.repoRoot);
|
|
15609
|
+
const stage = stageOverlay(definition, options.stage);
|
|
15610
|
+
return {
|
|
15611
|
+
repoRoot: options.repoRoot,
|
|
15612
|
+
stage: options.stage,
|
|
15613
|
+
tenantSlug: stage.tenantSlug,
|
|
15614
|
+
targetTemplateSlug: stage.defaultAgentTemplateSlug,
|
|
15615
|
+
operations: [
|
|
15616
|
+
...evalOperations(options.repoRoot, stage),
|
|
15617
|
+
...workspaceFileOperations(options.repoRoot, stage),
|
|
15618
|
+
...seedOperations(options.repoRoot, stage),
|
|
15619
|
+
...stage.branding ? [{ kind: "branding", branding: stage.branding }] : []
|
|
15620
|
+
]
|
|
15621
|
+
};
|
|
15622
|
+
}
|
|
15623
|
+
async function applyEnterpriseOverlay(plan, client) {
|
|
15624
|
+
const result = {
|
|
15625
|
+
stage: plan.stage,
|
|
15626
|
+
tenantSlug: plan.tenantSlug,
|
|
15627
|
+
evals: { inserted: 0, updated: 0, skipped: 0, packs: [] },
|
|
15628
|
+
workspaceFiles: { written: 0, packs: [] },
|
|
15629
|
+
seeds: { validated: 0, packs: [] },
|
|
15630
|
+
branding: "not-configured"
|
|
15631
|
+
};
|
|
15632
|
+
const existing = await client.listEvalTestCases();
|
|
15633
|
+
const existingByOverlayKey = new Map(
|
|
15634
|
+
existing.flatMap(
|
|
15635
|
+
(testCase) => (testCase.tags ?? []).filter((tag) => tag.startsWith("customer-overlay:key:")).map((tag) => [tag, testCase])
|
|
15636
|
+
)
|
|
15637
|
+
);
|
|
15638
|
+
for (const operation of plan.operations) {
|
|
15639
|
+
if (operation.kind === "eval-pack") {
|
|
15640
|
+
const packResult = {
|
|
15641
|
+
pack: operation.pack,
|
|
15642
|
+
inserted: 0,
|
|
15643
|
+
updated: 0,
|
|
15644
|
+
skipped: 0
|
|
15645
|
+
};
|
|
15646
|
+
for (const testCase of operation.testCases) {
|
|
15647
|
+
const input20 = withOverlayTags(
|
|
15648
|
+
operation.pack,
|
|
15649
|
+
testCase,
|
|
15650
|
+
client.targetAgentTemplateId
|
|
15651
|
+
);
|
|
15652
|
+
const existingCase = existingByOverlayKey.get(
|
|
15653
|
+
overlayKeyTag(operation.pack, testCase)
|
|
15654
|
+
);
|
|
15655
|
+
if (existingCase) {
|
|
15656
|
+
if (sameEval(existingCase, input20)) {
|
|
15657
|
+
result.evals.skipped++;
|
|
15658
|
+
packResult.skipped++;
|
|
15659
|
+
} else {
|
|
15660
|
+
await client.updateEvalTestCase(existingCase.id, input20);
|
|
15661
|
+
result.evals.updated++;
|
|
15662
|
+
packResult.updated++;
|
|
15663
|
+
}
|
|
15664
|
+
} else {
|
|
15665
|
+
await client.createEvalTestCase(input20);
|
|
15666
|
+
result.evals.inserted++;
|
|
15667
|
+
packResult.inserted++;
|
|
15668
|
+
}
|
|
15669
|
+
}
|
|
15670
|
+
result.evals.packs.push(packResult);
|
|
15671
|
+
continue;
|
|
15672
|
+
}
|
|
15673
|
+
if (operation.kind === "workspace-file-pack") {
|
|
15674
|
+
for (const file of operation.files) {
|
|
15675
|
+
await client.putWorkspaceFile({
|
|
15676
|
+
templateSlug: operation.targetTemplateSlug,
|
|
15677
|
+
path: workspaceTargetPath(
|
|
15678
|
+
operation.family,
|
|
15679
|
+
operation.pack,
|
|
15680
|
+
file.relativePath
|
|
15681
|
+
),
|
|
15682
|
+
content: file.content
|
|
15683
|
+
});
|
|
15684
|
+
result.workspaceFiles.written++;
|
|
15685
|
+
}
|
|
15686
|
+
result.workspaceFiles.packs.push({
|
|
15687
|
+
family: operation.family,
|
|
15688
|
+
pack: operation.pack,
|
|
15689
|
+
written: operation.files.length
|
|
15690
|
+
});
|
|
15691
|
+
continue;
|
|
15692
|
+
}
|
|
15693
|
+
if (operation.kind === "seed-pack") {
|
|
15694
|
+
result.seeds.validated++;
|
|
15695
|
+
result.seeds.packs.push(operation.pack);
|
|
15696
|
+
continue;
|
|
15697
|
+
}
|
|
15698
|
+
if (operation.kind === "branding") {
|
|
15699
|
+
result.branding = "recorded";
|
|
15700
|
+
}
|
|
15701
|
+
}
|
|
15702
|
+
return result;
|
|
15703
|
+
}
|
|
15704
|
+
function evalOperations(repoRoot, stage) {
|
|
15705
|
+
return stage.evalPacks.map((pack) => ({
|
|
15706
|
+
kind: "eval-pack",
|
|
15707
|
+
pack,
|
|
15708
|
+
testCases: readCustomerEvalPack(repoRoot, pack)
|
|
15709
|
+
}));
|
|
15710
|
+
}
|
|
15711
|
+
function workspaceFileOperations(repoRoot, stage) {
|
|
15712
|
+
return [
|
|
15713
|
+
...stage.skillPacks.map((pack) => ({
|
|
15714
|
+
kind: "workspace-file-pack",
|
|
15715
|
+
family: "skills",
|
|
15716
|
+
pack,
|
|
15717
|
+
targetTemplateSlug: stage.defaultAgentTemplateSlug,
|
|
15718
|
+
files: collectOverlayFiles(repoRoot, "skills", pack)
|
|
15719
|
+
})),
|
|
15720
|
+
...stage.workspaceDefaultPacks.map((pack) => ({
|
|
15721
|
+
kind: "workspace-file-pack",
|
|
15722
|
+
family: "workspace-defaults",
|
|
15723
|
+
pack,
|
|
15724
|
+
targetTemplateSlug: stage.defaultAgentTemplateSlug,
|
|
15725
|
+
files: collectOverlayFiles(repoRoot, "workspace-defaults", pack)
|
|
15726
|
+
}))
|
|
15727
|
+
];
|
|
15728
|
+
}
|
|
15729
|
+
function seedOperations(repoRoot, stage) {
|
|
15730
|
+
return stage.seedPacks.map((pack) => ({
|
|
15731
|
+
kind: "seed-pack",
|
|
15732
|
+
pack,
|
|
15733
|
+
payload: readJsonSeedPack(repoRoot, pack)
|
|
15734
|
+
}));
|
|
15735
|
+
}
|
|
15736
|
+
function workspaceTargetPath(family, pack, relativePath) {
|
|
15737
|
+
if (family === "skills") return `skills/${pack}/${relativePath}`;
|
|
15738
|
+
return relativePath;
|
|
15739
|
+
}
|
|
15740
|
+
function withOverlayTags(pack, testCase, defaultAgentTemplateId) {
|
|
15741
|
+
return {
|
|
15742
|
+
...testCase,
|
|
15743
|
+
agentTemplateId: testCase.agentTemplateId ?? defaultAgentTemplateId,
|
|
15744
|
+
tags: Array.from(
|
|
15745
|
+
/* @__PURE__ */ new Set([
|
|
15746
|
+
...testCase.tags ?? [],
|
|
15747
|
+
"source:customer-overlay",
|
|
15748
|
+
`customer-overlay:pack:${pack}`,
|
|
15749
|
+
overlayKeyTag(pack, testCase)
|
|
15750
|
+
])
|
|
15751
|
+
)
|
|
15752
|
+
};
|
|
15753
|
+
}
|
|
15754
|
+
function overlayKeyTag(pack, testCase) {
|
|
15755
|
+
return `customer-overlay:key:${pack}/${slugify(testCase.name)}`;
|
|
15756
|
+
}
|
|
15757
|
+
function sameEval(existing, next) {
|
|
15758
|
+
return existing.name === next.name && existing.category === next.category && existing.query === next.query && (existing.systemPrompt ?? null) === (next.systemPrompt ?? null) && JSON.stringify(existing.assertions ?? []) === JSON.stringify(next.assertions ?? []) && JSON.stringify(existing.agentcoreEvaluatorIds ?? []) === JSON.stringify(next.agentcoreEvaluatorIds ?? []) && JSON.stringify(existing.tags ?? []) === JSON.stringify(next.tags ?? []) && (existing.enabled ?? true) === (next.enabled ?? true);
|
|
15759
|
+
}
|
|
15760
|
+
function slugify(value) {
|
|
15761
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
15762
|
+
}
|
|
15763
|
+
|
|
15764
|
+
// src/commands/enterprise/overlay.ts
|
|
15765
|
+
function registerEnterpriseOverlayCommand(program2) {
|
|
15766
|
+
const overlay = program2.command("overlay").description(
|
|
15767
|
+
"Validate and apply customer overlay packs from a deployment repo."
|
|
15768
|
+
);
|
|
15769
|
+
overlay.command("plan").argument("[repoRoot]", "Deployment repository root", ".").description(
|
|
15770
|
+
"Validate customer/deployment.json and print the overlay plan."
|
|
15771
|
+
).option("-s, --stage <name>", "Deployment stage").option("-r, --region <region>", "AWS region", "us-east-1").action(
|
|
15772
|
+
(repoRoot, opts) => runEnterpriseOverlayPlan(repoRoot, opts)
|
|
15773
|
+
);
|
|
15774
|
+
overlay.command("apply").argument("[repoRoot]", "Deployment repository root", ".").description("Apply customer overlay packs to the deployed stage.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--dry-run", "Validate and print the apply plan without mutation").action(
|
|
15775
|
+
(repoRoot, opts) => runEnterpriseOverlayApply(repoRoot, opts)
|
|
15776
|
+
);
|
|
15777
|
+
}
|
|
15778
|
+
async function runEnterpriseOverlayPlan(repoRoot, opts) {
|
|
15779
|
+
const plan = buildEnterpriseOverlayPlan({
|
|
15780
|
+
repoRoot: resolve6(repoRoot),
|
|
15781
|
+
stage: resolveOverlayStage(opts)
|
|
15782
|
+
});
|
|
15783
|
+
printJson(publicPlan(plan));
|
|
15784
|
+
}
|
|
15785
|
+
async function runEnterpriseOverlayApply(repoRoot, opts) {
|
|
15786
|
+
const stage = resolveOverlayStage(opts);
|
|
15787
|
+
const plan = buildEnterpriseOverlayPlan({
|
|
15788
|
+
repoRoot: resolve6(repoRoot),
|
|
15789
|
+
stage
|
|
15790
|
+
});
|
|
15791
|
+
if (opts.dryRun) {
|
|
15792
|
+
printJson({ dryRun: true, plan: publicPlan(plan) });
|
|
15793
|
+
return;
|
|
15794
|
+
}
|
|
15795
|
+
const client = await createOverlayApiClient(plan, opts);
|
|
15796
|
+
const result = await applyEnterpriseOverlay(plan, client);
|
|
15797
|
+
if (isJsonMode()) {
|
|
15798
|
+
printJson(result);
|
|
15799
|
+
return;
|
|
15800
|
+
}
|
|
15801
|
+
printSuccess(
|
|
15802
|
+
`Applied overlay for ${plan.tenantSlug}: ${result.evals.inserted} eval(s) inserted, ${result.evals.updated} updated, ${result.workspaceFiles.written} workspace file(s) written.`
|
|
15803
|
+
);
|
|
15804
|
+
}
|
|
15805
|
+
async function createOverlayApiClient(plan, opts) {
|
|
15806
|
+
const region = opts.region ?? "us-east-1";
|
|
15807
|
+
const ctx = await resolveEvalContext({
|
|
15808
|
+
stage: plan.stage,
|
|
15809
|
+
region,
|
|
15810
|
+
tenant: opts.tenant ?? plan.tenantSlug
|
|
15811
|
+
});
|
|
15812
|
+
const templateId = await resolveTemplateId(
|
|
15813
|
+
ctx.client,
|
|
15814
|
+
ctx.tenantId,
|
|
15815
|
+
plan.targetTemplateSlug
|
|
15816
|
+
);
|
|
15817
|
+
const auth = await resolveAuth({ stage: plan.stage, region });
|
|
15818
|
+
const apiUrl = getApiEndpoint(plan.stage, region);
|
|
15819
|
+
if (!apiUrl) {
|
|
15820
|
+
printError(
|
|
15821
|
+
`Cannot discover API endpoint for stage "${plan.stage}" in ${region}.`
|
|
15822
|
+
);
|
|
15823
|
+
process.exit(1);
|
|
15824
|
+
}
|
|
15825
|
+
return {
|
|
15826
|
+
targetAgentTemplateId: templateId,
|
|
15827
|
+
async listEvalTestCases() {
|
|
15828
|
+
const data = await gqlQuery(ctx.client, EvalTestCasesDoc, {
|
|
15829
|
+
tenantId: ctx.tenantId,
|
|
15830
|
+
category: null,
|
|
15831
|
+
search: null
|
|
15832
|
+
});
|
|
15833
|
+
return data.evalTestCases;
|
|
15834
|
+
},
|
|
15835
|
+
async createEvalTestCase(input20) {
|
|
15836
|
+
await gqlMutate(ctx.client, CreateEvalTestCaseDoc, {
|
|
15837
|
+
tenantId: ctx.tenantId,
|
|
15838
|
+
input: evalInput(input20)
|
|
15839
|
+
});
|
|
15840
|
+
},
|
|
15841
|
+
async updateEvalTestCase(id, input20) {
|
|
15842
|
+
await gqlMutate(ctx.client, UpdateEvalTestCaseDoc, {
|
|
15843
|
+
id,
|
|
15844
|
+
input: evalInput(input20)
|
|
15845
|
+
});
|
|
15846
|
+
},
|
|
15847
|
+
async putWorkspaceFile(input20) {
|
|
15848
|
+
const response = await fetch(
|
|
15849
|
+
`${apiUrl.replace(/\/+$/, "")}/api/workspaces/files`,
|
|
15850
|
+
{
|
|
15851
|
+
method: "POST",
|
|
15852
|
+
headers: {
|
|
15853
|
+
"content-type": "application/json",
|
|
15854
|
+
...auth.headers,
|
|
15855
|
+
"x-tenant-id": ctx.tenantId
|
|
15856
|
+
},
|
|
15857
|
+
body: JSON.stringify({
|
|
15858
|
+
action: "put",
|
|
15859
|
+
templateId,
|
|
15860
|
+
path: input20.path,
|
|
15861
|
+
content: input20.content
|
|
15862
|
+
})
|
|
15863
|
+
}
|
|
15864
|
+
);
|
|
15865
|
+
if (!response.ok) {
|
|
15866
|
+
const text = await response.text();
|
|
15867
|
+
throw new Error(
|
|
15868
|
+
`PUT ${input20.path} failed: ${response.status} ${text || response.statusText}`
|
|
15869
|
+
);
|
|
15870
|
+
}
|
|
15871
|
+
}
|
|
15872
|
+
};
|
|
15873
|
+
}
|
|
15874
|
+
async function resolveTemplateId(client, tenantId, templateSlug) {
|
|
15875
|
+
const data = await gqlQuery(client, AgentTemplatesForEvalDoc, { tenantId });
|
|
15876
|
+
const template = data.agentTemplates.find(
|
|
15877
|
+
(item) => item.slug === templateSlug
|
|
15878
|
+
);
|
|
15879
|
+
if (!template) {
|
|
15880
|
+
throw new Error(
|
|
15881
|
+
`Agent template "${templateSlug}" not found for tenant ${tenantId}`
|
|
15882
|
+
);
|
|
15883
|
+
}
|
|
15884
|
+
return template.id;
|
|
15885
|
+
}
|
|
15886
|
+
function evalInput(input20) {
|
|
15887
|
+
return {
|
|
15888
|
+
name: input20.name,
|
|
15889
|
+
category: input20.category,
|
|
15890
|
+
query: input20.query,
|
|
15891
|
+
systemPrompt: input20.systemPrompt ?? null,
|
|
15892
|
+
agentTemplateId: input20.agentTemplateId ?? null,
|
|
15893
|
+
assertions: input20.assertions,
|
|
15894
|
+
agentcoreEvaluatorIds: input20.agentcoreEvaluatorIds ?? [],
|
|
15895
|
+
tags: input20.tags ?? [],
|
|
15896
|
+
enabled: input20.enabled ?? true
|
|
15897
|
+
};
|
|
15898
|
+
}
|
|
15899
|
+
function publicPlan(plan) {
|
|
15900
|
+
return {
|
|
15901
|
+
stage: plan.stage,
|
|
15902
|
+
tenantSlug: plan.tenantSlug,
|
|
15903
|
+
targetTemplateSlug: plan.targetTemplateSlug,
|
|
15904
|
+
operations: plan.operations.map((operation) => {
|
|
15905
|
+
if (operation.kind === "eval-pack") {
|
|
15906
|
+
return {
|
|
15907
|
+
kind: operation.kind,
|
|
15908
|
+
pack: operation.pack,
|
|
15909
|
+
testCases: operation.testCases.length
|
|
15910
|
+
};
|
|
15911
|
+
}
|
|
15912
|
+
if (operation.kind === "workspace-file-pack") {
|
|
15913
|
+
return {
|
|
15914
|
+
kind: operation.kind,
|
|
15915
|
+
family: operation.family,
|
|
15916
|
+
pack: operation.pack,
|
|
15917
|
+
targetTemplateSlug: operation.targetTemplateSlug,
|
|
15918
|
+
files: operation.files.length
|
|
15919
|
+
};
|
|
15920
|
+
}
|
|
15921
|
+
if (operation.kind === "seed-pack") {
|
|
15922
|
+
return {
|
|
15923
|
+
kind: operation.kind,
|
|
15924
|
+
pack: operation.pack,
|
|
15925
|
+
status: "validated"
|
|
15926
|
+
};
|
|
15927
|
+
}
|
|
15928
|
+
return { kind: operation.kind, status: "recorded" };
|
|
15929
|
+
})
|
|
15930
|
+
};
|
|
15931
|
+
}
|
|
15932
|
+
function resolveOverlayStage(opts) {
|
|
15933
|
+
const stage = opts.stage ?? process.env.STAGE;
|
|
15934
|
+
if (!stage) {
|
|
15935
|
+
throw new Error("Deployment stage is required. Pass --stage or set STAGE.");
|
|
15936
|
+
}
|
|
15937
|
+
return stage;
|
|
15938
|
+
}
|
|
15939
|
+
|
|
15940
|
+
// src/commands/enterprise.ts
|
|
15941
|
+
function registerEnterpriseCommand(program2) {
|
|
15942
|
+
const enterprise = program2.command("enterprise").description(
|
|
15943
|
+
"Bootstrap and operate customer-owned ThinkWork enterprise deployment repositories."
|
|
15944
|
+
);
|
|
15945
|
+
registerEnterpriseBootstrapCommand(enterprise);
|
|
15946
|
+
registerEnterpriseOverlayCommand(enterprise);
|
|
15947
|
+
}
|
|
15948
|
+
|
|
14396
15949
|
// src/cli.ts
|
|
14397
15950
|
var program = new Command();
|
|
14398
15951
|
program.name("thinkwork").description(
|
|
@@ -14458,6 +16011,7 @@ registerPerformanceCommand(program);
|
|
|
14458
16011
|
registerTraceCommand(program);
|
|
14459
16012
|
registerDashboardCommand(program);
|
|
14460
16013
|
registerEvalCommand(program);
|
|
16014
|
+
registerEnterpriseCommand(program);
|
|
14461
16015
|
registerWikiCommand(program);
|
|
14462
16016
|
for (const cmd of program.commands) {
|
|
14463
16017
|
if (!cmd.options.some((o) => o.long === "--json")) {
|