thinkwork-cli 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/dist/cli.js +1187 -315
- package/dist/terraform/examples/greenfield/main.tf +325 -19
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +14 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +61 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +54 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +197 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +70 -0
- package/dist/terraform/modules/app/agentcore-flue/README.md +58 -0
- package/dist/terraform/modules/app/agentcore-flue/main.tf +322 -0
- package/dist/terraform/modules/app/agentcore-flue/outputs.tf +23 -0
- package/dist/terraform/modules/app/agentcore-flue/variables.tf +91 -0
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +0 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +165 -0
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +4 -0
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +5 -0
- package/dist/terraform/modules/app/computer-runtime/README.md +15 -0
- package/dist/terraform/modules/app/computer-runtime/main.tf +406 -0
- package/dist/terraform/modules/app/computer-runtime/outputs.tf +75 -0
- package/dist/terraform/modules/app/computer-runtime/variables.tf +66 -0
- package/dist/terraform/modules/app/hindsight-memory/main.tf +6 -0
- package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +128 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +1454 -43
- package/dist/terraform/modules/app/lambda-api/main.tf +221 -12
- package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +118 -0
- package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +49 -0
- package/dist/terraform/modules/app/lambda-api/outputs.tf +38 -0
- package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +43 -0
- package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +53 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +349 -2
- package/dist/terraform/modules/app/lambda-api/workspace-events.tf +125 -0
- package/dist/terraform/modules/app/routines-stepfunctions/main.tf +453 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +66 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +200 -0
- package/dist/terraform/modules/app/static-site/main.tf +146 -5
- package/dist/terraform/modules/app/www-dns/main.tf +118 -15
- package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
- package/dist/terraform/modules/app/www-dns/variables.tf +42 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +164 -3
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +34 -0
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +16 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/README.md +145 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/main.tf +573 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/outputs.tf +43 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/variables.tf +93 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +269 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/outputs.tf +23 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +50 -0
- package/dist/terraform/modules/data/s3-backups-bucket/main.tf +123 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +13 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +2 -2
- package/dist/terraform/modules/thinkwork/main.tf +439 -21
- package/dist/terraform/modules/thinkwork/outputs.tf +121 -0
- package/dist/terraform/modules/thinkwork/variables.tf +153 -2
- package/dist/terraform/schema.graphql +17 -0
- package/package.json +15 -14
package/dist/cli.js
CHANGED
|
@@ -196,7 +196,7 @@ async function ensureWorkspace(cwd, stage) {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
function runTerraformRaw(cwd, args) {
|
|
199
|
-
return new Promise((
|
|
199
|
+
return new Promise((resolve4, reject) => {
|
|
200
200
|
const proc = spawn("terraform", args, {
|
|
201
201
|
cwd,
|
|
202
202
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -206,13 +206,13 @@ function runTerraformRaw(cwd, args) {
|
|
|
206
206
|
proc.stdout.on("data", (d) => stdout += d);
|
|
207
207
|
proc.stderr.on("data", (d) => stderr += d);
|
|
208
208
|
proc.on("close", (code) => {
|
|
209
|
-
if (code === 0)
|
|
209
|
+
if (code === 0) resolve4(stdout);
|
|
210
210
|
else reject(new Error(`terraform ${args.join(" ")} failed (exit ${code}): ${stderr}`));
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
213
|
}
|
|
214
214
|
function runTerraform(cwd, args) {
|
|
215
|
-
return new Promise((
|
|
215
|
+
return new Promise((resolve4) => {
|
|
216
216
|
console.log(`
|
|
217
217
|
\u2192 terraform ${args.join(" ")}
|
|
218
218
|
`);
|
|
@@ -220,7 +220,7 @@ function runTerraform(cwd, args) {
|
|
|
220
220
|
cwd,
|
|
221
221
|
stdio: "inherit"
|
|
222
222
|
});
|
|
223
|
-
proc.on("close", (code) =>
|
|
223
|
+
proc.on("close", (code) => resolve4(code ?? 1));
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
226
|
async function ensureInit(cwd) {
|
|
@@ -427,6 +427,12 @@ function registerPlanCommand(program2) {
|
|
|
427
427
|
});
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
// src/commands/deploy.ts
|
|
431
|
+
import { spawn as spawn2 } from "child_process";
|
|
432
|
+
import { existsSync as existsSync3 } from "fs";
|
|
433
|
+
import { dirname as dirname2, resolve as pathResolve } from "path";
|
|
434
|
+
import { fileURLToPath } from "url";
|
|
435
|
+
|
|
430
436
|
// src/prompt.ts
|
|
431
437
|
import { createInterface } from "readline";
|
|
432
438
|
async function confirm(message) {
|
|
@@ -434,69 +440,125 @@ async function confirm(message) {
|
|
|
434
440
|
input: process.stdin,
|
|
435
441
|
output: process.stdout
|
|
436
442
|
});
|
|
437
|
-
return new Promise((
|
|
443
|
+
return new Promise((resolve4) => {
|
|
438
444
|
rl.question(`${message} [y/N] `, (answer) => {
|
|
439
445
|
rl.close();
|
|
440
|
-
|
|
446
|
+
resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
441
447
|
});
|
|
442
448
|
});
|
|
443
449
|
}
|
|
444
450
|
|
|
445
451
|
// src/commands/deploy.ts
|
|
446
452
|
function registerDeployCommand(program2) {
|
|
447
|
-
program2.command("deploy").description(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const ok = await confirm(` Stage "${stage}" is production-like. Deploy?`);
|
|
463
|
-
if (!ok) {
|
|
464
|
-
console.log(" Aborted.");
|
|
465
|
-
process.exit(0);
|
|
453
|
+
program2.command("deploy").description(
|
|
454
|
+
"Run terraform apply for a stage. Prompts for stage in a TTY when omitted."
|
|
455
|
+
).option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option(
|
|
456
|
+
"-c, --component <tier>",
|
|
457
|
+
"Component tier (foundation|data|app|all)",
|
|
458
|
+
"all"
|
|
459
|
+
).option("-y, --yes", "Skip interactive confirmation (for CI)").action(
|
|
460
|
+
async (opts) => {
|
|
461
|
+
const startTime = Date.now();
|
|
462
|
+
try {
|
|
463
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
464
|
+
const compCheck = validateComponent(opts.component);
|
|
465
|
+
if (!compCheck.valid) {
|
|
466
|
+
printError(compCheck.error);
|
|
467
|
+
process.exit(1);
|
|
466
468
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (!
|
|
470
|
-
|
|
471
|
-
|
|
469
|
+
const identity = getAwsIdentity();
|
|
470
|
+
printHeader("deploy", stage, identity);
|
|
471
|
+
if (!identity) {
|
|
472
|
+
printWarning(
|
|
473
|
+
"Could not resolve AWS identity. Is the AWS CLI configured?"
|
|
474
|
+
);
|
|
472
475
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
476
|
+
if (isProdLike(stage) && !opts.yes) {
|
|
477
|
+
const ok = await confirm(
|
|
478
|
+
` Stage "${stage}" is production-like. Deploy?`
|
|
479
|
+
);
|
|
480
|
+
if (!ok) {
|
|
481
|
+
console.log(" Aborted.");
|
|
482
|
+
process.exit(0);
|
|
483
|
+
}
|
|
484
|
+
} else if (!opts.yes) {
|
|
485
|
+
const ok = await confirm(` Deploy to stage "${stage}"?`);
|
|
486
|
+
if (!ok) {
|
|
487
|
+
console.log(" Aborted.");
|
|
488
|
+
process.exit(0);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
492
|
+
const tiers = expandComponent(opts.component);
|
|
493
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
494
|
+
const tier = tiers[i];
|
|
495
|
+
printTierHeader(tier, i, tiers.length);
|
|
496
|
+
const cwd = resolveTierDir(terraformDir, stage, tier);
|
|
497
|
+
await ensureInit(cwd);
|
|
498
|
+
await ensureWorkspace(cwd, stage);
|
|
499
|
+
const code = await runTerraform(cwd, [
|
|
500
|
+
"apply",
|
|
501
|
+
"-auto-approve",
|
|
502
|
+
`-var=stage=${stage}`
|
|
503
|
+
]);
|
|
504
|
+
if (code !== 0) {
|
|
505
|
+
printError(`Deploy failed for ${tier} (exit ${code})`);
|
|
506
|
+
process.exit(code);
|
|
507
|
+
}
|
|
490
508
|
}
|
|
509
|
+
printSuccess("Deploy complete");
|
|
510
|
+
await runPostDeployProbe(stage);
|
|
511
|
+
printSummary("deploy", stage, tiers, startTime);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
if (isCancellation(err)) return;
|
|
514
|
+
throw err;
|
|
491
515
|
}
|
|
492
|
-
printSuccess("Deploy complete");
|
|
493
|
-
printSummary("deploy", stage, tiers, startTime);
|
|
494
|
-
} catch (err) {
|
|
495
|
-
if (isCancellation(err)) return;
|
|
496
|
-
throw err;
|
|
497
516
|
}
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
async function runPostDeployProbe(stage) {
|
|
520
|
+
const scriptPath = locatePostDeployScript();
|
|
521
|
+
if (!scriptPath) {
|
|
522
|
+
printWarning(
|
|
523
|
+
"post-deploy probe script not found \u2014 skipping AgentCore drift check"
|
|
524
|
+
);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
await new Promise((resolve4) => {
|
|
528
|
+
const proc = spawn2("bash", [scriptPath, "--stage", stage], {
|
|
529
|
+
stdio: "inherit",
|
|
530
|
+
env: process.env
|
|
531
|
+
});
|
|
532
|
+
proc.on("close", (code) => {
|
|
533
|
+
if (code !== 0) {
|
|
534
|
+
printWarning(
|
|
535
|
+
`post-deploy probe exited ${code} \u2014 deploy not rolled back`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
resolve4();
|
|
539
|
+
});
|
|
540
|
+
proc.on("error", (err) => {
|
|
541
|
+
printWarning(`post-deploy probe spawn failed: ${err.message}`);
|
|
542
|
+
resolve4();
|
|
543
|
+
});
|
|
498
544
|
});
|
|
499
545
|
}
|
|
546
|
+
function locatePostDeployScript() {
|
|
547
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
548
|
+
const candidates = [
|
|
549
|
+
pathResolve(here, "..", "..", "..", "..", "scripts", "post-deploy.sh"),
|
|
550
|
+
pathResolve(process.cwd(), "scripts", "post-deploy.sh"),
|
|
551
|
+
pathResolve(
|
|
552
|
+
process.env.THINKWORK_TERRAFORM_DIR || ".",
|
|
553
|
+
"scripts",
|
|
554
|
+
"post-deploy.sh"
|
|
555
|
+
)
|
|
556
|
+
];
|
|
557
|
+
for (const candidate of candidates) {
|
|
558
|
+
if (existsSync3(candidate)) return candidate;
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
500
562
|
|
|
501
563
|
// src/commands/destroy.ts
|
|
502
564
|
function registerDestroyCommand(program2) {
|
|
@@ -687,17 +749,17 @@ function registerOutputsCommand(program2) {
|
|
|
687
749
|
}
|
|
688
750
|
|
|
689
751
|
// src/commands/config.ts
|
|
690
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as
|
|
752
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
|
|
691
753
|
import chalk4 from "chalk";
|
|
692
754
|
|
|
693
755
|
// src/environments.ts
|
|
694
|
-
import { existsSync as
|
|
756
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
695
757
|
import { join as join2 } from "path";
|
|
696
758
|
import { homedir as homedir2 } from "os";
|
|
697
759
|
var THINKWORK_HOME = join2(homedir2(), ".thinkwork");
|
|
698
760
|
var ENVIRONMENTS_DIR = join2(THINKWORK_HOME, "environments");
|
|
699
761
|
function ensureDir(dir) {
|
|
700
|
-
if (!
|
|
762
|
+
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
701
763
|
}
|
|
702
764
|
function saveEnvironment(config) {
|
|
703
765
|
ensureDir(ENVIRONMENTS_DIR);
|
|
@@ -710,13 +772,13 @@ function saveEnvironment(config) {
|
|
|
710
772
|
}
|
|
711
773
|
function loadEnvironment(stage) {
|
|
712
774
|
const configPath = join2(ENVIRONMENTS_DIR, stage, "config.json");
|
|
713
|
-
if (!
|
|
775
|
+
if (!existsSync4(configPath)) return null;
|
|
714
776
|
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
715
777
|
}
|
|
716
778
|
function listEnvironments() {
|
|
717
|
-
if (!
|
|
779
|
+
if (!existsSync4(ENVIRONMENTS_DIR)) return [];
|
|
718
780
|
return readdirSync(ENVIRONMENTS_DIR).filter((name) => {
|
|
719
|
-
return
|
|
781
|
+
return existsSync4(join2(ENVIRONMENTS_DIR, name, "config.json"));
|
|
720
782
|
}).map((name) => {
|
|
721
783
|
return JSON.parse(
|
|
722
784
|
readFileSync2(join2(ENVIRONMENTS_DIR, name, "config.json"), "utf-8")
|
|
@@ -725,19 +787,19 @@ function listEnvironments() {
|
|
|
725
787
|
}
|
|
726
788
|
function resolveTerraformDir(stage) {
|
|
727
789
|
const env = loadEnvironment(stage);
|
|
728
|
-
if (env?.terraformDir &&
|
|
790
|
+
if (env?.terraformDir && existsSync4(env.terraformDir)) {
|
|
729
791
|
return env.terraformDir;
|
|
730
792
|
}
|
|
731
793
|
const envVar = process.env.THINKWORK_TERRAFORM_DIR;
|
|
732
|
-
if (envVar &&
|
|
794
|
+
if (envVar && existsSync4(envVar)) return envVar;
|
|
733
795
|
const cwdTf = join2(process.cwd(), "terraform");
|
|
734
|
-
if (
|
|
796
|
+
if (existsSync4(join2(cwdTf, "main.tf"))) return cwdTf;
|
|
735
797
|
return null;
|
|
736
798
|
}
|
|
737
799
|
|
|
738
800
|
// src/commands/config.ts
|
|
739
801
|
function readTfVar(tfvarsPath, key) {
|
|
740
|
-
if (!
|
|
802
|
+
if (!existsSync5(tfvarsPath)) return null;
|
|
741
803
|
const content = readFileSync3(tfvarsPath, "utf-8");
|
|
742
804
|
const quoted = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
|
|
743
805
|
if (quoted) return quoted[1];
|
|
@@ -746,7 +808,7 @@ function readTfVar(tfvarsPath, key) {
|
|
|
746
808
|
}
|
|
747
809
|
var BARE_KEYS = /* @__PURE__ */ new Set(["enable_hindsight"]);
|
|
748
810
|
function setTfVar(tfvarsPath, key, value) {
|
|
749
|
-
if (!
|
|
811
|
+
if (!existsSync5(tfvarsPath)) {
|
|
750
812
|
throw new Error(`terraform.tfvars not found at ${tfvarsPath}`);
|
|
751
813
|
}
|
|
752
814
|
let content = readFileSync3(tfvarsPath, "utf-8");
|
|
@@ -766,7 +828,7 @@ function resolveTfvarsPath(stage) {
|
|
|
766
828
|
const tfDir = resolveTerraformDir(stage);
|
|
767
829
|
if (tfDir) {
|
|
768
830
|
const direct = `${tfDir}/terraform.tfvars`;
|
|
769
|
-
if (
|
|
831
|
+
if (existsSync5(direct)) return direct;
|
|
770
832
|
}
|
|
771
833
|
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
772
834
|
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
@@ -793,7 +855,7 @@ function registerConfigCommand(program2) {
|
|
|
793
855
|
console.log(` ${chalk4.bold("Updated:")} ${env.updatedAt}`);
|
|
794
856
|
console.log(chalk4.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
795
857
|
const tfvarsPath = `${env.terraformDir}/terraform.tfvars`;
|
|
796
|
-
if (
|
|
858
|
+
if (existsSync5(tfvarsPath)) {
|
|
797
859
|
console.log("");
|
|
798
860
|
console.log(chalk4.dim(" terraform.tfvars:"));
|
|
799
861
|
const content = readFileSync3(tfvarsPath, "utf-8");
|
|
@@ -914,28 +976,28 @@ function registerConfigCommand(program2) {
|
|
|
914
976
|
}
|
|
915
977
|
|
|
916
978
|
// src/commands/bootstrap.ts
|
|
917
|
-
import { spawn as
|
|
979
|
+
import { spawn as spawn3 } from "child_process";
|
|
918
980
|
import { resolve } from "path";
|
|
919
981
|
function getTerraformOutput(cwd, key) {
|
|
920
|
-
return new Promise((
|
|
921
|
-
const proc =
|
|
982
|
+
return new Promise((resolve4, reject) => {
|
|
983
|
+
const proc = spawn3("terraform", ["output", "-raw", key], {
|
|
922
984
|
cwd,
|
|
923
985
|
stdio: ["pipe", "pipe", "pipe"]
|
|
924
986
|
});
|
|
925
987
|
let stdout = "";
|
|
926
988
|
proc.stdout.on("data", (d) => stdout += d);
|
|
927
989
|
proc.on("close", (code) => {
|
|
928
|
-
if (code === 0)
|
|
990
|
+
if (code === 0) resolve4(stdout.trim());
|
|
929
991
|
else reject(new Error(`terraform output ${key} failed (exit ${code})`));
|
|
930
992
|
});
|
|
931
993
|
});
|
|
932
994
|
}
|
|
933
995
|
function runScript(scriptPath, args) {
|
|
934
|
-
return new Promise((
|
|
935
|
-
const proc =
|
|
996
|
+
return new Promise((resolve4) => {
|
|
997
|
+
const proc = spawn3("bash", [scriptPath, ...args], {
|
|
936
998
|
stdio: "inherit"
|
|
937
999
|
});
|
|
938
|
-
proc.on("close", (code) =>
|
|
1000
|
+
proc.on("close", (code) => resolve4(code ?? 1));
|
|
939
1001
|
});
|
|
940
1002
|
}
|
|
941
1003
|
function registerBootstrapCommand(program2) {
|
|
@@ -990,7 +1052,7 @@ import { select as select2, Separator } from "@inquirer/prompts";
|
|
|
990
1052
|
import chalk7 from "chalk";
|
|
991
1053
|
|
|
992
1054
|
// src/aws-profiles.ts
|
|
993
|
-
import { existsSync as
|
|
1055
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
994
1056
|
import { homedir as homedir3 } from "os";
|
|
995
1057
|
import { join as join3 } from "path";
|
|
996
1058
|
var CREDENTIALS_PATH = join3(homedir3(), ".aws", "credentials");
|
|
@@ -1033,7 +1095,7 @@ function classify(fields) {
|
|
|
1033
1095
|
}
|
|
1034
1096
|
function listAwsProfiles() {
|
|
1035
1097
|
const byName = /* @__PURE__ */ new Map();
|
|
1036
|
-
if (
|
|
1098
|
+
if (existsSync6(CREDENTIALS_PATH)) {
|
|
1037
1099
|
const sections = parseIni(readFileSync4(CREDENTIALS_PATH, "utf-8"));
|
|
1038
1100
|
for (const [section, fields] of Object.entries(sections)) {
|
|
1039
1101
|
byName.set(section, {
|
|
@@ -1043,7 +1105,7 @@ function listAwsProfiles() {
|
|
|
1043
1105
|
});
|
|
1044
1106
|
}
|
|
1045
1107
|
}
|
|
1046
|
-
if (
|
|
1108
|
+
if (existsSync6(CONFIG_PATH)) {
|
|
1047
1109
|
const sections = parseIni(readFileSync4(CONFIG_PATH, "utf-8"));
|
|
1048
1110
|
for (const [section, fields] of Object.entries(sections)) {
|
|
1049
1111
|
const name = normalizeConfigSection(section);
|
|
@@ -1271,7 +1333,7 @@ function discoverCognitoConfig(stage, region) {
|
|
|
1271
1333
|
// src/cognito-oauth.ts
|
|
1272
1334
|
import { createServer } from "http";
|
|
1273
1335
|
import { randomBytes } from "crypto";
|
|
1274
|
-
import { spawn as
|
|
1336
|
+
import { spawn as spawn4 } from "child_process";
|
|
1275
1337
|
import chalk6 from "chalk";
|
|
1276
1338
|
var CLI_LOOPBACK_PORT = 42010;
|
|
1277
1339
|
var CALLBACK_PATH = "/callback";
|
|
@@ -1310,7 +1372,7 @@ function buildAuthorizeUrl(cognito, redirectUri, state) {
|
|
|
1310
1372
|
return `${cognito.domainUrl}/oauth2/authorize?${params.toString()}`;
|
|
1311
1373
|
}
|
|
1312
1374
|
function waitForCallbackCode(opts) {
|
|
1313
|
-
return new Promise((
|
|
1375
|
+
return new Promise((resolve4, reject) => {
|
|
1314
1376
|
const server = createServer((req, res) => handleRequest(req, res));
|
|
1315
1377
|
let finished = false;
|
|
1316
1378
|
const finish = (err, code) => {
|
|
@@ -1321,7 +1383,7 @@ function waitForCallbackCode(opts) {
|
|
|
1321
1383
|
closer.closeAllConnections?.();
|
|
1322
1384
|
server.close(() => {
|
|
1323
1385
|
if (err) reject(err);
|
|
1324
|
-
else
|
|
1386
|
+
else resolve4(code);
|
|
1325
1387
|
});
|
|
1326
1388
|
};
|
|
1327
1389
|
const timer = setTimeout(() => {
|
|
@@ -1450,7 +1512,7 @@ function openInBrowser(url) {
|
|
|
1450
1512
|
const cmd = platform2 === "darwin" ? "open" : platform2 === "win32" ? "cmd" : "xdg-open";
|
|
1451
1513
|
const args = platform2 === "win32" ? ["/c", "start", "", url] : [url];
|
|
1452
1514
|
try {
|
|
1453
|
-
|
|
1515
|
+
spawn4(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
1454
1516
|
} catch {
|
|
1455
1517
|
}
|
|
1456
1518
|
}
|
|
@@ -1512,10 +1574,10 @@ function escapeHtml(s) {
|
|
|
1512
1574
|
// src/commands/login.ts
|
|
1513
1575
|
function ask(prompt) {
|
|
1514
1576
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1515
|
-
return new Promise((
|
|
1577
|
+
return new Promise((resolve4) => {
|
|
1516
1578
|
rl.question(prompt, (answer) => {
|
|
1517
1579
|
rl.close();
|
|
1518
|
-
|
|
1580
|
+
resolve4(answer.trim());
|
|
1519
1581
|
});
|
|
1520
1582
|
});
|
|
1521
1583
|
}
|
|
@@ -1994,20 +2056,20 @@ Examples:
|
|
|
1994
2056
|
}
|
|
1995
2057
|
|
|
1996
2058
|
// src/commands/init.ts
|
|
1997
|
-
import { existsSync as
|
|
1998
|
-
import { resolve as resolve2, join as join5, dirname as
|
|
2059
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, cpSync } from "fs";
|
|
2060
|
+
import { resolve as resolve2, join as join5, dirname as dirname3 } from "path";
|
|
1999
2061
|
import { execSync as execSync7 } from "child_process";
|
|
2000
|
-
import { fileURLToPath } from "url";
|
|
2062
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2001
2063
|
import { createInterface as createInterface3 } from "readline";
|
|
2002
2064
|
import chalk8 from "chalk";
|
|
2003
|
-
var __dirname =
|
|
2065
|
+
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
2004
2066
|
function ask2(prompt, defaultVal = "") {
|
|
2005
2067
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2006
2068
|
const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
|
|
2007
|
-
return new Promise((
|
|
2069
|
+
return new Promise((resolve4) => {
|
|
2008
2070
|
rl.question(` ${prompt}${suffix}: `, (answer) => {
|
|
2009
2071
|
rl.close();
|
|
2010
|
-
|
|
2072
|
+
resolve4(answer.trim() || defaultVal);
|
|
2011
2073
|
});
|
|
2012
2074
|
});
|
|
2013
2075
|
}
|
|
@@ -2025,9 +2087,9 @@ function generateSecret(length = 32) {
|
|
|
2025
2087
|
}
|
|
2026
2088
|
function findBundledTerraform() {
|
|
2027
2089
|
const bundled = resolve2(__dirname, "terraform");
|
|
2028
|
-
if (
|
|
2090
|
+
if (existsSync8(join5(bundled, "modules"))) return bundled;
|
|
2029
2091
|
const repoTf = resolve2(__dirname, "..", "..", "..", "terraform");
|
|
2030
|
-
if (
|
|
2092
|
+
if (existsSync8(join5(repoTf, "modules"))) return repoTf;
|
|
2031
2093
|
throw new Error(
|
|
2032
2094
|
"Terraform modules not found. The CLI package may be incomplete.\nTry reinstalling: npm install -g thinkwork-cli@latest"
|
|
2033
2095
|
);
|
|
@@ -2087,9 +2149,9 @@ function registerInitCommand(program2) {
|
|
|
2087
2149
|
printError("Stage name is required. Pass -s <name> or re-run in an interactive terminal.");
|
|
2088
2150
|
process.exit(1);
|
|
2089
2151
|
}
|
|
2090
|
-
const { input:
|
|
2152
|
+
const { input: input4 } = await import("@inquirer/prompts");
|
|
2091
2153
|
try {
|
|
2092
|
-
stage = await
|
|
2154
|
+
stage = await input4({
|
|
2093
2155
|
message: "Stage name (e.g. dev, staging, prod):",
|
|
2094
2156
|
validate: (v) => validateStage(v).error ?? true
|
|
2095
2157
|
});
|
|
@@ -2119,7 +2181,7 @@ function registerInitCommand(program2) {
|
|
|
2119
2181
|
const targetDir = resolve2(opts.dir);
|
|
2120
2182
|
const tfDir = join5(targetDir, "terraform");
|
|
2121
2183
|
const tfvarsPath = join5(tfDir, "terraform.tfvars");
|
|
2122
|
-
if (
|
|
2184
|
+
if (existsSync8(tfvarsPath)) {
|
|
2123
2185
|
printWarning(`terraform.tfvars already exists at ${tfvarsPath}`);
|
|
2124
2186
|
const overwrite = await ask2("Overwrite?", "N");
|
|
2125
2187
|
if (overwrite.toLowerCase() !== "y") {
|
|
@@ -2189,18 +2251,18 @@ function registerInitCommand(program2) {
|
|
|
2189
2251
|
for (const dir of copyDirs) {
|
|
2190
2252
|
const src = join5(bundledTf, dir);
|
|
2191
2253
|
const dst = join5(tfDir, dir);
|
|
2192
|
-
if (
|
|
2254
|
+
if (existsSync8(src) && !existsSync8(dst)) {
|
|
2193
2255
|
cpSync(src, dst, { recursive: true });
|
|
2194
2256
|
}
|
|
2195
2257
|
}
|
|
2196
2258
|
const schemaPath = join5(bundledTf, "schema.graphql");
|
|
2197
|
-
if (
|
|
2259
|
+
if (existsSync8(schemaPath) && !existsSync8(join5(tfDir, "schema.graphql"))) {
|
|
2198
2260
|
cpSync(schemaPath, join5(tfDir, "schema.graphql"));
|
|
2199
2261
|
}
|
|
2200
2262
|
const tfvars = buildTfvars(config);
|
|
2201
2263
|
writeFileSync4(tfvarsPath, tfvars);
|
|
2202
2264
|
const mainTfPath = join5(tfDir, "main.tf");
|
|
2203
|
-
if (!
|
|
2265
|
+
if (!existsSync8(mainTfPath)) {
|
|
2204
2266
|
writeFileSync4(mainTfPath, `################################################################################
|
|
2205
2267
|
# Thinkwork \u2014 ${config.stage}
|
|
2206
2268
|
# Generated by: thinkwork init -s ${config.stage}
|
|
@@ -2628,12 +2690,13 @@ and AgentCore for per-stage detail.
|
|
|
2628
2690
|
|
|
2629
2691
|
// src/commands/mcp.ts
|
|
2630
2692
|
import chalk10 from "chalk";
|
|
2693
|
+
import { createClient, adminKeys, AdminOpsError } from "@thinkwork/admin-ops";
|
|
2631
2694
|
|
|
2632
2695
|
// src/api-client.ts
|
|
2633
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
2696
|
+
import { readFileSync as readFileSync5, existsSync as existsSync9 } from "fs";
|
|
2634
2697
|
import { execSync as execSync9 } from "child_process";
|
|
2635
2698
|
function readTfVar2(tfvarsPath, key) {
|
|
2636
|
-
if (!
|
|
2699
|
+
if (!existsSync9(tfvarsPath)) return null;
|
|
2637
2700
|
const content = readFileSync5(tfvarsPath, "utf-8");
|
|
2638
2701
|
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
|
|
2639
2702
|
return match ? match[1] : null;
|
|
@@ -2642,7 +2705,7 @@ function resolveTfvarsPath2(stage) {
|
|
|
2642
2705
|
const tfDir = resolveTerraformDir(stage);
|
|
2643
2706
|
if (tfDir) {
|
|
2644
2707
|
const direct = `${tfDir}/terraform.tfvars`;
|
|
2645
|
-
if (
|
|
2708
|
+
if (existsSync9(direct)) return direct;
|
|
2646
2709
|
}
|
|
2647
2710
|
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
2648
2711
|
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
@@ -2949,14 +3012,14 @@ Examples:
|
|
|
2949
3012
|
$ thinkwork mcp add my-tools --url https://mcp.example.com/crm \\
|
|
2950
3013
|
--auth-type tenant_api_key --api-key sk-abc -s dev -t acme
|
|
2951
3014
|
|
|
2952
|
-
# OAuth
|
|
2953
|
-
$ thinkwork mcp add
|
|
2954
|
-
--auth-type per_user_oauth --oauth-provider
|
|
3015
|
+
# OAuth-backed MCP integration (users connect from the mobile app)
|
|
3016
|
+
$ thinkwork mcp add crm-tools --url https://mcp.example.com/crm \\
|
|
3017
|
+
--auth-type per_user_oauth --oauth-provider crm_provider -s dev -t acme
|
|
2955
3018
|
`
|
|
2956
3019
|
).action(
|
|
2957
3020
|
async (nameArg, opts) => {
|
|
2958
3021
|
try {
|
|
2959
|
-
const { input:
|
|
3022
|
+
const { input: input4 } = await import("@inquirer/prompts");
|
|
2960
3023
|
const { stage, api, tenant } = await resolveMcpContext(opts);
|
|
2961
3024
|
let name = nameArg;
|
|
2962
3025
|
if (!name) {
|
|
@@ -2964,7 +3027,7 @@ Examples:
|
|
|
2964
3027
|
printError("Name is required. Pass it as a positional arg.");
|
|
2965
3028
|
process.exit(1);
|
|
2966
3029
|
}
|
|
2967
|
-
name = await
|
|
3030
|
+
name = await input4({ message: "Server name:" });
|
|
2968
3031
|
}
|
|
2969
3032
|
let url = opts.url;
|
|
2970
3033
|
if (!url) {
|
|
@@ -2972,7 +3035,7 @@ Examples:
|
|
|
2972
3035
|
printError("--url is required. Pass it as a flag.");
|
|
2973
3036
|
process.exit(1);
|
|
2974
3037
|
}
|
|
2975
|
-
url = await
|
|
3038
|
+
url = await input4({
|
|
2976
3039
|
message: "MCP server URL:",
|
|
2977
3040
|
validate: (v) => v.startsWith("http://") || v.startsWith("https://") ? true : "URL must start with http:// or https://"
|
|
2978
3041
|
});
|
|
@@ -3006,13 +3069,13 @@ Examples:
|
|
|
3006
3069
|
`
|
|
3007
3070
|
Examples:
|
|
3008
3071
|
# Change URL in place (preserves agent assignments, unlike remove + re-add)
|
|
3009
|
-
$ thinkwork mcp update
|
|
3072
|
+
$ thinkwork mcp update routing-server --url https://mcp.example.com/routing
|
|
3010
3073
|
|
|
3011
3074
|
# Disable without deleting
|
|
3012
|
-
$ thinkwork mcp update
|
|
3075
|
+
$ thinkwork mcp update routing-server --disable
|
|
3013
3076
|
|
|
3014
3077
|
# Rename + change transport
|
|
3015
|
-
$ thinkwork mcp update 629dcee1-1e14-4b83-9907-cb529e6035f6 --name "
|
|
3078
|
+
$ thinkwork mcp update 629dcee1-1e14-4b83-9907-cb529e6035f6 --name "Routing Server" --transport sse
|
|
3016
3079
|
|
|
3017
3080
|
# Interactive \u2014 pick the server from a list
|
|
3018
3081
|
$ thinkwork mcp update
|
|
@@ -3065,7 +3128,7 @@ Examples:
|
|
|
3065
3128
|
$ thinkwork mcp remove
|
|
3066
3129
|
|
|
3067
3130
|
# By slug (case-insensitive)
|
|
3068
|
-
$ thinkwork mcp remove
|
|
3131
|
+
$ thinkwork mcp remove routing-server
|
|
3069
3132
|
|
|
3070
3133
|
# By UUID (from \`mcp list\` or --json)
|
|
3071
3134
|
$ thinkwork mcp remove 629dcee1-1e14-4b83-9907-cb529e6035f6
|
|
@@ -3132,7 +3195,7 @@ Examples:
|
|
|
3132
3195
|
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
|
|
3133
3196
|
async (mcpServerArg, opts) => {
|
|
3134
3197
|
try {
|
|
3135
|
-
const { input:
|
|
3198
|
+
const { input: input4 } = await import("@inquirer/prompts");
|
|
3136
3199
|
const { api, tenant } = await resolveMcpContext(opts);
|
|
3137
3200
|
const server = await resolveServer(mcpServerArg, api, tenant.slug);
|
|
3138
3201
|
let agent = opts.agent;
|
|
@@ -3141,7 +3204,7 @@ Examples:
|
|
|
3141
3204
|
printError("--agent is required. Pass it as a flag.");
|
|
3142
3205
|
process.exit(1);
|
|
3143
3206
|
}
|
|
3144
|
-
agent = await
|
|
3207
|
+
agent = await input4({ message: "Agent ID:" });
|
|
3145
3208
|
}
|
|
3146
3209
|
const result = await apiFetch(
|
|
3147
3210
|
api.apiUrl,
|
|
@@ -3164,7 +3227,7 @@ Examples:
|
|
|
3164
3227
|
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
|
|
3165
3228
|
async (mcpServerArg, opts) => {
|
|
3166
3229
|
try {
|
|
3167
|
-
const { input:
|
|
3230
|
+
const { input: input4 } = await import("@inquirer/prompts");
|
|
3168
3231
|
const { api, tenant } = await resolveMcpContext(opts);
|
|
3169
3232
|
const server = await resolveServer(mcpServerArg, api, tenant.slug);
|
|
3170
3233
|
let agent = opts.agent;
|
|
@@ -3173,7 +3236,7 @@ Examples:
|
|
|
3173
3236
|
printError("--agent is required. Pass it as a flag.");
|
|
3174
3237
|
process.exit(1);
|
|
3175
3238
|
}
|
|
3176
|
-
agent = await
|
|
3239
|
+
agent = await input4({ message: "Agent ID:" });
|
|
3177
3240
|
}
|
|
3178
3241
|
await apiFetch(
|
|
3179
3242
|
api.apiUrl,
|
|
@@ -3189,6 +3252,175 @@ Examples:
|
|
|
3189
3252
|
}
|
|
3190
3253
|
}
|
|
3191
3254
|
);
|
|
3255
|
+
const key = mcp.command("key").alias("keys").description("Manage per-tenant Bearer tokens for the admin-ops MCP server.");
|
|
3256
|
+
key.command("create").description(
|
|
3257
|
+
"Generate a new MCP admin token. Prints the raw token ONCE \u2014 save it immediately."
|
|
3258
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID").option("--name <label>", 'Human label for the key (default "default")').addHelpText(
|
|
3259
|
+
"after",
|
|
3260
|
+
`
|
|
3261
|
+
Examples:
|
|
3262
|
+
$ thinkwork mcp key create -t acme --name ci
|
|
3263
|
+
$ thinkwork mcp key create -t acme --json # token surfaces under .token
|
|
3264
|
+
|
|
3265
|
+
The token is shown ONCE. Hand it to the MCP client immediately; the server
|
|
3266
|
+
stores only the SHA-256 hash. To rotate, create a new one and revoke the
|
|
3267
|
+
old one.
|
|
3268
|
+
`
|
|
3269
|
+
).action(async (opts) => {
|
|
3270
|
+
try {
|
|
3271
|
+
const ctx = await resolveMcpContext(opts);
|
|
3272
|
+
const client = createClient({
|
|
3273
|
+
apiUrl: ctx.api.apiUrl,
|
|
3274
|
+
authSecret: ctx.api.authSecret
|
|
3275
|
+
});
|
|
3276
|
+
const created = await adminKeys.createAdminKey(client, ctx.tenant.slug, {
|
|
3277
|
+
name: opts.name
|
|
3278
|
+
});
|
|
3279
|
+
printJson(created);
|
|
3280
|
+
printWarning("This token will NOT be shown again. Copy it now.");
|
|
3281
|
+
printSuccess(`MCP admin key created: ${chalk10.bold(created.name)} (${created.id})`);
|
|
3282
|
+
console.log(` ${chalk10.dim("Token:")} ${chalk10.cyan(created.token)}`);
|
|
3283
|
+
} catch (err) {
|
|
3284
|
+
if (isCancellation(err)) return;
|
|
3285
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
3286
|
+
process.exit(1);
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
key.command("list").alias("ls").description("List MCP admin keys for a tenant (metadata only, never the raw token).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID").option("--all", "Include revoked keys").action(async (opts) => {
|
|
3290
|
+
try {
|
|
3291
|
+
const ctx = await resolveMcpContext(opts);
|
|
3292
|
+
const client = createClient({
|
|
3293
|
+
apiUrl: ctx.api.apiUrl,
|
|
3294
|
+
authSecret: ctx.api.authSecret
|
|
3295
|
+
});
|
|
3296
|
+
const all = await adminKeys.listAdminKeys(client, ctx.tenant.slug);
|
|
3297
|
+
const rows = opts.all ? all : all.filter((k) => !k.revoked_at);
|
|
3298
|
+
printJson(rows);
|
|
3299
|
+
printTable(rows, [
|
|
3300
|
+
{ key: "name", header: "NAME" },
|
|
3301
|
+
{ key: "id", header: "ID" },
|
|
3302
|
+
{ key: "created_at", header: "CREATED" },
|
|
3303
|
+
{ key: "last_used_at", header: "LAST USED" },
|
|
3304
|
+
{ key: "revoked_at", header: "REVOKED" }
|
|
3305
|
+
]);
|
|
3306
|
+
} catch (err) {
|
|
3307
|
+
if (isCancellation(err)) return;
|
|
3308
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
3309
|
+
process.exit(1);
|
|
3310
|
+
}
|
|
3311
|
+
});
|
|
3312
|
+
key.command("revoke <id>").description("Revoke an MCP admin key. Idempotent \u2014 revoking an already-revoked key is a no-op.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID").action(async (keyId, opts) => {
|
|
3313
|
+
try {
|
|
3314
|
+
const ctx = await resolveMcpContext(opts);
|
|
3315
|
+
const client = createClient({
|
|
3316
|
+
apiUrl: ctx.api.apiUrl,
|
|
3317
|
+
authSecret: ctx.api.authSecret
|
|
3318
|
+
});
|
|
3319
|
+
await adminKeys.revokeAdminKey(client, ctx.tenant.slug, keyId);
|
|
3320
|
+
printSuccess(`MCP admin key revoked: ${keyId}`);
|
|
3321
|
+
} catch (err) {
|
|
3322
|
+
if (err instanceof AdminOpsError && err.status === 404) {
|
|
3323
|
+
printError(`Key ${keyId} not found for tenant ${opts.tenant ?? ""}`);
|
|
3324
|
+
process.exit(2);
|
|
3325
|
+
}
|
|
3326
|
+
if (isCancellation(err)) return;
|
|
3327
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
3328
|
+
process.exit(1);
|
|
3329
|
+
}
|
|
3330
|
+
});
|
|
3331
|
+
mcp.command("provision").description(
|
|
3332
|
+
"Provision the admin-ops MCP for a tenant: mint a new key + store in Secrets Manager + register in tenant_mcp_servers."
|
|
3333
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID (omit with --all)").option(
|
|
3334
|
+
"--url <url>",
|
|
3335
|
+
"Override the MCP endpoint URL (defaults to the stage's execute-api or MCP_CUSTOM_DOMAIN)"
|
|
3336
|
+
).option("--all", "Provision for every tenant the caller can see (backfill mode)").addHelpText(
|
|
3337
|
+
"after",
|
|
3338
|
+
`
|
|
3339
|
+
Examples:
|
|
3340
|
+
# One tenant (interactive picker or -t)
|
|
3341
|
+
$ thinkwork mcp provision -t acme
|
|
3342
|
+
|
|
3343
|
+
# Explicit custom-domain URL
|
|
3344
|
+
$ thinkwork mcp provision -t acme --url https://mcp.thinkwork.ai/mcp/admin
|
|
3345
|
+
|
|
3346
|
+
# Backfill every tenant at once
|
|
3347
|
+
$ thinkwork mcp provision --all
|
|
3348
|
+
|
|
3349
|
+
The raw token is stored in Secrets Manager and duplicated into
|
|
3350
|
+
tenant_mcp_servers.auth_config \u2014 it is NOT printed on stdout. After
|
|
3351
|
+
provisioning, each agent still needs the server assigned via the admin
|
|
3352
|
+
SPA (or a future \`thinkwork mcp assign\` CLI pass).
|
|
3353
|
+
`
|
|
3354
|
+
).action(async (opts) => {
|
|
3355
|
+
try {
|
|
3356
|
+
if (opts.all && opts.tenant) {
|
|
3357
|
+
printError("--all and --tenant are mutually exclusive.");
|
|
3358
|
+
process.exit(1);
|
|
3359
|
+
}
|
|
3360
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
3361
|
+
const api = resolveApiConfig(stage);
|
|
3362
|
+
if (!api) process.exit(1);
|
|
3363
|
+
async function provisionOne(tenantIdOrSlug, label) {
|
|
3364
|
+
const res = await apiFetch(
|
|
3365
|
+
api.apiUrl,
|
|
3366
|
+
api.authSecret,
|
|
3367
|
+
`/api/tenants/${encodeURIComponent(tenantIdOrSlug)}/mcp-admin-provision`,
|
|
3368
|
+
{
|
|
3369
|
+
method: "POST",
|
|
3370
|
+
body: JSON.stringify(opts.url ? { url: opts.url } : {})
|
|
3371
|
+
}
|
|
3372
|
+
);
|
|
3373
|
+
printSuccess(
|
|
3374
|
+
`${label}: ${res.provisioned} (tenant_mcp_servers.id=${res.tenantMcpServerId}, url=${res.url})`
|
|
3375
|
+
);
|
|
3376
|
+
return res;
|
|
3377
|
+
}
|
|
3378
|
+
if (opts.all) {
|
|
3379
|
+
const tenantRows = await apiFetch(
|
|
3380
|
+
api.apiUrl,
|
|
3381
|
+
api.authSecret,
|
|
3382
|
+
"/api/tenants",
|
|
3383
|
+
{}
|
|
3384
|
+
);
|
|
3385
|
+
if (!Array.isArray(tenantRows) || tenantRows.length === 0) {
|
|
3386
|
+
printWarning("No tenants found.");
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
printHeader("mcp provision --all", stage);
|
|
3390
|
+
const results = [];
|
|
3391
|
+
for (const t of tenantRows) {
|
|
3392
|
+
try {
|
|
3393
|
+
await provisionOne(t.slug, `${t.name} (${t.slug})`);
|
|
3394
|
+
results.push({ slug: t.slug, ok: true });
|
|
3395
|
+
} catch (err) {
|
|
3396
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3397
|
+
printError(` \u2717 ${t.slug}: ${msg}`);
|
|
3398
|
+
results.push({ slug: t.slug, ok: false, reason: msg });
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
const ok = results.filter((r) => r.ok).length;
|
|
3402
|
+
const bad = results.length - ok;
|
|
3403
|
+
if (bad === 0) {
|
|
3404
|
+
printSuccess(`All ${ok} tenants provisioned.`);
|
|
3405
|
+
} else {
|
|
3406
|
+
printWarning(`${ok}/${results.length} succeeded; ${bad} failed.`);
|
|
3407
|
+
process.exit(1);
|
|
3408
|
+
}
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
const tenant = await resolveTenantRest({
|
|
3412
|
+
flag: opts.tenant,
|
|
3413
|
+
stage,
|
|
3414
|
+
apiUrl: api.apiUrl,
|
|
3415
|
+
authSecret: api.authSecret
|
|
3416
|
+
});
|
|
3417
|
+
await provisionOne(tenant.slug, tenant.slug);
|
|
3418
|
+
} catch (err) {
|
|
3419
|
+
if (isCancellation(err)) return;
|
|
3420
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
3421
|
+
process.exit(1);
|
|
3422
|
+
}
|
|
3423
|
+
});
|
|
3192
3424
|
}
|
|
3193
3425
|
|
|
3194
3426
|
// src/commands/tools.ts
|
|
@@ -3463,11 +3695,11 @@ function registerUpdateCommand(program2) {
|
|
|
3463
3695
|
}
|
|
3464
3696
|
|
|
3465
3697
|
// src/commands/user.ts
|
|
3466
|
-
import { spawn as
|
|
3698
|
+
import { spawn as spawn5 } from "child_process";
|
|
3467
3699
|
import { input as input2, select as select7 } from "@inquirer/prompts";
|
|
3468
3700
|
function getTerraformOutput2(cwd, key) {
|
|
3469
|
-
return new Promise((
|
|
3470
|
-
const proc =
|
|
3701
|
+
return new Promise((resolve4, reject) => {
|
|
3702
|
+
const proc = spawn5("terraform", ["output", "-raw", key], {
|
|
3471
3703
|
cwd,
|
|
3472
3704
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3473
3705
|
});
|
|
@@ -3476,7 +3708,7 @@ function getTerraformOutput2(cwd, key) {
|
|
|
3476
3708
|
proc.stdout.on("data", (d) => stdout += d);
|
|
3477
3709
|
proc.stderr.on("data", (d) => stderr += d);
|
|
3478
3710
|
proc.on("close", (code) => {
|
|
3479
|
-
if (code === 0)
|
|
3711
|
+
if (code === 0) resolve4(stdout.trim());
|
|
3480
3712
|
else
|
|
3481
3713
|
reject(
|
|
3482
3714
|
new Error(
|
|
@@ -3487,7 +3719,7 @@ function getTerraformOutput2(cwd, key) {
|
|
|
3487
3719
|
});
|
|
3488
3720
|
}
|
|
3489
3721
|
function runAwsCognitoReset(userPoolId, username, region) {
|
|
3490
|
-
return new Promise((
|
|
3722
|
+
return new Promise((resolve4) => {
|
|
3491
3723
|
const args = [
|
|
3492
3724
|
"cognito-idp",
|
|
3493
3725
|
"admin-reset-user-password",
|
|
@@ -3499,12 +3731,12 @@ function runAwsCognitoReset(userPoolId, username, region) {
|
|
|
3499
3731
|
"json"
|
|
3500
3732
|
];
|
|
3501
3733
|
if (region) args.push("--region", region);
|
|
3502
|
-
const proc =
|
|
3734
|
+
const proc = spawn5("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3503
3735
|
let stdout = "";
|
|
3504
3736
|
let stderr = "";
|
|
3505
3737
|
proc.stdout.on("data", (d) => stdout += d);
|
|
3506
3738
|
proc.stderr.on("data", (d) => stderr += d);
|
|
3507
|
-
proc.on("close", (code) =>
|
|
3739
|
+
proc.on("close", (code) => resolve4({ code: code ?? 1, stdout, stderr }));
|
|
3508
3740
|
});
|
|
3509
3741
|
}
|
|
3510
3742
|
function requireTty2(label) {
|
|
@@ -3785,9 +4017,8 @@ import { gql } from "@urql/core";
|
|
|
3785
4017
|
|
|
3786
4018
|
// src/lib/gql-client.ts
|
|
3787
4019
|
import {
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
fetchExchange
|
|
4020
|
+
CombinedError,
|
|
4021
|
+
stringifyDocument
|
|
3791
4022
|
} from "@urql/core";
|
|
3792
4023
|
|
|
3793
4024
|
// src/lib/resolve-auth.ts
|
|
@@ -3883,20 +4114,7 @@ async function getGqlClient(opts) {
|
|
|
3883
4114
|
}
|
|
3884
4115
|
const url = `${baseUrl.replace(/\/+$/, "")}/graphql`;
|
|
3885
4116
|
const auth = await resolveAuth({ stage: opts.stage, region });
|
|
3886
|
-
const client =
|
|
3887
|
-
url,
|
|
3888
|
-
exchanges: [cacheExchange, fetchExchange],
|
|
3889
|
-
fetchOptions: () => ({
|
|
3890
|
-
method: "POST",
|
|
3891
|
-
headers: {
|
|
3892
|
-
"content-type": "application/json",
|
|
3893
|
-
...auth.headers
|
|
3894
|
-
}
|
|
3895
|
-
}),
|
|
3896
|
-
// CLI calls are short-lived and we want server truth on every run —
|
|
3897
|
-
// bypass the in-memory cache to avoid stale reads between quick commands.
|
|
3898
|
-
requestPolicy: "network-only"
|
|
3899
|
-
});
|
|
4117
|
+
const client = createCliGqlClient(url, auth.headers);
|
|
3900
4118
|
return {
|
|
3901
4119
|
client,
|
|
3902
4120
|
url,
|
|
@@ -3904,14 +4122,95 @@ async function getGqlClient(opts) {
|
|
|
3904
4122
|
tenantSlug: auth.tenantSlug
|
|
3905
4123
|
};
|
|
3906
4124
|
}
|
|
4125
|
+
function createCliGqlClient(url, headers) {
|
|
4126
|
+
return {
|
|
4127
|
+
query: (doc, variables) => ({
|
|
4128
|
+
toPromise: () => executeGraphql(url, headers, doc, variables)
|
|
4129
|
+
}),
|
|
4130
|
+
mutation: (doc, variables) => ({
|
|
4131
|
+
toPromise: () => executeGraphql(url, headers, doc, variables)
|
|
4132
|
+
})
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
3907
4135
|
async function gqlQuery(client, doc, variables) {
|
|
3908
|
-
const res = await client.query(doc, variables).toPromise();
|
|
4136
|
+
const res = await client.query(serializeDocument(doc), variables).toPromise();
|
|
3909
4137
|
return unwrap(res);
|
|
3910
4138
|
}
|
|
3911
4139
|
async function gqlMutate(client, doc, variables) {
|
|
3912
|
-
const res = await client.mutation(doc, variables).toPromise();
|
|
4140
|
+
const res = await client.mutation(serializeDocument(doc), variables).toPromise();
|
|
3913
4141
|
return unwrap(res);
|
|
3914
4142
|
}
|
|
4143
|
+
function serializeDocument(doc) {
|
|
4144
|
+
return stringifyDocument(doc);
|
|
4145
|
+
}
|
|
4146
|
+
async function executeGraphql(url, headers, doc, variables) {
|
|
4147
|
+
const query = serializeDocument(doc);
|
|
4148
|
+
try {
|
|
4149
|
+
const response = await fetch(url, {
|
|
4150
|
+
method: "POST",
|
|
4151
|
+
headers: {
|
|
4152
|
+
"content-type": "application/json",
|
|
4153
|
+
...headers
|
|
4154
|
+
},
|
|
4155
|
+
body: JSON.stringify({ query, variables })
|
|
4156
|
+
});
|
|
4157
|
+
const text = await response.text();
|
|
4158
|
+
let payload = {};
|
|
4159
|
+
if (text) {
|
|
4160
|
+
try {
|
|
4161
|
+
payload = JSON.parse(text);
|
|
4162
|
+
} catch {
|
|
4163
|
+
return makeNetworkErrorResult(
|
|
4164
|
+
`GraphQL request failed with non-JSON response: ${text}`,
|
|
4165
|
+
response
|
|
4166
|
+
);
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
if (payload.errors?.length) {
|
|
4170
|
+
return {
|
|
4171
|
+
data: payload.data,
|
|
4172
|
+
error: new CombinedError({
|
|
4173
|
+
graphQLErrors: payload.errors,
|
|
4174
|
+
response
|
|
4175
|
+
}),
|
|
4176
|
+
extensions: payload.extensions,
|
|
4177
|
+
stale: false,
|
|
4178
|
+
hasNext: false
|
|
4179
|
+
};
|
|
4180
|
+
}
|
|
4181
|
+
if (!response.ok) {
|
|
4182
|
+
return makeNetworkErrorResult(
|
|
4183
|
+
`GraphQL request failed with HTTP ${response.status}`,
|
|
4184
|
+
response
|
|
4185
|
+
);
|
|
4186
|
+
}
|
|
4187
|
+
return {
|
|
4188
|
+
data: payload.data,
|
|
4189
|
+
error: void 0,
|
|
4190
|
+
extensions: payload.extensions,
|
|
4191
|
+
stale: false,
|
|
4192
|
+
hasNext: false
|
|
4193
|
+
};
|
|
4194
|
+
} catch (err) {
|
|
4195
|
+
return {
|
|
4196
|
+
error: new CombinedError({
|
|
4197
|
+
networkError: err instanceof Error ? err : new Error(String(err))
|
|
4198
|
+
}),
|
|
4199
|
+
stale: false,
|
|
4200
|
+
hasNext: false
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
function makeNetworkErrorResult(message, response) {
|
|
4205
|
+
return {
|
|
4206
|
+
error: new CombinedError({
|
|
4207
|
+
networkError: new Error(message),
|
|
4208
|
+
response
|
|
4209
|
+
}),
|
|
4210
|
+
stale: false,
|
|
4211
|
+
hasNext: false
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
3915
4214
|
function unwrap(res) {
|
|
3916
4215
|
if (res.error) {
|
|
3917
4216
|
const msg = res.error.graphQLErrors.map((e) => e.message).filter(Boolean).join("; ") || res.error.networkError?.message || "GraphQL request failed";
|
|
@@ -4012,20 +4311,20 @@ function notYetImplemented(commandPath, phase) {
|
|
|
4012
4311
|
// src/commands/thread.ts
|
|
4013
4312
|
function registerThreadCommand(program2) {
|
|
4014
4313
|
const thread = program2.command("thread").alias("threads").description(
|
|
4015
|
-
"Create, list, update, and comment on threads
|
|
4314
|
+
"Create, list, update, and comment on threads in a tenant."
|
|
4016
4315
|
);
|
|
4017
|
-
thread.command("list").alias("ls").description("List threads in a tenant with optional filters.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--
|
|
4316
|
+
thread.command("list").alias("ls").description("List threads in a tenant with optional filters.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--assignee <id>", "Filter by assignee (user or agent ID). Use `me` to match the caller.").option("--agent <id>", "Filter threads worked on by a specific agent").option("--search <q>", "Full-text search over thread titles").option("--limit <n>", "Max rows (default 50)", "50").option("--archived", "Include archived threads").addHelpText(
|
|
4018
4317
|
"after",
|
|
4019
4318
|
`
|
|
4020
4319
|
Examples:
|
|
4021
|
-
# Open work on the default stage/tenant
|
|
4022
|
-
$ thinkwork thread list --status IN_PROGRESS
|
|
4023
|
-
|
|
4024
|
-
# Pipe to jq
|
|
4025
|
-
$ thinkwork thread list --json | jq '.[] | select(.priority=="URGENT")'
|
|
4026
|
-
|
|
4027
4320
|
# Everything assigned to me
|
|
4028
4321
|
$ thinkwork thread list --assignee me
|
|
4322
|
+
|
|
4323
|
+
# Limit + JSON for piping
|
|
4324
|
+
$ thinkwork thread list --limit 100 --json | jq '.[] | .title'
|
|
4325
|
+
|
|
4326
|
+
# Archived threads only
|
|
4327
|
+
$ thinkwork thread list --archived
|
|
4029
4328
|
`
|
|
4030
4329
|
).action(() => notYetImplemented("thread list", 1));
|
|
4031
4330
|
thread.command("get <idOrNumber>").description("Fetch one thread by ID or by its tenant-scoped issue number.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
@@ -4037,38 +4336,29 @@ Examples:
|
|
|
4037
4336
|
$ thinkwork thread get 42 --json | jq .assignee
|
|
4038
4337
|
`
|
|
4039
4338
|
).action(() => notYetImplemented("thread get", 1));
|
|
4040
|
-
thread.command("create [title]").description("Create a new thread. Prompts for missing fields when running in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--
|
|
4339
|
+
thread.command("create [title]").description("Create a new thread. Prompts for missing fields when running in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--assignee <id>", "Assign on create (user or agent ID)").option("--due <iso>", "Due date as ISO-8601").option("--label <name...>", "Attach label(s) by name (repeatable)").addHelpText(
|
|
4041
4340
|
"after",
|
|
4042
4341
|
`
|
|
4043
4342
|
Examples:
|
|
4044
|
-
# Fully interactive \u2014 walkthrough prompts for title
|
|
4343
|
+
# Fully interactive \u2014 walkthrough prompts for title and assignee.
|
|
4045
4344
|
$ thinkwork thread create
|
|
4046
4345
|
|
|
4047
4346
|
# Scripted
|
|
4048
4347
|
$ thinkwork thread create "Investigate latency spike" \\
|
|
4049
|
-
--
|
|
4348
|
+
--assignee agt-obs-1 --label ops --label oncall
|
|
4050
4349
|
|
|
4051
4350
|
# Mix: pass the title, prompt for the rest.
|
|
4052
4351
|
$ thinkwork thread create "Investigate latency spike"
|
|
4053
4352
|
`
|
|
4054
4353
|
).action(() => notYetImplemented("thread create", 1));
|
|
4055
|
-
thread.command("update <id>").description("Update a thread's title,
|
|
4354
|
+
thread.command("update <id>").description("Update a thread's title, assignee, labels, or due date.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--title <t>", "Rename").option("--assignee <id>", "Reassign (user or agent ID)").option("--due <iso>", "Due date").addHelpText(
|
|
4056
4355
|
"after",
|
|
4057
4356
|
`
|
|
4058
4357
|
Examples:
|
|
4059
|
-
$ thinkwork thread update thr-abc --
|
|
4060
|
-
$ thinkwork thread update thr-abc --assignee agt-ops
|
|
4358
|
+
$ thinkwork thread update thr-abc --title "New title"
|
|
4359
|
+
$ thinkwork thread update thr-abc --assignee agt-ops
|
|
4061
4360
|
`
|
|
4062
4361
|
).action(() => notYetImplemented("thread update", 1));
|
|
4063
|
-
thread.command("close <id>").description("Mark a thread DONE. Shortcut for `thread update <id> --status DONE`.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--comment <text>", "Add a closing comment").addHelpText(
|
|
4064
|
-
"after",
|
|
4065
|
-
`
|
|
4066
|
-
Examples:
|
|
4067
|
-
$ thinkwork thread close thr-abc
|
|
4068
|
-
$ thinkwork thread close thr-abc --comment "fixed in #124"
|
|
4069
|
-
`
|
|
4070
|
-
).action(() => notYetImplemented("thread close", 1));
|
|
4071
|
-
thread.command("reopen <id>").description("Move a thread from DONE/CANCELLED back to TODO.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread reopen", 1));
|
|
4072
4362
|
thread.command("checkout <id>").description("Claim a thread so an agent can work it (locks other agents out).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent to check it out to (defaults to the caller)").addHelpText(
|
|
4073
4363
|
"after",
|
|
4074
4364
|
`
|
|
@@ -4076,7 +4366,7 @@ Examples:
|
|
|
4076
4366
|
$ thinkwork thread checkout thr-abc --agent agt-fixer
|
|
4077
4367
|
`
|
|
4078
4368
|
).action(() => notYetImplemented("thread checkout", 1));
|
|
4079
|
-
thread.command("release <id>").description("Release a checked-out thread
|
|
4369
|
+
thread.command("release <id>").description("Release a checked-out thread (unlocks it so another agent can claim it).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread release", 1));
|
|
4080
4370
|
thread.command("comment <id> [content]").description("Add a comment to a thread. Prompts for content if omitted and TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--file <path>", "Read comment content from a file (markdown)").addHelpText(
|
|
4081
4371
|
"after",
|
|
4082
4372
|
`
|
|
@@ -4262,6 +4552,216 @@ Examples:
|
|
|
4262
4552
|
version.command("rollback <agentId> <versionId>").description("Restore an agent to a prior version. Creates a new version pointing at the old config.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("agent version rollback", 2));
|
|
4263
4553
|
}
|
|
4264
4554
|
|
|
4555
|
+
// src/commands/computer.ts
|
|
4556
|
+
import chalk14 from "chalk";
|
|
4557
|
+
async function resolveComputerContext(opts) {
|
|
4558
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
4559
|
+
const api = resolveApiConfig(stage);
|
|
4560
|
+
if (!api) process.exit(1);
|
|
4561
|
+
return { stage, api };
|
|
4562
|
+
}
|
|
4563
|
+
function resolveTenantId(opts) {
|
|
4564
|
+
const tenantId = opts.tenant ?? opts.tenantId;
|
|
4565
|
+
if (!tenantId) {
|
|
4566
|
+
printError(
|
|
4567
|
+
"Tenant ID is required. Pass --tenant <uuid> or --tenant-id <uuid>."
|
|
4568
|
+
);
|
|
4569
|
+
process.exit(1);
|
|
4570
|
+
}
|
|
4571
|
+
return tenantId;
|
|
4572
|
+
}
|
|
4573
|
+
function resolveComputerId(opts) {
|
|
4574
|
+
const computerId = opts.computer ?? opts.computerId;
|
|
4575
|
+
if (!computerId) {
|
|
4576
|
+
printError(
|
|
4577
|
+
"Computer ID is required. Pass --computer <uuid> or --computer-id <uuid>."
|
|
4578
|
+
);
|
|
4579
|
+
process.exit(1);
|
|
4580
|
+
}
|
|
4581
|
+
return computerId;
|
|
4582
|
+
}
|
|
4583
|
+
function resolveTaskType(opts) {
|
|
4584
|
+
if (!opts.type?.trim()) {
|
|
4585
|
+
printError("Task type is required. Pass --type <task-type>.");
|
|
4586
|
+
process.exit(1);
|
|
4587
|
+
}
|
|
4588
|
+
return opts.type.trim().toLowerCase();
|
|
4589
|
+
}
|
|
4590
|
+
function printMigrationReport(response) {
|
|
4591
|
+
printJson(response);
|
|
4592
|
+
if (isJsonMode()) return;
|
|
4593
|
+
if (!response.report) return;
|
|
4594
|
+
const summary = response.report.summary ?? {};
|
|
4595
|
+
console.log("");
|
|
4596
|
+
console.log(chalk14.bold(" Summary"));
|
|
4597
|
+
for (const [status, count] of Object.entries(summary)) {
|
|
4598
|
+
if (!count) continue;
|
|
4599
|
+
console.log(` ${status.padEnd(28)} ${count}`);
|
|
4600
|
+
}
|
|
4601
|
+
const rows = (response.report.groups ?? []).map((group) => ({
|
|
4602
|
+
owner: group.owner?.name ?? group.owner?.email ?? group.ownerUserId ?? "unpaired",
|
|
4603
|
+
source: group.primaryAgent?.name ?? group.primaryAgentId ?? "\u2014",
|
|
4604
|
+
template: group.primaryAgent?.templateName ?? "\u2014",
|
|
4605
|
+
status: group.status,
|
|
4606
|
+
action: group.recommendedAction ?? "\u2014",
|
|
4607
|
+
reason: group.reasons?.[0] ?? "\u2014"
|
|
4608
|
+
}));
|
|
4609
|
+
console.log("");
|
|
4610
|
+
printTable(rows, [
|
|
4611
|
+
{ key: "owner", header: "Owner" },
|
|
4612
|
+
{ key: "source", header: "Source Agent" },
|
|
4613
|
+
{ key: "template", header: "Template" },
|
|
4614
|
+
{ key: "status", header: "Status" },
|
|
4615
|
+
{ key: "action", header: "Action" },
|
|
4616
|
+
{ key: "reason", header: "Reason" }
|
|
4617
|
+
]);
|
|
4618
|
+
}
|
|
4619
|
+
function registerComputerCommand(program2) {
|
|
4620
|
+
const computer = program2.command("computer").alias("computers").description("Manage ThinkWork Computers and migration operations");
|
|
4621
|
+
const migration = computer.command("migration").description("Dry-run or apply Agent-to-Computer migration");
|
|
4622
|
+
migration.command("dry-run").description("Inspect Agent-to-Computer migration candidates").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").action(
|
|
4623
|
+
async (opts) => {
|
|
4624
|
+
const { stage, api } = await resolveComputerContext(opts);
|
|
4625
|
+
const tenantId = resolveTenantId(opts);
|
|
4626
|
+
if (!isJsonMode()) printHeader("computer migration dry-run", stage);
|
|
4627
|
+
const response = await apiFetchRaw(
|
|
4628
|
+
api.apiUrl,
|
|
4629
|
+
api.authSecret,
|
|
4630
|
+
"/api/migrations/agents-to-computers",
|
|
4631
|
+
{
|
|
4632
|
+
method: "POST",
|
|
4633
|
+
body: JSON.stringify({ tenantId, mode: "dry-run" })
|
|
4634
|
+
}
|
|
4635
|
+
);
|
|
4636
|
+
if (!response.ok) {
|
|
4637
|
+
printJson(response.body);
|
|
4638
|
+
printError(response.body.error ?? `HTTP ${response.status}`);
|
|
4639
|
+
process.exit(1);
|
|
4640
|
+
}
|
|
4641
|
+
printMigrationReport(response.body);
|
|
4642
|
+
if (!isJsonMode()) printSuccess("Computer migration dry-run complete");
|
|
4643
|
+
}
|
|
4644
|
+
);
|
|
4645
|
+
migration.command("apply").description(
|
|
4646
|
+
"Apply Agent-to-Computer migration after reviewing dry-run output"
|
|
4647
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").option("--confirm", "Confirm the migration apply operation").option("--idempotency-key <key>", "Operator-supplied migration run key").action(
|
|
4648
|
+
async (opts) => {
|
|
4649
|
+
const { stage, api } = await resolveComputerContext(opts);
|
|
4650
|
+
const tenantId = resolveTenantId(opts);
|
|
4651
|
+
if (!opts.confirm) {
|
|
4652
|
+
printWarning(
|
|
4653
|
+
"Apply is intentionally gated. Re-run with --confirm after reviewing dry-run output."
|
|
4654
|
+
);
|
|
4655
|
+
process.exit(1);
|
|
4656
|
+
}
|
|
4657
|
+
if (!isJsonMode()) printHeader("computer migration apply", stage);
|
|
4658
|
+
const response = await apiFetchRaw(
|
|
4659
|
+
api.apiUrl,
|
|
4660
|
+
api.authSecret,
|
|
4661
|
+
"/api/migrations/agents-to-computers",
|
|
4662
|
+
{
|
|
4663
|
+
method: "POST",
|
|
4664
|
+
body: JSON.stringify({
|
|
4665
|
+
tenantId,
|
|
4666
|
+
mode: "apply",
|
|
4667
|
+
idempotencyKey: opts.idempotencyKey
|
|
4668
|
+
})
|
|
4669
|
+
}
|
|
4670
|
+
);
|
|
4671
|
+
if (!response.ok) {
|
|
4672
|
+
printJson(response.body);
|
|
4673
|
+
printError(response.body.error ?? `HTTP ${response.status}`);
|
|
4674
|
+
if (response.status === 409 && response.body.blockers) {
|
|
4675
|
+
if (!isJsonMode()) {
|
|
4676
|
+
console.log("");
|
|
4677
|
+
console.log(chalk14.bold(" Blockers"));
|
|
4678
|
+
console.log(JSON.stringify(response.body.blockers, null, 2));
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
process.exit(1);
|
|
4682
|
+
}
|
|
4683
|
+
printMigrationReport(response.body);
|
|
4684
|
+
if (!isJsonMode()) {
|
|
4685
|
+
printSuccess(
|
|
4686
|
+
`Computer migration applied: ${response.body.created?.length ?? 0} created, ${response.body.skipped?.length ?? 0} skipped`
|
|
4687
|
+
);
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
);
|
|
4691
|
+
const runtime = computer.command("runtime").description("Provision and control ECS-backed Computer runtimes");
|
|
4692
|
+
for (const action of [
|
|
4693
|
+
"provision",
|
|
4694
|
+
"start",
|
|
4695
|
+
"stop",
|
|
4696
|
+
"restart",
|
|
4697
|
+
"status"
|
|
4698
|
+
]) {
|
|
4699
|
+
runtime.command(action).description(`${action} a Computer runtime`).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").option("-c, --computer <uuid>", "Computer ID").option("--computer-id <uuid>", "Computer ID").action(
|
|
4700
|
+
async (opts) => {
|
|
4701
|
+
const { stage, api } = await resolveComputerContext(opts);
|
|
4702
|
+
const tenantId = resolveTenantId(opts);
|
|
4703
|
+
const computerId = resolveComputerId(opts);
|
|
4704
|
+
if (!isJsonMode()) {
|
|
4705
|
+
printHeader(`computer runtime ${action}`, stage);
|
|
4706
|
+
}
|
|
4707
|
+
const response = await apiFetchRaw(
|
|
4708
|
+
api.apiUrl,
|
|
4709
|
+
api.authSecret,
|
|
4710
|
+
"/api/computers/manager",
|
|
4711
|
+
{
|
|
4712
|
+
method: "POST",
|
|
4713
|
+
body: JSON.stringify({ action, tenantId, computerId })
|
|
4714
|
+
}
|
|
4715
|
+
);
|
|
4716
|
+
printJson(response.body);
|
|
4717
|
+
if (!response.ok) {
|
|
4718
|
+
printError(response.body.error ?? `HTTP ${response.status}`);
|
|
4719
|
+
process.exit(1);
|
|
4720
|
+
}
|
|
4721
|
+
if (!isJsonMode()) {
|
|
4722
|
+
printSuccess(`Computer runtime ${action} complete`);
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
);
|
|
4726
|
+
}
|
|
4727
|
+
const task = computer.command("task").description("Enqueue work for a ThinkWork Computer runtime");
|
|
4728
|
+
task.command("enqueue").description("Enqueue a Computer runtime task").option("--type <type>", "Task type").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").option("-c, --computer <uuid>", "Computer ID").option("--computer-id <uuid>", "Computer ID").option("--path <path>", "Workspace-relative path for file-write tasks").option("--content <content>", "UTF-8 content for file-write tasks").option("--idempotency-key <key>", "Operator-supplied idempotency key").action(
|
|
4729
|
+
async (opts) => {
|
|
4730
|
+
const { stage, api } = await resolveComputerContext(opts);
|
|
4731
|
+
const tenantId = resolveTenantId(opts);
|
|
4732
|
+
const computerId = resolveComputerId(opts);
|
|
4733
|
+
const taskType = resolveTaskType(opts);
|
|
4734
|
+
const input4 = taskType === "workspace_file_write" ? { path: opts.path, content: opts.content } : void 0;
|
|
4735
|
+
if (!isJsonMode()) printHeader("computer task enqueue", stage);
|
|
4736
|
+
const response = await apiFetchRaw(
|
|
4737
|
+
api.apiUrl,
|
|
4738
|
+
api.authSecret,
|
|
4739
|
+
"/api/computers/runtime/tasks",
|
|
4740
|
+
{
|
|
4741
|
+
method: "POST",
|
|
4742
|
+
body: JSON.stringify({
|
|
4743
|
+
tenantId,
|
|
4744
|
+
computerId,
|
|
4745
|
+
taskType,
|
|
4746
|
+
input: input4,
|
|
4747
|
+
idempotencyKey: opts.idempotencyKey
|
|
4748
|
+
})
|
|
4749
|
+
}
|
|
4750
|
+
);
|
|
4751
|
+
printJson(response.body);
|
|
4752
|
+
if (!response.ok) {
|
|
4753
|
+
printError(response.body.error ?? `HTTP ${response.status}`);
|
|
4754
|
+
process.exit(1);
|
|
4755
|
+
}
|
|
4756
|
+
if (!isJsonMode()) {
|
|
4757
|
+
printSuccess(
|
|
4758
|
+
`Queued Computer task ${response.body.task?.id ?? taskType}`
|
|
4759
|
+
);
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
);
|
|
4763
|
+
}
|
|
4764
|
+
|
|
4265
4765
|
// src/commands/template.ts
|
|
4266
4766
|
function registerTemplateCommand(program2) {
|
|
4267
4767
|
const tpl = program2.command("template").alias("templates").description("Manage agent templates \u2014 reusable configs you spawn agents from.");
|
|
@@ -4299,10 +4799,70 @@ Examples:
|
|
|
4299
4799
|
}
|
|
4300
4800
|
|
|
4301
4801
|
// src/commands/tenant.ts
|
|
4802
|
+
import { createClient as createClient2, tenants as tenantOps, AdminOpsError as AdminOpsError2 } from "@thinkwork/admin-ops";
|
|
4302
4803
|
function registerTenantCommand(program2) {
|
|
4303
4804
|
const tenant = program2.command("tenant").alias("tenants").description("Manage tenants (workspaces) \u2014 create, rename, and configure plans / defaults.");
|
|
4304
|
-
tenant.command("list").alias("ls").description("List tenants the caller can see.").option("-s, --stage <name>", "Deployment stage").
|
|
4305
|
-
|
|
4805
|
+
tenant.command("list").alias("ls").description("List tenants the caller can see.").option("-s, --stage <name>", "Deployment stage").addHelpText(
|
|
4806
|
+
"after",
|
|
4807
|
+
`
|
|
4808
|
+
Examples:
|
|
4809
|
+
$ thinkwork tenant list
|
|
4810
|
+
$ thinkwork tenant list -s dev
|
|
4811
|
+
$ thinkwork tenant list --json | jq '.[].slug'
|
|
4812
|
+
`
|
|
4813
|
+
).action(async (opts) => {
|
|
4814
|
+
try {
|
|
4815
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
4816
|
+
const api = resolveApiConfig(stage);
|
|
4817
|
+
if (!api) process.exit(1);
|
|
4818
|
+
const client = createClient2({ apiUrl: api.apiUrl, authSecret: api.authSecret });
|
|
4819
|
+
const rows = await tenantOps.listTenants(client);
|
|
4820
|
+
printJson(rows);
|
|
4821
|
+
printTable(rows, [
|
|
4822
|
+
{ key: "slug", header: "SLUG" },
|
|
4823
|
+
{ key: "name", header: "NAME" },
|
|
4824
|
+
{ key: "plan", header: "PLAN" },
|
|
4825
|
+
{ key: "id", header: "ID" }
|
|
4826
|
+
]);
|
|
4827
|
+
} catch (err) {
|
|
4828
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
4829
|
+
process.exit(1);
|
|
4830
|
+
}
|
|
4831
|
+
});
|
|
4832
|
+
tenant.command("get <idOrSlug>").description("Fetch one tenant by ID or slug.").option("-s, --stage <name>", "Deployment stage").addHelpText(
|
|
4833
|
+
"after",
|
|
4834
|
+
`
|
|
4835
|
+
Examples:
|
|
4836
|
+
$ thinkwork tenant get acme
|
|
4837
|
+
$ thinkwork tenant get 0a2b... --json
|
|
4838
|
+
`
|
|
4839
|
+
).action(async (idOrSlug, opts) => {
|
|
4840
|
+
try {
|
|
4841
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
4842
|
+
const api = resolveApiConfig(stage);
|
|
4843
|
+
if (!api) process.exit(1);
|
|
4844
|
+
const client = createClient2({ apiUrl: api.apiUrl, authSecret: api.authSecret });
|
|
4845
|
+
const isUuid2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
4846
|
+
idOrSlug
|
|
4847
|
+
);
|
|
4848
|
+
const tenant2 = isUuid2 ? await tenantOps.getTenant(client, idOrSlug) : await tenantOps.getTenantBySlug(client, idOrSlug);
|
|
4849
|
+
printJson(tenant2);
|
|
4850
|
+
printTable([tenant2], [
|
|
4851
|
+
{ key: "slug", header: "SLUG" },
|
|
4852
|
+
{ key: "name", header: "NAME" },
|
|
4853
|
+
{ key: "plan", header: "PLAN" },
|
|
4854
|
+
{ key: "issue_prefix", header: "PREFIX" },
|
|
4855
|
+
{ key: "id", header: "ID" }
|
|
4856
|
+
]);
|
|
4857
|
+
} catch (err) {
|
|
4858
|
+
if (err instanceof AdminOpsError2 && err.status === 404) {
|
|
4859
|
+
printError(`Tenant "${idOrSlug}" not found`);
|
|
4860
|
+
process.exit(2);
|
|
4861
|
+
}
|
|
4862
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
4863
|
+
process.exit(1);
|
|
4864
|
+
}
|
|
4865
|
+
});
|
|
4306
4866
|
tenant.command("create [name]").description("Create a new tenant. The caller becomes its first owner.").option("-s, --stage <name>", "Deployment stage").option("--slug <slug>", "URL-safe slug (lowercase, hyphens). Generated from name if omitted.").option("--plan <plan>", "Plan tier (free, team, enterprise, \u2026)", "team").option("--issue-prefix <prefix>", "Issue-number prefix for thread numbers (e.g. ACME)").addHelpText(
|
|
4307
4867
|
"after",
|
|
4308
4868
|
`
|
|
@@ -4492,29 +5052,218 @@ Examples:
|
|
|
4492
5052
|
wh.command("deliveries <id>").description("Show recent delivery attempts (success/failure, response status).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max rows", "25").action(() => notYetImplemented("webhook deliveries", 3));
|
|
4493
5053
|
}
|
|
4494
5054
|
|
|
4495
|
-
// src/
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
5055
|
+
// src/lib/plugin-zip.ts
|
|
5056
|
+
import { createReadStream, promises as fsp, statSync } from "fs";
|
|
5057
|
+
import { basename, join as join6, relative, resolve as resolve3, sep } from "path";
|
|
5058
|
+
import JSZip from "jszip";
|
|
5059
|
+
var PluginZipError = class extends Error {
|
|
5060
|
+
constructor(message, kind) {
|
|
5061
|
+
super(message);
|
|
5062
|
+
this.kind = kind;
|
|
5063
|
+
this.name = "PluginZipError";
|
|
5064
|
+
}
|
|
5065
|
+
kind;
|
|
5066
|
+
};
|
|
5067
|
+
async function buildPluginZip(pluginDir) {
|
|
5068
|
+
const root = resolve3(pluginDir);
|
|
5069
|
+
let stat;
|
|
5070
|
+
try {
|
|
5071
|
+
stat = await fsp.stat(root);
|
|
5072
|
+
} catch {
|
|
5073
|
+
throw new PluginZipError(
|
|
5074
|
+
`Plugin directory not found: ${pluginDir}`,
|
|
5075
|
+
"missing-directory"
|
|
5076
|
+
);
|
|
5077
|
+
}
|
|
5078
|
+
if (!stat.isDirectory()) {
|
|
5079
|
+
throw new PluginZipError(
|
|
5080
|
+
`Plugin path must be a directory: ${pluginDir}`,
|
|
5081
|
+
"missing-directory"
|
|
5082
|
+
);
|
|
5083
|
+
}
|
|
5084
|
+
const manifestPath = join6(root, "plugin.json");
|
|
5085
|
+
let manifestRaw;
|
|
5086
|
+
try {
|
|
5087
|
+
manifestRaw = await fsp.readFile(manifestPath, "utf8");
|
|
5088
|
+
} catch {
|
|
5089
|
+
throw new PluginZipError(
|
|
5090
|
+
`plugin.json is required at the root of the plugin folder (expected ${manifestPath}).`,
|
|
5091
|
+
"missing-plugin-json"
|
|
5092
|
+
);
|
|
5093
|
+
}
|
|
5094
|
+
let parsed;
|
|
5095
|
+
try {
|
|
5096
|
+
parsed = JSON.parse(manifestRaw);
|
|
5097
|
+
} catch (err) {
|
|
5098
|
+
throw new PluginZipError(
|
|
5099
|
+
`plugin.json is not valid JSON: ${err.message}`,
|
|
5100
|
+
"invalid-plugin-json"
|
|
5101
|
+
);
|
|
5102
|
+
}
|
|
5103
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5104
|
+
throw new PluginZipError(
|
|
5105
|
+
`plugin.json must be a JSON object with a "name" field.`,
|
|
5106
|
+
"invalid-plugin-json"
|
|
5107
|
+
);
|
|
5108
|
+
}
|
|
5109
|
+
const manifest = parsed;
|
|
5110
|
+
if (typeof manifest.name !== "string" || manifest.name.trim() === "") {
|
|
5111
|
+
throw new PluginZipError(
|
|
5112
|
+
`plugin.json must declare a non-empty "name" string.`,
|
|
5113
|
+
"invalid-plugin-json"
|
|
5114
|
+
);
|
|
5115
|
+
}
|
|
5116
|
+
const zip = new JSZip();
|
|
5117
|
+
const entries = await walkDir(root, root);
|
|
5118
|
+
for (const entry of entries) {
|
|
5119
|
+
if (entry.isSymbolicLink) {
|
|
5120
|
+
throw new PluginZipError(
|
|
5121
|
+
`Refusing to zip symlink: ${entry.relPath} (would be rejected server-side).`,
|
|
5122
|
+
"unsafe-entry"
|
|
5123
|
+
);
|
|
5124
|
+
}
|
|
5125
|
+
if (hasParentSegment(entry.relPath)) {
|
|
5126
|
+
throw new PluginZipError(
|
|
5127
|
+
`Refusing to zip path with traversal segment: ${entry.relPath}`,
|
|
5128
|
+
"unsafe-entry"
|
|
5129
|
+
);
|
|
5130
|
+
}
|
|
5131
|
+
const archivePath = entry.relPath.split(sep).join("/");
|
|
5132
|
+
zip.file(archivePath, createReadStream(entry.absPath));
|
|
5133
|
+
}
|
|
5134
|
+
let buffer;
|
|
5135
|
+
try {
|
|
5136
|
+
buffer = await zip.generateAsync({
|
|
5137
|
+
type: "nodebuffer",
|
|
5138
|
+
compression: "DEFLATE",
|
|
5139
|
+
compressionOptions: { level: 6 }
|
|
5140
|
+
});
|
|
5141
|
+
} catch (err) {
|
|
5142
|
+
throw new PluginZipError(
|
|
5143
|
+
`Failed to compress plugin: ${err.message}`,
|
|
5144
|
+
"io"
|
|
5145
|
+
);
|
|
5146
|
+
}
|
|
5147
|
+
const metadata = {
|
|
5148
|
+
name: manifest.name.trim(),
|
|
5149
|
+
version: typeof manifest.version === "string" && manifest.version.trim() !== "" ? manifest.version.trim() : void 0,
|
|
5150
|
+
description: typeof manifest.description === "string" ? manifest.description.trim() || void 0 : void 0
|
|
5151
|
+
};
|
|
5152
|
+
return {
|
|
5153
|
+
buffer,
|
|
5154
|
+
plugin: metadata,
|
|
5155
|
+
fileCount: entries.length,
|
|
5156
|
+
zipFileName: `${basename(root)}.zip`
|
|
5157
|
+
};
|
|
5158
|
+
}
|
|
5159
|
+
async function walkDir(rootDir, currentDir) {
|
|
5160
|
+
const out = [];
|
|
5161
|
+
const dirents = await fsp.readdir(currentDir, { withFileTypes: true });
|
|
5162
|
+
for (const ent of dirents) {
|
|
5163
|
+
const abs = join6(currentDir, ent.name);
|
|
5164
|
+
const rel = relative(rootDir, abs);
|
|
5165
|
+
if (ent.name === ".git" || ent.name === ".DS_Store" || ent.name === "node_modules") {
|
|
5166
|
+
continue;
|
|
5167
|
+
}
|
|
5168
|
+
if (ent.isSymbolicLink()) {
|
|
5169
|
+
out.push({ absPath: abs, relPath: rel, isSymbolicLink: true });
|
|
5170
|
+
continue;
|
|
5171
|
+
}
|
|
5172
|
+
if (ent.isDirectory()) {
|
|
5173
|
+
out.push(...await walkDir(rootDir, abs));
|
|
5174
|
+
continue;
|
|
5175
|
+
}
|
|
5176
|
+
if (ent.isFile()) {
|
|
5177
|
+
try {
|
|
5178
|
+
statSync(abs);
|
|
5179
|
+
} catch {
|
|
5180
|
+
continue;
|
|
5181
|
+
}
|
|
5182
|
+
out.push({ absPath: abs, relPath: rel, isSymbolicLink: false });
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
return out.sort(
|
|
5186
|
+
(a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0
|
|
5187
|
+
);
|
|
5188
|
+
}
|
|
5189
|
+
function hasParentSegment(relPath) {
|
|
5190
|
+
const parts = relPath.split(sep);
|
|
5191
|
+
return parts.some((p) => p === "..");
|
|
5192
|
+
}
|
|
4506
5193
|
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
5194
|
+
// src/lib/plugin-push.ts
|
|
5195
|
+
async function pushPluginZip(input4) {
|
|
5196
|
+
const base = input4.apiUrl.replace(/\/+$/, "");
|
|
5197
|
+
const presignRes = await fetch(`${base}/api/plugins/presign`, {
|
|
5198
|
+
method: "POST",
|
|
5199
|
+
headers: withJson(input4.headers),
|
|
5200
|
+
body: JSON.stringify({ fileName: input4.fileName })
|
|
5201
|
+
});
|
|
5202
|
+
if (!presignRes.ok) {
|
|
5203
|
+
throw new Error(`presign failed: ${await describeHttpError(presignRes)}`);
|
|
5204
|
+
}
|
|
5205
|
+
const presign = await presignRes.json();
|
|
5206
|
+
if (!presign.uploadUrl || !presign.s3Key) {
|
|
5207
|
+
throw new Error(
|
|
5208
|
+
`presign returned invalid response: ${JSON.stringify(presign)}`
|
|
5209
|
+
);
|
|
5210
|
+
}
|
|
5211
|
+
const putRes = await fetch(presign.uploadUrl, {
|
|
5212
|
+
method: "PUT",
|
|
5213
|
+
headers: { "Content-Type": "application/zip" },
|
|
5214
|
+
body: new Uint8Array(input4.zipBuffer)
|
|
5215
|
+
});
|
|
5216
|
+
if (!putRes.ok) {
|
|
5217
|
+
throw new Error(`S3 PUT failed: HTTP ${putRes.status}`);
|
|
5218
|
+
}
|
|
5219
|
+
const installRes = await fetch(`${base}/api/plugins/upload`, {
|
|
5220
|
+
method: "POST",
|
|
5221
|
+
headers: withJson(input4.headers),
|
|
5222
|
+
body: JSON.stringify({ s3Key: presign.s3Key })
|
|
5223
|
+
});
|
|
5224
|
+
const installBody = await installRes.json().catch(() => ({}));
|
|
5225
|
+
if (installRes.status === 400 && installBody && installBody.valid === false) {
|
|
5226
|
+
return {
|
|
5227
|
+
status: "validation-failed",
|
|
5228
|
+
errors: installBody.errors ?? [],
|
|
5229
|
+
warnings: installBody.warnings ?? []
|
|
5230
|
+
};
|
|
5231
|
+
}
|
|
5232
|
+
if (!installRes.ok) {
|
|
5233
|
+
const uploadId = typeof installBody.uploadId === "string" ? installBody.uploadId : "";
|
|
5234
|
+
return {
|
|
5235
|
+
status: "failed",
|
|
5236
|
+
uploadId,
|
|
5237
|
+
phase: typeof installBody.phase === "string" ? installBody.phase : void 0,
|
|
5238
|
+
errorMessage: typeof installBody.errorMessage === "string" && installBody.errorMessage || typeof installBody.error === "string" && installBody.error || `HTTP ${installRes.status}`
|
|
5239
|
+
};
|
|
5240
|
+
}
|
|
5241
|
+
const plugin = installBody.plugin;
|
|
5242
|
+
if (!plugin || typeof installBody.uploadId !== "string") {
|
|
5243
|
+
throw new Error(
|
|
5244
|
+
`install response missing uploadId/plugin: ${JSON.stringify(installBody)}`
|
|
5245
|
+
);
|
|
5246
|
+
}
|
|
5247
|
+
return {
|
|
5248
|
+
status: "installed",
|
|
5249
|
+
uploadId: installBody.uploadId,
|
|
5250
|
+
plugin,
|
|
5251
|
+
warnings: Array.isArray(installBody.warnings) ? installBody.warnings : []
|
|
5252
|
+
};
|
|
5253
|
+
}
|
|
5254
|
+
function withJson(headers) {
|
|
5255
|
+
return { "Content-Type": "application/json", ...headers };
|
|
5256
|
+
}
|
|
5257
|
+
async function describeHttpError(res) {
|
|
5258
|
+
const text = await res.text().catch(() => "");
|
|
5259
|
+
return `HTTP ${res.status} ${text.slice(0, 200)}`;
|
|
4513
5260
|
}
|
|
4514
5261
|
|
|
4515
5262
|
// src/commands/skill.ts
|
|
4516
5263
|
function registerSkillCommand(program2) {
|
|
4517
|
-
const skill = program2.command("skill").alias("skills").description(
|
|
5264
|
+
const skill = program2.command("skill").alias("skills").description(
|
|
5265
|
+
"Browse the catalog, install, upgrade, or publish custom skills."
|
|
5266
|
+
);
|
|
4518
5267
|
skill.command("catalog").description("Browse the public skill catalog (not tenant-scoped).").option("-s, --stage <name>", "Deployment stage").option("--search <q>", "Filter by keyword").option("--tag <t>", "Filter by tag").action(() => notYetImplemented("skill catalog", 3));
|
|
4519
5268
|
skill.command("list").alias("ls").description("List skills installed / published in the current tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--custom-only", "Only show tenant-owned custom skills").action(() => notYetImplemented("skill list", 3));
|
|
4520
5269
|
skill.command("install <slug>").description("Install a public skill into the tenant. Idempotent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--version <v>", "Pin to a specific version (default: latest)").addHelpText(
|
|
@@ -4526,7 +5275,9 @@ Examples:
|
|
|
4526
5275
|
`
|
|
4527
5276
|
).action(() => notYetImplemented("skill install", 3));
|
|
4528
5277
|
skill.command("upgrade <slug>").description("Upgrade an installed skill to the latest catalog version.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("skill upgrade", 3));
|
|
4529
|
-
skill.command("create [slug]").description(
|
|
5278
|
+
skill.command("create [slug]").description(
|
|
5279
|
+
"Publish a custom tenant-scoped skill (walkthrough for missing fields in TTY)."
|
|
5280
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--manifest-file <path>", "Path to the MCP server manifest JSON").option("--endpoint <url>", "MCP server HTTP/SSE endpoint").addHelpText(
|
|
4530
5281
|
"after",
|
|
4531
5282
|
`
|
|
4532
5283
|
Examples:
|
|
@@ -4535,7 +5286,103 @@ Examples:
|
|
|
4535
5286
|
`
|
|
4536
5287
|
).action(() => notYetImplemented("skill create", 3));
|
|
4537
5288
|
skill.command("update <slug>").description("Update a custom skill's manifest, endpoint, or description.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--manifest-file <path>").option("--endpoint <url>").action(() => notYetImplemented("skill update", 3));
|
|
4538
|
-
skill.command("delete <slug>").description(
|
|
5289
|
+
skill.command("delete <slug>").description(
|
|
5290
|
+
"Delete a custom skill. Public catalog skills are uninstalled via this too."
|
|
5291
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("skill delete", 3));
|
|
5292
|
+
skill.command("push <folder>").description(
|
|
5293
|
+
"Zip a local plugin folder and upload it to the tenant as a pending plugin."
|
|
5294
|
+
).option("-s, --stage <name>", "Deployment stage").option("--region <name>", "AWS region", "us-east-1").addHelpText(
|
|
5295
|
+
"after",
|
|
5296
|
+
`
|
|
5297
|
+
Examples:
|
|
5298
|
+
$ thinkwork skill push ./my-plugin
|
|
5299
|
+
$ thinkwork skill push ./my-plugin --stage dev
|
|
5300
|
+
|
|
5301
|
+
The folder must contain a plugin.json manifest. MCP servers shipped
|
|
5302
|
+
inside the plugin will land as 'pending' and need admin approval
|
|
5303
|
+
under Capabilities \u2192 MCP Servers before agents can invoke them.
|
|
5304
|
+
`
|
|
5305
|
+
).action(
|
|
5306
|
+
async (folder, opts) => {
|
|
5307
|
+
await runPushCommand(folder, opts);
|
|
5308
|
+
}
|
|
5309
|
+
);
|
|
5310
|
+
}
|
|
5311
|
+
async function runPushCommand(folder, opts) {
|
|
5312
|
+
const region = opts.region ?? "us-east-1";
|
|
5313
|
+
const stage = await resolveStage({ flag: opts.stage, region });
|
|
5314
|
+
let zipped;
|
|
5315
|
+
try {
|
|
5316
|
+
zipped = await buildPluginZip(folder);
|
|
5317
|
+
} catch (err) {
|
|
5318
|
+
if (err instanceof PluginZipError) {
|
|
5319
|
+
printError(err.message);
|
|
5320
|
+
process.exit(1);
|
|
5321
|
+
}
|
|
5322
|
+
throw err;
|
|
5323
|
+
}
|
|
5324
|
+
const auth = await resolveAuth({ stage, region, requireCognito: true });
|
|
5325
|
+
if (auth.mode !== "cognito") {
|
|
5326
|
+
printError(
|
|
5327
|
+
`skill push requires a Cognito session. Run \`thinkwork login --stage ${stage}\`.`
|
|
5328
|
+
);
|
|
5329
|
+
process.exit(1);
|
|
5330
|
+
}
|
|
5331
|
+
const apiUrl = getApiEndpoint(stage, region);
|
|
5332
|
+
if (!apiUrl) {
|
|
5333
|
+
printError(
|
|
5334
|
+
`Could not discover API endpoint for stage "${stage}" in ${region}. Is the stack deployed?`
|
|
5335
|
+
);
|
|
5336
|
+
process.exit(1);
|
|
5337
|
+
}
|
|
5338
|
+
printSuccess(
|
|
5339
|
+
`Prepared plugin "${zipped.plugin.name}" \u2014 ${zipped.fileCount} file(s), ${formatBytes(zipped.buffer.length)}`
|
|
5340
|
+
);
|
|
5341
|
+
let result;
|
|
5342
|
+
try {
|
|
5343
|
+
result = await pushPluginZip({
|
|
5344
|
+
apiUrl,
|
|
5345
|
+
headers: auth.headers,
|
|
5346
|
+
zipBuffer: zipped.buffer,
|
|
5347
|
+
fileName: zipped.zipFileName
|
|
5348
|
+
});
|
|
5349
|
+
} catch (err) {
|
|
5350
|
+
printError(`Upload failed: ${err.message}`);
|
|
5351
|
+
process.exit(1);
|
|
5352
|
+
}
|
|
5353
|
+
if (result.status === "validation-failed") {
|
|
5354
|
+
printError("Plugin validation failed");
|
|
5355
|
+
for (const e of result.errors) console.log(` - ${e}`);
|
|
5356
|
+
for (const w of result.warnings) printWarning(w);
|
|
5357
|
+
process.exit(1);
|
|
5358
|
+
}
|
|
5359
|
+
if (result.status === "failed") {
|
|
5360
|
+
printError(
|
|
5361
|
+
`Install failed${result.phase ? ` at phase ${result.phase}` : ""}: ${result.errorMessage}`
|
|
5362
|
+
);
|
|
5363
|
+
if (result.uploadId) {
|
|
5364
|
+
console.log(` upload id: ${result.uploadId}`);
|
|
5365
|
+
}
|
|
5366
|
+
process.exit(1);
|
|
5367
|
+
}
|
|
5368
|
+
const skillCount = result.plugin.skills.length;
|
|
5369
|
+
const mcpCount = result.plugin.mcpServers.length;
|
|
5370
|
+
printSuccess(
|
|
5371
|
+
`Installed "${result.plugin.name}" \u2014 ${skillCount} skill(s)` + (mcpCount > 0 ? `, ${mcpCount} MCP server(s) pending admin approval` : "")
|
|
5372
|
+
);
|
|
5373
|
+
console.log(` upload id: ${result.uploadId}`);
|
|
5374
|
+
if (mcpCount > 0) {
|
|
5375
|
+
console.log(
|
|
5376
|
+
` approve at: admin SPA \u2192 Capabilities \u2192 MCP Servers (filter: status=pending)`
|
|
5377
|
+
);
|
|
5378
|
+
}
|
|
5379
|
+
for (const w of result.warnings) printWarning(w);
|
|
5380
|
+
}
|
|
5381
|
+
function formatBytes(bytes) {
|
|
5382
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
5383
|
+
const kb = bytes / 1024;
|
|
5384
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
5385
|
+
return `${(kb / 1024).toFixed(2)} MB`;
|
|
4539
5386
|
}
|
|
4540
5387
|
|
|
4541
5388
|
// src/commands/memory.ts
|
|
@@ -4669,7 +5516,7 @@ Examples:
|
|
|
4669
5516
|
}
|
|
4670
5517
|
|
|
4671
5518
|
// src/commands/eval/run.ts
|
|
4672
|
-
import {
|
|
5519
|
+
import { checkbox, confirm as confirm2 } from "@inquirer/prompts";
|
|
4673
5520
|
import ora2 from "ora";
|
|
4674
5521
|
|
|
4675
5522
|
// src/gql/graphql.ts
|
|
@@ -4678,6 +5525,7 @@ var CliEvalRunDocument = { "kind": "Document", "definitions": [{ "kind": "Operat
|
|
|
4678
5525
|
var CliEvalRunResultsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalRunResults" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "runId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "evalRunResults" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "runId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "runId" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "testCaseId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "testCaseName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "score" } }, { "kind": "Field", "name": { "kind": "Name", "value": "durationMs" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentSessionId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "input" } }, { "kind": "Field", "name": { "kind": "Name", "value": "expected" } }, { "kind": "Field", "name": { "kind": "Name", "value": "actualOutput" } }, { "kind": "Field", "name": { "kind": "Name", "value": "evaluatorResults" } }, { "kind": "Field", "name": { "kind": "Name", "value": "assertions" } }, { "kind": "Field", "name": { "kind": "Name", "value": "errorMessage" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
|
|
4679
5526
|
var CliEvalTestCasesDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalTestCases" }, "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": "category" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "search" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "evalTestCases" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "category" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "category" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "search" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "search" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "query" } }, { "kind": "Field", "name": { "kind": "Name", "value": "systemPrompt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentcoreEvaluatorIds" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tags" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "source" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updatedAt" } }] } }] } }] };
|
|
4680
5527
|
var CliEvalTestCaseDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalTestCase" }, "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": "evalTestCase" }, "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": "tenantId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "query" } }, { "kind": "Field", "name": { "kind": "Name", "value": "systemPrompt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "assertions" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentcoreEvaluatorIds" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tags" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "source" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updatedAt" } }] } }] } }] };
|
|
5528
|
+
var CliComputersForEvalDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliComputersForEval" }, "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": "computers" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }], "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": "runtimeStatus" } }] } }] } }] };
|
|
4681
5529
|
var CliAgentTemplatesForEvalDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliAgentTemplatesForEval" }, "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": "agentTemplates" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }], "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": "model" } }, { "kind": "Field", "name": { "kind": "Name", "value": "isPublished" } }] } }] } }] };
|
|
4682
5530
|
var CliTenantBySlugDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliTenantBySlug" }, "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" } }] } }] } }] };
|
|
4683
5531
|
var CliStartEvalRunDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliStartEvalRun" }, "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": "input" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "StartEvalRunInput" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "startEvalRun" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "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": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "model" } }, { "kind": "Field", "name": { "kind": "Name", "value": "categories" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "totalTests" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
|
|
@@ -4696,17 +5544,18 @@ var CliWikiCompileJobsDocument = { "kind": "Document", "definitions": [{ "kind":
|
|
|
4696
5544
|
|
|
4697
5545
|
// src/gql/gql.ts
|
|
4698
5546
|
var documents = {
|
|
4699
|
-
"\n query CliEvalRuns($tenantId: ID!, $agentId: ID, $limit: Int, $offset: Int) {\n evalRuns(tenantId: $tenantId
|
|
5547
|
+
"\n query CliEvalRuns($tenantId: ID!, $agentId: ID, $limit: Int, $offset: Int) {\n evalRuns(\n tenantId: $tenantId\n agentId: $agentId\n limit: $limit\n offset: $offset\n ) {\n totalCount\n items {\n id\n status\n model\n categories\n agentId\n agentName\n agentTemplateId\n agentTemplateName\n totalTests\n passed\n failed\n passRate\n regression\n costUsd\n errorMessage\n startedAt\n completedAt\n createdAt\n }\n }\n }\n": CliEvalRunsDocument,
|
|
4700
5548
|
"\n query CliEvalRun($id: ID!) {\n evalRun(id: $id) {\n id\n status\n model\n categories\n agentId\n agentName\n agentTemplateId\n agentTemplateName\n totalTests\n passed\n failed\n passRate\n regression\n costUsd\n errorMessage\n startedAt\n completedAt\n createdAt\n }\n }\n": CliEvalRunDocument,
|
|
4701
5549
|
"\n query CliEvalRunResults($runId: ID!) {\n evalRunResults(runId: $runId) {\n id\n testCaseId\n testCaseName\n category\n status\n score\n durationMs\n agentSessionId\n input\n expected\n actualOutput\n evaluatorResults\n assertions\n errorMessage\n createdAt\n }\n }\n": CliEvalRunResultsDocument,
|
|
4702
5550
|
"\n query CliEvalTestCases($tenantId: ID!, $category: String, $search: String) {\n evalTestCases(tenantId: $tenantId, category: $category, search: $search) {\n id\n name\n category\n query\n systemPrompt\n agentTemplateId\n agentTemplateName\n agentcoreEvaluatorIds\n tags\n enabled\n source\n createdAt\n updatedAt\n }\n }\n": CliEvalTestCasesDocument,
|
|
4703
5551
|
"\n query CliEvalTestCase($id: ID!) {\n evalTestCase(id: $id) {\n id\n tenantId\n name\n category\n query\n systemPrompt\n agentTemplateId\n agentTemplateName\n assertions\n agentcoreEvaluatorIds\n tags\n enabled\n source\n createdAt\n updatedAt\n }\n }\n": CliEvalTestCaseDocument,
|
|
5552
|
+
"\n query CliComputersForEval($tenantId: ID!) {\n computers(tenantId: $tenantId) {\n id\n name\n slug\n runtimeStatus\n }\n }\n": CliComputersForEvalDocument,
|
|
4704
5553
|
"\n query CliAgentTemplatesForEval($tenantId: ID!) {\n agentTemplates(tenantId: $tenantId) {\n id\n name\n slug\n model\n isPublished\n }\n }\n": CliAgentTemplatesForEvalDocument,
|
|
4705
5554
|
"\n query CliTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n slug\n name\n }\n }\n": CliTenantBySlugDocument,
|
|
4706
5555
|
"\n mutation CliStartEvalRun($tenantId: ID!, $input: StartEvalRunInput!) {\n startEvalRun(tenantId: $tenantId, input: $input) {\n id\n status\n model\n categories\n agentTemplateId\n agentTemplateName\n totalTests\n createdAt\n }\n }\n": CliStartEvalRunDocument,
|
|
4707
5556
|
"\n mutation CliCancelEvalRun($id: ID!) {\n cancelEvalRun(id: $id) {\n id\n status\n completedAt\n }\n }\n": CliCancelEvalRunDocument,
|
|
4708
5557
|
"\n mutation CliDeleteEvalRun($id: ID!) {\n deleteEvalRun(id: $id)\n }\n": CliDeleteEvalRunDocument,
|
|
4709
|
-
"\n mutation CliCreateEvalTestCase($tenantId: ID
|
|
5558
|
+
"\n mutation CliCreateEvalTestCase(\n $tenantId: ID!\n $input: CreateEvalTestCaseInput!\n ) {\n createEvalTestCase(tenantId: $tenantId, input: $input) {\n id\n name\n category\n }\n }\n": CliCreateEvalTestCaseDocument,
|
|
4710
5559
|
"\n mutation CliUpdateEvalTestCase($id: ID!, $input: UpdateEvalTestCaseInput!) {\n updateEvalTestCase(id: $id, input: $input) {\n id\n name\n category\n enabled\n }\n }\n": CliUpdateEvalTestCaseDocument,
|
|
4711
5560
|
"\n mutation CliDeleteEvalTestCase($id: ID!) {\n deleteEvalTestCase(id: $id)\n }\n": CliDeleteEvalTestCaseDocument,
|
|
4712
5561
|
"\n mutation CliSeedEvalTestCases($tenantId: ID!, $categories: [String!]) {\n seedEvalTestCases(tenantId: $tenantId, categories: $categories)\n }\n": CliSeedEvalTestCasesDocument,
|
|
@@ -4724,7 +5573,12 @@ function graphql(source) {
|
|
|
4724
5573
|
// src/commands/eval/gql.ts
|
|
4725
5574
|
var EvalRunsDoc = graphql(`
|
|
4726
5575
|
query CliEvalRuns($tenantId: ID!, $agentId: ID, $limit: Int, $offset: Int) {
|
|
4727
|
-
evalRuns(
|
|
5576
|
+
evalRuns(
|
|
5577
|
+
tenantId: $tenantId
|
|
5578
|
+
agentId: $agentId
|
|
5579
|
+
limit: $limit
|
|
5580
|
+
offset: $offset
|
|
5581
|
+
) {
|
|
4728
5582
|
totalCount
|
|
4729
5583
|
items {
|
|
4730
5584
|
id
|
|
@@ -4834,6 +5688,16 @@ var EvalTestCaseDoc = graphql(`
|
|
|
4834
5688
|
}
|
|
4835
5689
|
}
|
|
4836
5690
|
`);
|
|
5691
|
+
var ComputersForEvalDoc = graphql(`
|
|
5692
|
+
query CliComputersForEval($tenantId: ID!) {
|
|
5693
|
+
computers(tenantId: $tenantId) {
|
|
5694
|
+
id
|
|
5695
|
+
name
|
|
5696
|
+
slug
|
|
5697
|
+
runtimeStatus
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
`);
|
|
4837
5701
|
var AgentTemplatesForEvalDoc = graphql(`
|
|
4838
5702
|
query CliAgentTemplatesForEval($tenantId: ID!) {
|
|
4839
5703
|
agentTemplates(tenantId: $tenantId) {
|
|
@@ -4883,7 +5747,10 @@ var DeleteEvalRunDoc = graphql(`
|
|
|
4883
5747
|
}
|
|
4884
5748
|
`);
|
|
4885
5749
|
var CreateEvalTestCaseDoc = graphql(`
|
|
4886
|
-
mutation CliCreateEvalTestCase(
|
|
5750
|
+
mutation CliCreateEvalTestCase(
|
|
5751
|
+
$tenantId: ID!
|
|
5752
|
+
$input: CreateEvalTestCaseInput!
|
|
5753
|
+
) {
|
|
4887
5754
|
createEvalTestCase(tenantId: $tenantId, input: $input) {
|
|
4888
5755
|
id
|
|
4889
5756
|
name
|
|
@@ -4981,123 +5848,75 @@ function isTerminalStatus(status) {
|
|
|
4981
5848
|
}
|
|
4982
5849
|
|
|
4983
5850
|
// src/commands/eval/run.ts
|
|
5851
|
+
var DEFAULT_EVAL_MODEL_ID = "moonshotai.kimi-k2.5";
|
|
4984
5852
|
async function runEvalRun(opts) {
|
|
4985
5853
|
const ctx = await resolveEvalContext(opts);
|
|
4986
5854
|
const interactive = isInteractive();
|
|
4987
|
-
|
|
5855
|
+
const deprecatedComputerId = opts.computer ?? null;
|
|
4988
5856
|
let categories = opts.category ?? null;
|
|
4989
5857
|
let testCaseIds = opts.testCase ?? null;
|
|
5858
|
+
if (deprecatedComputerId) {
|
|
5859
|
+
printError(
|
|
5860
|
+
"--computer is no longer supported for eval runs. Evals run directly against the default Agent template."
|
|
5861
|
+
);
|
|
5862
|
+
process.exit(1);
|
|
5863
|
+
}
|
|
5864
|
+
if (opts.model && opts.model !== DEFAULT_EVAL_MODEL_ID) {
|
|
5865
|
+
printError(
|
|
5866
|
+
`--model is no longer configurable for eval runs. Evals use ${DEFAULT_EVAL_MODEL_ID}.`
|
|
5867
|
+
);
|
|
5868
|
+
process.exit(1);
|
|
5869
|
+
}
|
|
4990
5870
|
const scopeSatisfied = testCaseIds && testCaseIds.length > 0 || categories && categories.length > 0 || opts.all === true;
|
|
4991
|
-
if (!
|
|
5871
|
+
if (!scopeSatisfied) {
|
|
4992
5872
|
if (!interactive) {
|
|
4993
5873
|
const missing = [];
|
|
4994
|
-
if (!
|
|
4995
|
-
|
|
5874
|
+
if (!scopeSatisfied)
|
|
5875
|
+
missing.push("one of --all | --category | --test-case");
|
|
4996
5876
|
printError(
|
|
4997
5877
|
`Missing required flag(s) in non-interactive session: ${missing.join(", ")}.`
|
|
4998
5878
|
);
|
|
4999
5879
|
process.exit(1);
|
|
5000
5880
|
}
|
|
5001
5881
|
}
|
|
5002
|
-
if (!
|
|
5003
|
-
const
|
|
5882
|
+
if (!scopeSatisfied) {
|
|
5883
|
+
const tcData = await gqlQuery(ctx.client, EvalTestCasesDoc, {
|
|
5004
5884
|
tenantId: ctx.tenantId
|
|
5005
5885
|
});
|
|
5006
|
-
const
|
|
5007
|
-
|
|
5008
|
-
|
|
5886
|
+
const distinctCategories = Array.from(
|
|
5887
|
+
new Set(
|
|
5888
|
+
(tcData.evalTestCases ?? []).filter((tc) => tc.enabled).map((tc) => tc.category)
|
|
5889
|
+
)
|
|
5890
|
+
).sort();
|
|
5891
|
+
if (distinctCategories.length === 0) {
|
|
5892
|
+
printError(
|
|
5893
|
+
"No enabled test cases exist for this tenant yet. Run `thinkwork eval seed` to load the starter pack."
|
|
5894
|
+
);
|
|
5009
5895
|
process.exit(1);
|
|
5010
5896
|
}
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
name: `${t.name}${t.model ? ` (${t.model})` : ""}${t.isPublished ? "" : " [draft]"}`,
|
|
5017
|
-
value: t.id
|
|
5018
|
-
})),
|
|
5019
|
-
loop: false
|
|
5020
|
-
})
|
|
5021
|
-
);
|
|
5022
|
-
}
|
|
5023
|
-
if (!scopeSatisfied) {
|
|
5024
|
-
const scope = await promptOrExit(
|
|
5025
|
-
() => select8({
|
|
5026
|
-
message: "How should we pick test cases?",
|
|
5027
|
-
choices: [
|
|
5028
|
-
{ name: "All enabled test cases", value: "all" },
|
|
5029
|
-
{ name: "Filter by category", value: "category" },
|
|
5030
|
-
{ name: "Pick specific test cases", value: "specific" }
|
|
5031
|
-
],
|
|
5897
|
+
const picked = await promptOrExit(
|
|
5898
|
+
() => checkbox({
|
|
5899
|
+
message: "Which categories? (space to toggle, enter to confirm)",
|
|
5900
|
+
choices: distinctCategories.map((c) => ({ name: c, value: c })),
|
|
5901
|
+
required: true,
|
|
5032
5902
|
loop: false
|
|
5033
5903
|
})
|
|
5034
5904
|
);
|
|
5035
|
-
|
|
5036
|
-
categories = null;
|
|
5037
|
-
testCaseIds = null;
|
|
5038
|
-
opts.all = true;
|
|
5039
|
-
} else if (scope === "category") {
|
|
5040
|
-
const tcData = await gqlQuery(ctx.client, EvalTestCasesDoc, {
|
|
5041
|
-
tenantId: ctx.tenantId
|
|
5042
|
-
});
|
|
5043
|
-
const distinctCategories = Array.from(
|
|
5044
|
-
new Set((tcData.evalTestCases ?? []).map((tc) => tc.category))
|
|
5045
|
-
).sort();
|
|
5046
|
-
if (distinctCategories.length === 0) {
|
|
5047
|
-
printError(
|
|
5048
|
-
"No test cases exist for this tenant yet. Run `thinkwork eval seed` to load the starter pack."
|
|
5049
|
-
);
|
|
5050
|
-
process.exit(1);
|
|
5051
|
-
}
|
|
5052
|
-
const picked = await promptOrExit(
|
|
5053
|
-
() => checkbox({
|
|
5054
|
-
message: "Which categories? (space to toggle, enter to confirm)",
|
|
5055
|
-
choices: distinctCategories.map((c) => ({ name: c, value: c })),
|
|
5056
|
-
required: true,
|
|
5057
|
-
loop: false
|
|
5058
|
-
})
|
|
5059
|
-
);
|
|
5060
|
-
categories = picked;
|
|
5061
|
-
} else {
|
|
5062
|
-
const tcData = await gqlQuery(ctx.client, EvalTestCasesDoc, {
|
|
5063
|
-
tenantId: ctx.tenantId
|
|
5064
|
-
});
|
|
5065
|
-
const options = (tcData.evalTestCases ?? []).filter((tc) => tc.enabled);
|
|
5066
|
-
if (options.length === 0) {
|
|
5067
|
-
printError("No enabled test cases to pick from.");
|
|
5068
|
-
process.exit(1);
|
|
5069
|
-
}
|
|
5070
|
-
const picked = await promptOrExit(
|
|
5071
|
-
() => checkbox({
|
|
5072
|
-
message: "Which test cases?",
|
|
5073
|
-
choices: options.map((tc) => ({
|
|
5074
|
-
name: `${tc.name} (${tc.category})`,
|
|
5075
|
-
value: tc.id
|
|
5076
|
-
})),
|
|
5077
|
-
required: true,
|
|
5078
|
-
loop: false
|
|
5079
|
-
})
|
|
5080
|
-
);
|
|
5081
|
-
testCaseIds = picked;
|
|
5082
|
-
}
|
|
5083
|
-
}
|
|
5084
|
-
if (!opts.model && interactive) {
|
|
5085
|
-
const entered = await promptOrExit(
|
|
5086
|
-
() => input3({
|
|
5087
|
-
message: "Model override? (blank for template default)",
|
|
5088
|
-
default: ""
|
|
5089
|
-
})
|
|
5090
|
-
);
|
|
5091
|
-
if (entered.trim()) opts.model = entered.trim();
|
|
5905
|
+
categories = picked;
|
|
5092
5906
|
}
|
|
5907
|
+
const requestedModel = opts.model ?? null;
|
|
5908
|
+
opts.model = DEFAULT_EVAL_MODEL_ID;
|
|
5093
5909
|
if (interactive && !isJsonMode()) {
|
|
5094
5910
|
const summaryLines = [
|
|
5095
5911
|
["Stage", ctx.stage],
|
|
5096
5912
|
["Tenant", ctx.tenantSlug],
|
|
5097
|
-
["Agent template"
|
|
5913
|
+
["Target", "Default Agent template"]
|
|
5098
5914
|
];
|
|
5915
|
+
if (requestedModel && requestedModel !== DEFAULT_EVAL_MODEL_ID)
|
|
5916
|
+
summaryLines.push(["Ignored Model", requestedModel]);
|
|
5099
5917
|
if (opts.model) summaryLines.push(["Model", opts.model]);
|
|
5100
|
-
if (categories && categories.length)
|
|
5918
|
+
if (categories && categories.length)
|
|
5919
|
+
summaryLines.push(["Categories", categories.join(", ")]);
|
|
5101
5920
|
if (testCaseIds && testCaseIds.length)
|
|
5102
5921
|
summaryLines.push(["Test cases", `${testCaseIds.length} picked`]);
|
|
5103
5922
|
if (opts.all && !categories?.length && !testCaseIds?.length)
|
|
@@ -5114,8 +5933,6 @@ async function runEvalRun(opts) {
|
|
|
5114
5933
|
const mutRes = await gqlMutate(ctx.client, StartEvalRunDoc, {
|
|
5115
5934
|
tenantId: ctx.tenantId,
|
|
5116
5935
|
input: {
|
|
5117
|
-
agentTemplateId,
|
|
5118
|
-
agentId: opts.agent ?? null,
|
|
5119
5936
|
model: opts.model ?? null,
|
|
5120
5937
|
categories: categories ?? null,
|
|
5121
5938
|
testCaseIds: testCaseIds ?? null
|
|
@@ -5123,7 +5940,12 @@ async function runEvalRun(opts) {
|
|
|
5123
5940
|
});
|
|
5124
5941
|
const run2 = mutRes.startEvalRun;
|
|
5125
5942
|
if (isJsonMode()) {
|
|
5126
|
-
printJson({
|
|
5943
|
+
printJson({
|
|
5944
|
+
runId: run2.id,
|
|
5945
|
+
status: run2.status,
|
|
5946
|
+
model: run2.model,
|
|
5947
|
+
categories: run2.categories
|
|
5948
|
+
});
|
|
5127
5949
|
} else {
|
|
5128
5950
|
printSuccess(`Started eval run ${run2.id} (status: ${run2.status}).`);
|
|
5129
5951
|
}
|
|
@@ -5147,8 +5969,12 @@ async function pollUntilTerminal(client, runId, intervalSec, timeoutSec) {
|
|
|
5147
5969
|
}
|
|
5148
5970
|
if (isTerminalStatus(run2.status)) {
|
|
5149
5971
|
if (spinner) {
|
|
5150
|
-
if (run2.status === "completed")
|
|
5151
|
-
|
|
5972
|
+
if (run2.status === "completed")
|
|
5973
|
+
spinner.succeed(
|
|
5974
|
+
`completed \u2014 ${run2.passed}/${run2.totalTests} (${fmtPercent(run2.passRate)})`
|
|
5975
|
+
);
|
|
5976
|
+
else if (run2.status === "failed")
|
|
5977
|
+
spinner.fail(`failed \u2014 ${run2.errorMessage ?? "unknown error"}`);
|
|
5152
5978
|
else spinner.warn("cancelled");
|
|
5153
5979
|
}
|
|
5154
5980
|
if (isJsonMode()) {
|
|
@@ -5466,7 +6292,7 @@ async function runEvalTestCaseGet(id, opts) {
|
|
|
5466
6292
|
|
|
5467
6293
|
// src/commands/eval/test-case/create.ts
|
|
5468
6294
|
import { readFileSync as readFileSync6 } from "fs";
|
|
5469
|
-
import { input as
|
|
6295
|
+
import { input as input3, select as select8, checkbox as checkbox2 } from "@inquirer/prompts";
|
|
5470
6296
|
var DEFAULT_EVALUATORS = [
|
|
5471
6297
|
"Builtin.Helpfulness",
|
|
5472
6298
|
"Builtin.Correctness",
|
|
@@ -5496,17 +6322,17 @@ async function runEvalTestCaseCreate(opts) {
|
|
|
5496
6322
|
if (!name) {
|
|
5497
6323
|
requireTty("Name");
|
|
5498
6324
|
name = await promptOrExit(
|
|
5499
|
-
() =>
|
|
6325
|
+
() => input3({ message: "Test case name?", validate: (v) => v.trim().length > 0 || "Required" })
|
|
5500
6326
|
);
|
|
5501
6327
|
}
|
|
5502
6328
|
if (!category) {
|
|
5503
6329
|
category = await promptOrExit(
|
|
5504
|
-
() =>
|
|
6330
|
+
() => input3({ message: "Category (free-form label)?", validate: (v) => v.trim().length > 0 || "Required" })
|
|
5505
6331
|
);
|
|
5506
6332
|
}
|
|
5507
6333
|
if (!query) {
|
|
5508
6334
|
query = await promptOrExit(
|
|
5509
|
-
() =>
|
|
6335
|
+
() => input3({ message: "Query the agent under test will receive?", validate: (v) => v.trim().length > 0 || "Required" })
|
|
5510
6336
|
);
|
|
5511
6337
|
}
|
|
5512
6338
|
if (interactive && agentTemplateId === null) {
|
|
@@ -5514,7 +6340,7 @@ async function runEvalTestCaseCreate(opts) {
|
|
|
5514
6340
|
const templates = tpls.agentTemplates ?? [];
|
|
5515
6341
|
if (templates.length > 0) {
|
|
5516
6342
|
const choice = await promptOrExit(
|
|
5517
|
-
() =>
|
|
6343
|
+
() => select8({
|
|
5518
6344
|
message: "Pin to an agent template? (Enter for none)",
|
|
5519
6345
|
choices: [
|
|
5520
6346
|
{ name: "\u2014 none \u2014 (runner picks)", value: "" },
|
|
@@ -5572,28 +6398,28 @@ async function runEvalTestCaseCreate(opts) {
|
|
|
5572
6398
|
import { readFileSync as readFileSync7 } from "fs";
|
|
5573
6399
|
async function runEvalTestCaseUpdate(id, opts) {
|
|
5574
6400
|
const ctx = await resolveEvalContext(opts);
|
|
5575
|
-
const
|
|
5576
|
-
if (opts.name !== void 0)
|
|
5577
|
-
if (opts.category !== void 0)
|
|
5578
|
-
if (opts.query !== void 0)
|
|
5579
|
-
if (opts.systemPrompt !== void 0)
|
|
5580
|
-
if (opts.agentTemplate !== void 0)
|
|
5581
|
-
if (opts.evaluator !== void 0)
|
|
5582
|
-
if (opts.tag !== void 0)
|
|
5583
|
-
if (opts.enabled !== void 0)
|
|
6401
|
+
const input4 = {};
|
|
6402
|
+
if (opts.name !== void 0) input4.name = opts.name;
|
|
6403
|
+
if (opts.category !== void 0) input4.category = opts.category;
|
|
6404
|
+
if (opts.query !== void 0) input4.query = opts.query;
|
|
6405
|
+
if (opts.systemPrompt !== void 0) input4.systemPrompt = opts.systemPrompt;
|
|
6406
|
+
if (opts.agentTemplate !== void 0) input4.agentTemplateId = opts.agentTemplate;
|
|
6407
|
+
if (opts.evaluator !== void 0) input4.agentcoreEvaluatorIds = opts.evaluator;
|
|
6408
|
+
if (opts.tag !== void 0) input4.tags = opts.tag;
|
|
6409
|
+
if (opts.enabled !== void 0) input4.enabled = opts.enabled;
|
|
5584
6410
|
if (opts.assertionsFile) {
|
|
5585
6411
|
const parsed = JSON.parse(readFileSync7(opts.assertionsFile, "utf8"));
|
|
5586
6412
|
if (!Array.isArray(parsed)) {
|
|
5587
6413
|
printError(`--assertions-file must contain a JSON array.`);
|
|
5588
6414
|
process.exit(1);
|
|
5589
6415
|
}
|
|
5590
|
-
|
|
6416
|
+
input4.assertions = parsed;
|
|
5591
6417
|
}
|
|
5592
|
-
if (Object.keys(
|
|
6418
|
+
if (Object.keys(input4).length === 0) {
|
|
5593
6419
|
printError("No fields to update. Pass at least one --<field>.");
|
|
5594
6420
|
process.exit(1);
|
|
5595
6421
|
}
|
|
5596
|
-
const res = await gqlMutate(ctx.client, UpdateEvalTestCaseDoc, { id, input:
|
|
6422
|
+
const res = await gqlMutate(ctx.client, UpdateEvalTestCaseDoc, { id, input: input4 });
|
|
5597
6423
|
if (isJsonMode()) {
|
|
5598
6424
|
printJson(res.updateEvalTestCase);
|
|
5599
6425
|
return;
|
|
@@ -5631,38 +6457,84 @@ async function runEvalTestCaseDelete(id, opts) {
|
|
|
5631
6457
|
// src/commands/eval.ts
|
|
5632
6458
|
function registerEvalCommand(program2) {
|
|
5633
6459
|
const evals = program2.command("eval").alias("evals").description(
|
|
5634
|
-
"Run evaluations against
|
|
5635
|
-
)
|
|
6460
|
+
"Run evaluations against the default AgentCore agent template and manage eval test cases. Integrates with the Evaluations Studio in the admin UI."
|
|
6461
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
|
|
6462
|
+
"--computer <id>",
|
|
6463
|
+
"Deprecated; evals now run directly against AgentCore"
|
|
6464
|
+
).option("--model <id>", "Deprecated; evals always use Kimi K2.5").option("--category <name...>", "Only run these categories (repeatable)").option(
|
|
6465
|
+
"--test-case <id...>",
|
|
6466
|
+
"Only run these specific test case IDs (repeatable)"
|
|
6467
|
+
).option("--all", "Run all enabled test cases for the tenant").option("--watch", "Block and poll until the run reaches a terminal status").option(
|
|
6468
|
+
"--timeout <seconds>",
|
|
6469
|
+
"Max wait seconds for --watch (default 900)",
|
|
6470
|
+
"900"
|
|
6471
|
+
).addHelpText(
|
|
6472
|
+
"after",
|
|
6473
|
+
`
|
|
6474
|
+
Default action:
|
|
6475
|
+
$ thinkwork evals
|
|
6476
|
+
|
|
6477
|
+
Equivalent explicit action:
|
|
6478
|
+
$ thinkwork eval run
|
|
6479
|
+
`
|
|
6480
|
+
).action(runEvalRun);
|
|
5636
6481
|
evals.command("run").description(
|
|
5637
6482
|
"Start an evaluation run. Prompts for missing values in a TTY; fails fast in non-interactive sessions."
|
|
5638
|
-
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
|
|
6483
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
|
|
6484
|
+
"--computer <id>",
|
|
6485
|
+
"Deprecated; evals now run directly against AgentCore"
|
|
6486
|
+
).option("--model <id>", "Deprecated; evals always use Kimi K2.5").option("--category <name...>", "Only run these categories (repeatable)").option(
|
|
6487
|
+
"--test-case <id...>",
|
|
6488
|
+
"Only run these specific test case IDs (repeatable)"
|
|
6489
|
+
).option("--all", "Run all enabled test cases for the tenant").option("--watch", "Block and poll until the run reaches a terminal status").option(
|
|
6490
|
+
"--timeout <seconds>",
|
|
6491
|
+
"Max wait seconds for --watch (default 900)",
|
|
6492
|
+
"900"
|
|
6493
|
+
).addHelpText(
|
|
5639
6494
|
"after",
|
|
5640
6495
|
`
|
|
5641
6496
|
Examples:
|
|
5642
6497
|
# Fire and return \u2014 prints the runId; view results in the admin UI
|
|
5643
|
-
$ thinkwork eval run --
|
|
6498
|
+
$ thinkwork eval run --category red-team-safety-scope
|
|
5644
6499
|
|
|
5645
|
-
# Pick categories
|
|
6500
|
+
# Pick categories interactively
|
|
5646
6501
|
$ thinkwork eval run
|
|
5647
6502
|
|
|
5648
6503
|
# Block until done
|
|
5649
|
-
$ thinkwork eval run --
|
|
6504
|
+
$ thinkwork eval run --all --watch --timeout 1800
|
|
5650
6505
|
`
|
|
5651
6506
|
).action(runEvalRun);
|
|
5652
6507
|
evals.command("list").alias("ls").description("List recent eval runs for the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--agent <id>", "Filter by agent under test").option("--limit <n>", "Max rows (default 25)", "25").option("--offset <n>", "Skip N rows", "0").action(runEvalList);
|
|
5653
|
-
evals.command("get <runId>").description("Show one eval run with its per-test-case results.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
|
|
6508
|
+
evals.command("get <runId>").description("Show one eval run with its per-test-case results.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
|
|
6509
|
+
"--results",
|
|
6510
|
+
"Also fetch per-test-case results (default: true)",
|
|
6511
|
+
true
|
|
6512
|
+
).option("--no-results", "Skip fetching per-test-case results").action(runEvalGet);
|
|
5654
6513
|
evals.command("watch <runId>").description("Poll an eval run until it reaches a terminal status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--interval <seconds>", "Poll interval (default 3)", "3").option("--timeout <seconds>", "Max wait seconds (default 900)", "900").action(runEvalWatch);
|
|
5655
6514
|
evals.command("cancel <runId>").description("Cancel a running or pending eval run.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalCancel);
|
|
5656
|
-
evals.command("delete <runId>").description(
|
|
5657
|
-
|
|
6515
|
+
evals.command("delete <runId>").description(
|
|
6516
|
+
"Delete an eval run and its results. Requires confirmation unless --yes."
|
|
6517
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("-y, --yes", "Skip the confirmation prompt").action(runEvalDelete);
|
|
6518
|
+
evals.command("categories").description(
|
|
6519
|
+
"List distinct categories present across the tenant's test cases."
|
|
6520
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalCategories);
|
|
5658
6521
|
evals.command("seed").description(
|
|
5659
|
-
"Idempotently seed the
|
|
6522
|
+
"Idempotently seed the ThinkWork RedTeam starter pack (189 test cases across 4 categories). Safe to re-run."
|
|
5660
6523
|
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--category <name...>", "Only seed these categories (repeatable)").action(runEvalSeed);
|
|
5661
6524
|
const tc = evals.command("test-case").alias("test-cases").description("Manage individual eval test cases (CRUD).");
|
|
5662
6525
|
tc.command("list").alias("ls").description("List test cases, optionally filtered by category or search.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--category <name>", "Filter by a single category").option("--search <q>", "Substring match on test case name").action(runEvalTestCaseList);
|
|
5663
6526
|
tc.command("get <id>").description("Show a single test case.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalTestCaseGet);
|
|
5664
|
-
tc.command("create").description("Create a new test case. Prompts for missing values in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--name <text>", "Human-readable name").option("--category <name>", "Category label (e.g. tool-safety, red-team)").option("--query <text>", "The user-facing query this agent will receive").option("--system-prompt <text>", "Optional system-prompt override").option("--agent-template <id>", "Pin to a specific agent template").option("--evaluator <id...>", "AgentCore evaluator IDs (repeatable)").option("--tag <name...>", "Tags (repeatable)").option("--enabled", "Mark enabled (default)", true).option("--no-enabled", "Mark disabled").option(
|
|
5665
|
-
|
|
6527
|
+
tc.command("create").description("Create a new test case. Prompts for missing values in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--name <text>", "Human-readable name").option("--category <name>", "Category label (e.g. tool-safety, red-team)").option("--query <text>", "The user-facing query this agent will receive").option("--system-prompt <text>", "Optional system-prompt override").option("--agent-template <id>", "Pin to a specific agent template").option("--evaluator <id...>", "AgentCore evaluator IDs (repeatable)").option("--tag <name...>", "Tags (repeatable)").option("--enabled", "Mark enabled (default)", true).option("--no-enabled", "Mark disabled").option(
|
|
6528
|
+
"--assertions-file <path>",
|
|
6529
|
+
"JSON file containing an array of assertions"
|
|
6530
|
+
).action(runEvalTestCaseCreate);
|
|
6531
|
+
tc.command("update <id>").description("Update a test case. Only supplied fields are changed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--name <text>").option("--category <name>").option("--query <text>").option("--system-prompt <text>").option("--agent-template <id>").option(
|
|
6532
|
+
"--evaluator <id...>",
|
|
6533
|
+
"Replace AgentCore evaluator IDs (repeatable)"
|
|
6534
|
+
).option("--tag <name...>", "Replace tags (repeatable)").option("--enabled", "Mark enabled").option("--no-enabled", "Mark disabled").option(
|
|
6535
|
+
"--assertions-file <path>",
|
|
6536
|
+
"JSON file containing an array of assertions (replaces all)"
|
|
6537
|
+
).action(runEvalTestCaseUpdate);
|
|
5666
6538
|
tc.command("delete <id>").description("Delete a test case. Requires confirmation unless --yes.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("-y, --yes", "Skip the confirmation prompt").action(runEvalTestCaseDelete);
|
|
5667
6539
|
}
|
|
5668
6540
|
|
|
@@ -5735,7 +6607,7 @@ var WikiCompileJobsDoc = graphql(`
|
|
|
5735
6607
|
`);
|
|
5736
6608
|
|
|
5737
6609
|
// src/commands/wiki/helpers.ts
|
|
5738
|
-
import { select as
|
|
6610
|
+
import { select as select9 } from "@inquirer/prompts";
|
|
5739
6611
|
async function resolveWikiContext(opts) {
|
|
5740
6612
|
const region = opts.region ?? "us-east-1";
|
|
5741
6613
|
const stage = await resolveStage({ flag: opts.stage, region });
|
|
@@ -5875,7 +6747,7 @@ async function resolveAgentScope(ctx, opts, config = {}) {
|
|
|
5875
6747
|
choices.push({ name: `${label}${slugPart} [${a.id}]`, value: a.id });
|
|
5876
6748
|
}
|
|
5877
6749
|
const pick = await promptOrExit(
|
|
5878
|
-
() =>
|
|
6750
|
+
() => select9({
|
|
5879
6751
|
message: "Which agent?",
|
|
5880
6752
|
choices,
|
|
5881
6753
|
loop: false
|
|
@@ -6571,6 +7443,7 @@ registerMessageCommand(program);
|
|
|
6571
7443
|
registerLabelCommand(program);
|
|
6572
7444
|
registerInboxCommand(program);
|
|
6573
7445
|
registerAgentCommand(program);
|
|
7446
|
+
registerComputerCommand(program);
|
|
6574
7447
|
registerTemplateCommand(program);
|
|
6575
7448
|
registerTenantCommand(program);
|
|
6576
7449
|
registerMemberCommand(program);
|
|
@@ -6581,7 +7454,6 @@ registerScheduledJobCommand(program);
|
|
|
6581
7454
|
registerTurnCommand(program);
|
|
6582
7455
|
registerWakeupCommand(program);
|
|
6583
7456
|
registerWebhookCommand(program);
|
|
6584
|
-
registerConnectorCommand(program);
|
|
6585
7457
|
registerSkillCommand(program);
|
|
6586
7458
|
registerMemoryCommand(program);
|
|
6587
7459
|
registerRecipeCommand(program);
|