thinkwork-cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js
CHANGED
|
@@ -32,6 +32,45 @@ function saveCliConfig(next, pathOverride) {
|
|
|
32
32
|
const merged = { ...loadCliConfig(pathOverride), ...next };
|
|
33
33
|
writeFileSync(path2, JSON.stringify(merged, null, 2) + "\n");
|
|
34
34
|
}
|
|
35
|
+
function saveStageSession(stage, session, pathOverride) {
|
|
36
|
+
const current = loadCliConfig(pathOverride);
|
|
37
|
+
const sessions = { ...current.sessions ?? {}, [stage]: session };
|
|
38
|
+
saveCliConfig({ sessions }, pathOverride);
|
|
39
|
+
}
|
|
40
|
+
function loadStageSession(stage, pathOverride) {
|
|
41
|
+
return loadCliConfig(pathOverride).sessions?.[stage] ?? null;
|
|
42
|
+
}
|
|
43
|
+
function clearStageSession(stage, pathOverride) {
|
|
44
|
+
const current = loadCliConfig(pathOverride);
|
|
45
|
+
if (!current.sessions?.[stage]) return;
|
|
46
|
+
const { [stage]: _removed, ...rest } = current.sessions;
|
|
47
|
+
saveCliConfig({ sessions: rest }, pathOverride);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/lib/output.ts
|
|
51
|
+
import chalk from "chalk";
|
|
52
|
+
var jsonMode = false;
|
|
53
|
+
function setJsonMode(enabled) {
|
|
54
|
+
jsonMode = Boolean(enabled);
|
|
55
|
+
}
|
|
56
|
+
function isJsonMode() {
|
|
57
|
+
return jsonMode;
|
|
58
|
+
}
|
|
59
|
+
function printJson(value) {
|
|
60
|
+
if (!jsonMode) return;
|
|
61
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
62
|
+
}
|
|
63
|
+
function printKeyValue(pairs) {
|
|
64
|
+
if (jsonMode) return;
|
|
65
|
+
const width = Math.max(...pairs.map(([k]) => k.length));
|
|
66
|
+
for (const [k, v] of pairs) {
|
|
67
|
+
const label = chalk.dim(k.padEnd(width) + " ");
|
|
68
|
+
console.log(` ${label}${v ?? chalk.dim("\u2014")}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function logStderr(message) {
|
|
72
|
+
process.stderr.write(message + "\n");
|
|
73
|
+
}
|
|
35
74
|
|
|
36
75
|
// src/config.ts
|
|
37
76
|
var VALID_COMPONENTS = ["foundation", "data", "app", "all"];
|
|
@@ -169,7 +208,7 @@ async function ensureInit(cwd) {
|
|
|
169
208
|
}
|
|
170
209
|
|
|
171
210
|
// src/ui.ts
|
|
172
|
-
import
|
|
211
|
+
import chalk2 from "chalk";
|
|
173
212
|
import ora from "ora";
|
|
174
213
|
var TIER_LABELS = {
|
|
175
214
|
foundation: "Foundation",
|
|
@@ -178,38 +217,38 @@ var TIER_LABELS = {
|
|
|
178
217
|
};
|
|
179
218
|
function printHeader(command, stage, identity) {
|
|
180
219
|
console.log("");
|
|
181
|
-
console.log(
|
|
182
|
-
console.log(
|
|
220
|
+
console.log(chalk2.bold.cyan(" \u2B21 Thinkwork") + chalk2.dim(` \u2014 ${command}`));
|
|
221
|
+
console.log(chalk2.dim(` Stage: ${chalk2.white(stage)}`));
|
|
183
222
|
if (identity) {
|
|
184
|
-
console.log(
|
|
223
|
+
console.log(chalk2.dim(` AWS: ${chalk2.white(identity.account)} / ${chalk2.white(identity.region)}`));
|
|
185
224
|
}
|
|
186
225
|
console.log("");
|
|
187
226
|
}
|
|
188
227
|
function printTierHeader(tier, index, total) {
|
|
189
228
|
const label = TIER_LABELS[tier] ?? tier;
|
|
190
|
-
const progress =
|
|
191
|
-
console.log(` ${progress} ${
|
|
229
|
+
const progress = chalk2.dim(`[${index + 1}/${total}]`);
|
|
230
|
+
console.log(` ${progress} ${chalk2.bold(label)}`);
|
|
192
231
|
}
|
|
193
232
|
function printSuccess(message) {
|
|
194
233
|
console.log(`
|
|
195
|
-
${
|
|
234
|
+
${chalk2.green("\u2713")} ${chalk2.bold(message)}`);
|
|
196
235
|
}
|
|
197
236
|
function printError(message) {
|
|
198
237
|
console.log(`
|
|
199
|
-
${
|
|
238
|
+
${chalk2.red("\u2717")} ${chalk2.bold.red(message)}`);
|
|
200
239
|
}
|
|
201
240
|
function printWarning(message) {
|
|
202
|
-
console.log(` ${
|
|
241
|
+
console.log(` ${chalk2.yellow("\u26A0")} ${message}`);
|
|
203
242
|
}
|
|
204
243
|
function printSummary(command, stage, tiers, startTime) {
|
|
205
244
|
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
206
245
|
console.log("");
|
|
207
|
-
console.log(
|
|
208
|
-
console.log(` ${
|
|
209
|
-
console.log(` ${
|
|
210
|
-
console.log(` ${
|
|
211
|
-
console.log(` ${
|
|
212
|
-
console.log(
|
|
246
|
+
console.log(chalk2.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"));
|
|
247
|
+
console.log(` ${chalk2.bold("Command:")} ${command}`);
|
|
248
|
+
console.log(` ${chalk2.bold("Stage:")} ${stage}`);
|
|
249
|
+
console.log(` ${chalk2.bold("Tiers:")} ${tiers.map((t) => TIER_LABELS[t] ?? t).join(" \u2192 ")}`);
|
|
250
|
+
console.log(` ${chalk2.bold("Time:")} ${elapsed}s`);
|
|
251
|
+
console.log(chalk2.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"));
|
|
213
252
|
}
|
|
214
253
|
|
|
215
254
|
// src/commands/plan.ts
|
|
@@ -384,7 +423,7 @@ function registerDestroyCommand(program2) {
|
|
|
384
423
|
}
|
|
385
424
|
|
|
386
425
|
// src/commands/doctor.ts
|
|
387
|
-
import
|
|
426
|
+
import chalk3 from "chalk";
|
|
388
427
|
import { execSync as execSync2 } from "child_process";
|
|
389
428
|
function checkAwsCli() {
|
|
390
429
|
return {
|
|
@@ -461,17 +500,17 @@ function registerDoctorCommand(program2) {
|
|
|
461
500
|
let allPass = true;
|
|
462
501
|
for (const check of checks) {
|
|
463
502
|
const result = check.run();
|
|
464
|
-
const icon = result.pass ?
|
|
465
|
-
const detail = result.pass ?
|
|
503
|
+
const icon = result.pass ? chalk3.green("\u2713") : chalk3.red("\u2717");
|
|
504
|
+
const detail = result.pass ? chalk3.dim(result.detail) : chalk3.yellow(result.detail);
|
|
466
505
|
console.log(` ${icon} ${check.name} ${detail}`);
|
|
467
506
|
if (!result.pass) allPass = false;
|
|
468
507
|
}
|
|
469
508
|
if (allPass) {
|
|
470
509
|
console.log(`
|
|
471
|
-
${
|
|
510
|
+
${chalk3.green.bold("All checks passed.")}`);
|
|
472
511
|
} else {
|
|
473
512
|
console.log(`
|
|
474
|
-
${
|
|
513
|
+
${chalk3.yellow.bold("Some checks failed.")} Fix the issues above before deploying.`);
|
|
475
514
|
}
|
|
476
515
|
process.exit(allPass ? 0 : 1);
|
|
477
516
|
});
|
|
@@ -510,7 +549,7 @@ function registerOutputsCommand(program2) {
|
|
|
510
549
|
|
|
511
550
|
// src/commands/config.ts
|
|
512
551
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
513
|
-
import
|
|
552
|
+
import chalk4 from "chalk";
|
|
514
553
|
|
|
515
554
|
// src/environments.ts
|
|
516
555
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
@@ -604,20 +643,20 @@ function registerConfigCommand(program2) {
|
|
|
604
643
|
process.exit(1);
|
|
605
644
|
}
|
|
606
645
|
console.log("");
|
|
607
|
-
console.log(
|
|
608
|
-
console.log(
|
|
609
|
-
console.log(` ${
|
|
610
|
-
console.log(` ${
|
|
611
|
-
console.log(` ${
|
|
612
|
-
console.log(` ${
|
|
613
|
-
console.log(` ${
|
|
614
|
-
console.log(` ${
|
|
615
|
-
console.log(` ${
|
|
616
|
-
console.log(
|
|
646
|
+
console.log(chalk4.bold.cyan(` \u2B21 ${env.stage}`));
|
|
647
|
+
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"));
|
|
648
|
+
console.log(` ${chalk4.bold("Region:")} ${env.region}`);
|
|
649
|
+
console.log(` ${chalk4.bold("Account:")} ${env.accountId}`);
|
|
650
|
+
console.log(` ${chalk4.bold("Database:")} ${env.databaseEngine}`);
|
|
651
|
+
console.log(` ${chalk4.bold("Memory:")} managed (always on)${env.enableHindsight ? " + hindsight" : ""}`);
|
|
652
|
+
console.log(` ${chalk4.bold("Terraform dir:")} ${env.terraformDir}`);
|
|
653
|
+
console.log(` ${chalk4.bold("Created:")} ${env.createdAt}`);
|
|
654
|
+
console.log(` ${chalk4.bold("Updated:")} ${env.updatedAt}`);
|
|
655
|
+
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"));
|
|
617
656
|
const tfvarsPath = `${env.terraformDir}/terraform.tfvars`;
|
|
618
657
|
if (existsSync4(tfvarsPath)) {
|
|
619
658
|
console.log("");
|
|
620
|
-
console.log(
|
|
659
|
+
console.log(chalk4.dim(" terraform.tfvars:"));
|
|
621
660
|
const content = readFileSync3(tfvarsPath, "utf-8");
|
|
622
661
|
for (const line of content.split("\n")) {
|
|
623
662
|
if (line.trim() && !line.trim().startsWith("#")) {
|
|
@@ -631,7 +670,7 @@ function registerConfigCommand(program2) {
|
|
|
631
670
|
/^(google_oauth_client_secret\s*=\s*)".*"/,
|
|
632
671
|
'$1"********"'
|
|
633
672
|
);
|
|
634
|
-
console.log(` ${
|
|
673
|
+
console.log(` ${chalk4.dim(masked)}`);
|
|
635
674
|
}
|
|
636
675
|
}
|
|
637
676
|
}
|
|
@@ -642,24 +681,24 @@ function registerConfigCommand(program2) {
|
|
|
642
681
|
if (envs.length === 0) {
|
|
643
682
|
console.log("");
|
|
644
683
|
console.log(" No environments found.");
|
|
645
|
-
console.log(` Run ${
|
|
684
|
+
console.log(` Run ${chalk4.cyan("thinkwork init -s <stage>")} to create one.`);
|
|
646
685
|
console.log("");
|
|
647
686
|
return;
|
|
648
687
|
}
|
|
649
688
|
console.log("");
|
|
650
|
-
console.log(
|
|
651
|
-
console.log(
|
|
689
|
+
console.log(chalk4.bold(" Environments"));
|
|
690
|
+
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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
652
691
|
for (const env of envs) {
|
|
653
|
-
const memBadge = env.enableHindsight ?
|
|
654
|
-
const dbBadge = env.databaseEngine === "rds-postgres" ?
|
|
692
|
+
const memBadge = env.enableHindsight ? chalk4.magenta("managed+hindsight") : chalk4.dim("managed");
|
|
693
|
+
const dbBadge = env.databaseEngine === "rds-postgres" ? chalk4.yellow("rds") : chalk4.dim("aurora");
|
|
655
694
|
console.log(
|
|
656
|
-
` ${
|
|
695
|
+
` ${chalk4.bold.cyan(env.stage.padEnd(16))}${env.region.padEnd(14)}${env.accountId.padEnd(16)}${dbBadge.padEnd(20)}${memBadge}`
|
|
657
696
|
);
|
|
658
697
|
}
|
|
659
|
-
console.log(
|
|
660
|
-
console.log(
|
|
698
|
+
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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
699
|
+
console.log(chalk4.dim(` ${envs.length} environment(s)`));
|
|
661
700
|
console.log("");
|
|
662
|
-
console.log(` Show details: ${
|
|
701
|
+
console.log(` Show details: ${chalk4.cyan("thinkwork config list -s <stage>")}`);
|
|
663
702
|
console.log("");
|
|
664
703
|
});
|
|
665
704
|
config.command("get <key>").description("Get a configuration value (e.g. enable-hindsight)").requiredOption("-s, --stage <name>", "Deployment stage").action((key, opts) => {
|
|
@@ -776,8 +815,8 @@ function registerBootstrapCommand(program2) {
|
|
|
776
815
|
bucket = await getTerraformOutput(cwd, "bucket_name");
|
|
777
816
|
dbEndpoint = await getTerraformOutput(cwd, "db_cluster_endpoint");
|
|
778
817
|
const secretArn = await getTerraformOutput(cwd, "db_secret_arn");
|
|
779
|
-
const { execSync:
|
|
780
|
-
const secretJson =
|
|
818
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
819
|
+
const secretJson = execSync11(
|
|
781
820
|
`aws secretsmanager get-secret-value --secret-id "${secretArn}" --query SecretString --output text`,
|
|
782
821
|
{ encoding: "utf-8" }
|
|
783
822
|
).trim();
|
|
@@ -800,10 +839,10 @@ function registerBootstrapCommand(program2) {
|
|
|
800
839
|
}
|
|
801
840
|
|
|
802
841
|
// src/commands/login.ts
|
|
803
|
-
import { execSync as
|
|
842
|
+
import { execSync as execSync6 } from "child_process";
|
|
804
843
|
import { createInterface as createInterface2 } from "readline";
|
|
805
844
|
import { select, Separator } from "@inquirer/prompts";
|
|
806
|
-
import
|
|
845
|
+
import chalk7 from "chalk";
|
|
807
846
|
|
|
808
847
|
// src/aws-profiles.ts
|
|
809
848
|
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
@@ -886,7 +925,7 @@ import { execSync as execSync3 } from "child_process";
|
|
|
886
925
|
import { mkdirSync as mkdirSync3, createWriteStream, chmodSync } from "fs";
|
|
887
926
|
import { join as join4 } from "path";
|
|
888
927
|
import { homedir as homedir4, platform, arch } from "os";
|
|
889
|
-
import
|
|
928
|
+
import chalk5 from "chalk";
|
|
890
929
|
function run(cmd, opts) {
|
|
891
930
|
try {
|
|
892
931
|
return execSync3(cmd, {
|
|
@@ -906,12 +945,12 @@ function hasBrew() {
|
|
|
906
945
|
}
|
|
907
946
|
async function ensureAwsCli() {
|
|
908
947
|
if (isInstalled("aws")) return true;
|
|
909
|
-
console.log(` ${
|
|
948
|
+
console.log(` ${chalk5.yellow("\u2192")} AWS CLI not found. Installing...`);
|
|
910
949
|
const os = platform();
|
|
911
950
|
if (os === "darwin" && hasBrew()) {
|
|
912
951
|
const result = run("brew install awscli");
|
|
913
952
|
if (result !== null && isInstalled("aws")) {
|
|
914
|
-
console.log(` ${
|
|
953
|
+
console.log(` ${chalk5.green("\u2713")} AWS CLI installed via Homebrew`);
|
|
915
954
|
return true;
|
|
916
955
|
}
|
|
917
956
|
}
|
|
@@ -926,7 +965,7 @@ async function ensureAwsCli() {
|
|
|
926
965
|
run(`"${tmpDir}/aws/install" --install-dir "${homedir4()}/.thinkwork/aws-cli" --bin-dir "${homedir4()}/.local/bin" --update`);
|
|
927
966
|
process.env.PATH = `${homedir4()}/.local/bin:${process.env.PATH}`;
|
|
928
967
|
if (isInstalled("aws")) {
|
|
929
|
-
console.log(` ${
|
|
968
|
+
console.log(` ${chalk5.green("\u2713")} AWS CLI installed to ~/.local/bin/aws`);
|
|
930
969
|
return true;
|
|
931
970
|
}
|
|
932
971
|
} catch {
|
|
@@ -941,24 +980,24 @@ async function ensureAwsCli() {
|
|
|
941
980
|
run(`curl -sL "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "${pkgPath}"`);
|
|
942
981
|
run(`installer -pkg "${pkgPath}" -target CurrentUserHomeDirectory 2>/dev/null || sudo installer -pkg "${pkgPath}" -target /`);
|
|
943
982
|
if (isInstalled("aws")) {
|
|
944
|
-
console.log(` ${
|
|
983
|
+
console.log(` ${chalk5.green("\u2713")} AWS CLI installed`);
|
|
945
984
|
return true;
|
|
946
985
|
}
|
|
947
986
|
} catch {
|
|
948
987
|
}
|
|
949
988
|
}
|
|
950
|
-
console.log(` ${
|
|
951
|
-
console.log(` Install manually: ${
|
|
989
|
+
console.log(` ${chalk5.red("\u2717")} Could not auto-install AWS CLI.`);
|
|
990
|
+
console.log(` Install manually: ${chalk5.cyan("https://aws.amazon.com/cli/")}`);
|
|
952
991
|
return false;
|
|
953
992
|
}
|
|
954
993
|
async function ensureTerraform() {
|
|
955
994
|
if (isInstalled("terraform")) return true;
|
|
956
|
-
console.log(` ${
|
|
995
|
+
console.log(` ${chalk5.yellow("\u2192")} Terraform not found. Installing...`);
|
|
957
996
|
const os = platform();
|
|
958
997
|
if ((os === "darwin" || os === "linux") && hasBrew()) {
|
|
959
998
|
const result = run("brew install hashicorp/tap/terraform");
|
|
960
999
|
if (result !== null && isInstalled("terraform")) {
|
|
961
|
-
console.log(` ${
|
|
1000
|
+
console.log(` ${chalk5.green("\u2713")} Terraform installed via Homebrew`);
|
|
962
1001
|
return true;
|
|
963
1002
|
}
|
|
964
1003
|
}
|
|
@@ -980,17 +1019,17 @@ async function ensureTerraform() {
|
|
|
980
1019
|
process.env.PATH = `${binDir}:${process.env.PATH}`;
|
|
981
1020
|
}
|
|
982
1021
|
if (isInstalled("terraform")) {
|
|
983
|
-
console.log(` ${
|
|
1022
|
+
console.log(` ${chalk5.green("\u2713")} Terraform ${tfVersion} installed to ~/.local/bin/terraform`);
|
|
984
1023
|
return true;
|
|
985
1024
|
}
|
|
986
1025
|
} catch {
|
|
987
1026
|
}
|
|
988
|
-
console.log(` ${
|
|
989
|
-
console.log(` Install manually: ${
|
|
1027
|
+
console.log(` ${chalk5.red("\u2717")} Could not auto-install Terraform.`);
|
|
1028
|
+
console.log(` Install manually: ${chalk5.cyan("https://developer.hashicorp.com/terraform/install")}`);
|
|
990
1029
|
return false;
|
|
991
1030
|
}
|
|
992
1031
|
async function ensurePrerequisites() {
|
|
993
|
-
console.log(
|
|
1032
|
+
console.log(chalk5.dim(" Checking prerequisites...\n"));
|
|
994
1033
|
const awsOk = await ensureAwsCli();
|
|
995
1034
|
const tfOk = await ensureTerraform();
|
|
996
1035
|
if (awsOk && tfOk) {
|
|
@@ -998,10 +1037,392 @@ async function ensurePrerequisites() {
|
|
|
998
1037
|
return true;
|
|
999
1038
|
}
|
|
1000
1039
|
console.log("");
|
|
1001
|
-
console.log(` ${
|
|
1040
|
+
console.log(` ${chalk5.red("Missing prerequisites.")} Install them and try again.`);
|
|
1002
1041
|
return false;
|
|
1003
1042
|
}
|
|
1004
1043
|
|
|
1044
|
+
// src/cognito-discovery.ts
|
|
1045
|
+
import { execSync as execSync4 } from "child_process";
|
|
1046
|
+
function runAws(cmd) {
|
|
1047
|
+
try {
|
|
1048
|
+
return execSync4(`aws ${cmd}`, {
|
|
1049
|
+
encoding: "utf-8",
|
|
1050
|
+
timeout: 15e3,
|
|
1051
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1052
|
+
}).trim();
|
|
1053
|
+
} catch {
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function tryTerraformOutput(stage) {
|
|
1058
|
+
const tfRoot = resolveTerraformDir(stage);
|
|
1059
|
+
if (!tfRoot) return null;
|
|
1060
|
+
let cwd;
|
|
1061
|
+
try {
|
|
1062
|
+
cwd = resolveTierDir(tfRoot, stage, "foundation");
|
|
1063
|
+
} catch {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
const read = (key) => {
|
|
1067
|
+
try {
|
|
1068
|
+
return execSync4(`terraform output -raw ${key}`, {
|
|
1069
|
+
cwd,
|
|
1070
|
+
encoding: "utf-8",
|
|
1071
|
+
timeout: 15e3,
|
|
1072
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1073
|
+
}).trim();
|
|
1074
|
+
} catch {
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
const userPoolId = read("user_pool_id") ?? void 0;
|
|
1079
|
+
const clientId = read("admin_client_id") ?? void 0;
|
|
1080
|
+
const domain = read("auth_domain") ?? void 0;
|
|
1081
|
+
return { userPoolId, clientId, domain };
|
|
1082
|
+
}
|
|
1083
|
+
function tryAwsDiscovery(stage, region) {
|
|
1084
|
+
const listRaw = runAws(
|
|
1085
|
+
`cognito-idp list-user-pools --max-results 60 --region ${region} --output json`
|
|
1086
|
+
);
|
|
1087
|
+
if (!listRaw) return {};
|
|
1088
|
+
const poolList = JSON.parse(listRaw);
|
|
1089
|
+
const pool = poolList.UserPools.find(
|
|
1090
|
+
(p) => (
|
|
1091
|
+
// `foundation/cognito/main.tf`:93 pattern
|
|
1092
|
+
p.Name === `thinkwork-${stage}-user-pool` || p.Name === `thinkwork-${stage}-users`
|
|
1093
|
+
)
|
|
1094
|
+
);
|
|
1095
|
+
if (!pool) return {};
|
|
1096
|
+
const clientsRaw = runAws(
|
|
1097
|
+
`cognito-idp list-user-pool-clients --user-pool-id ${pool.Id} --region ${region} --output json`
|
|
1098
|
+
);
|
|
1099
|
+
let clientId;
|
|
1100
|
+
if (clientsRaw) {
|
|
1101
|
+
const clients = JSON.parse(clientsRaw);
|
|
1102
|
+
const admin = clients.UserPoolClients.find(
|
|
1103
|
+
(c) => c.ClientName === "ThinkworkAdmin"
|
|
1104
|
+
);
|
|
1105
|
+
clientId = admin?.ClientId;
|
|
1106
|
+
}
|
|
1107
|
+
const domain = `thinkwork-${stage}`;
|
|
1108
|
+
return { userPoolId: pool.Id, clientId, domain };
|
|
1109
|
+
}
|
|
1110
|
+
function discoverCognitoConfig(stage, region) {
|
|
1111
|
+
const fromTf = tryTerraformOutput(stage) ?? {};
|
|
1112
|
+
const fromAws = tryAwsDiscovery(stage, region);
|
|
1113
|
+
const userPoolId = fromTf.userPoolId ?? fromAws.userPoolId;
|
|
1114
|
+
const clientId = fromTf.clientId ?? fromAws.clientId;
|
|
1115
|
+
const domain = fromTf.domain ?? fromAws.domain;
|
|
1116
|
+
if (!userPoolId || !clientId || !domain) return null;
|
|
1117
|
+
return {
|
|
1118
|
+
userPoolId,
|
|
1119
|
+
clientId,
|
|
1120
|
+
domain,
|
|
1121
|
+
domainUrl: `https://${domain}.auth.${region}.amazoncognito.com`,
|
|
1122
|
+
region
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/cognito-oauth.ts
|
|
1127
|
+
import { createServer } from "http";
|
|
1128
|
+
import { randomBytes } from "crypto";
|
|
1129
|
+
import { spawn as spawn3 } from "child_process";
|
|
1130
|
+
import chalk6 from "chalk";
|
|
1131
|
+
var CLI_LOOPBACK_PORT = 42010;
|
|
1132
|
+
var CALLBACK_PATH = "/callback";
|
|
1133
|
+
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1134
|
+
async function loginWithCognito(opts) {
|
|
1135
|
+
const port = opts.port ?? CLI_LOOPBACK_PORT;
|
|
1136
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1137
|
+
const redirectUri = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
|
|
1138
|
+
const state = randomBytes(16).toString("hex");
|
|
1139
|
+
const authorizeUrl = buildAuthorizeUrl(opts.cognito, redirectUri, state);
|
|
1140
|
+
const code = await waitForCallbackCode({
|
|
1141
|
+
port,
|
|
1142
|
+
expectedState: state,
|
|
1143
|
+
timeoutMs,
|
|
1144
|
+
onListening: () => {
|
|
1145
|
+
logStderr("");
|
|
1146
|
+
logStderr(` ${chalk6.cyan("Opening browser to sign in\u2026")}`);
|
|
1147
|
+
logStderr(` ${chalk6.dim("If it doesn't open automatically, visit:")}`);
|
|
1148
|
+
logStderr(` ${chalk6.dim(authorizeUrl)}`);
|
|
1149
|
+
logStderr("");
|
|
1150
|
+
if (opts.openBrowser !== false) {
|
|
1151
|
+
(opts.launchBrowser ?? openInBrowser)(authorizeUrl);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
return exchangeCodeForTokens(opts.cognito, redirectUri, code);
|
|
1156
|
+
}
|
|
1157
|
+
function buildAuthorizeUrl(cognito, redirectUri, state) {
|
|
1158
|
+
const params = new URLSearchParams({
|
|
1159
|
+
client_id: cognito.clientId,
|
|
1160
|
+
response_type: "code",
|
|
1161
|
+
scope: "openid email profile",
|
|
1162
|
+
redirect_uri: redirectUri,
|
|
1163
|
+
state
|
|
1164
|
+
});
|
|
1165
|
+
return `${cognito.domainUrl}/oauth2/authorize?${params.toString()}`;
|
|
1166
|
+
}
|
|
1167
|
+
function waitForCallbackCode(opts) {
|
|
1168
|
+
return new Promise((resolve3, reject) => {
|
|
1169
|
+
const server = createServer((req, res) => handleRequest(req, res));
|
|
1170
|
+
let finished = false;
|
|
1171
|
+
const finish = (err, code) => {
|
|
1172
|
+
if (finished) return;
|
|
1173
|
+
finished = true;
|
|
1174
|
+
clearTimeout(timer);
|
|
1175
|
+
const closer = server;
|
|
1176
|
+
closer.closeAllConnections?.();
|
|
1177
|
+
server.close(() => {
|
|
1178
|
+
if (err) reject(err);
|
|
1179
|
+
else resolve3(code);
|
|
1180
|
+
});
|
|
1181
|
+
};
|
|
1182
|
+
const timer = setTimeout(() => {
|
|
1183
|
+
finish(
|
|
1184
|
+
new Error(
|
|
1185
|
+
`Timed out waiting for sign-in after ${Math.round(opts.timeoutMs / 1e3)}s. Cancel with Ctrl+C and retry.`
|
|
1186
|
+
)
|
|
1187
|
+
);
|
|
1188
|
+
}, opts.timeoutMs);
|
|
1189
|
+
function handleRequest(req, res) {
|
|
1190
|
+
if (!req.url) return;
|
|
1191
|
+
const parsed = new URL(req.url, `http://127.0.0.1:${opts.port}`);
|
|
1192
|
+
if (parsed.pathname !== CALLBACK_PATH) {
|
|
1193
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
1194
|
+
res.end("Not found. Return to your CLI and close this tab.");
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const code = parsed.searchParams.get("code");
|
|
1198
|
+
const state = parsed.searchParams.get("state");
|
|
1199
|
+
const error = parsed.searchParams.get("error");
|
|
1200
|
+
if (error) {
|
|
1201
|
+
const desc = parsed.searchParams.get("error_description") || error;
|
|
1202
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
1203
|
+
res.end(renderErrorPage(desc));
|
|
1204
|
+
finish(new Error(`Cognito returned an error: ${desc}`));
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (!code || !state) {
|
|
1208
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
1209
|
+
res.end("Missing code or state.");
|
|
1210
|
+
finish(new Error("Cognito callback missing code or state parameter."));
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (state !== opts.expectedState) {
|
|
1214
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
1215
|
+
res.end("State mismatch.");
|
|
1216
|
+
finish(
|
|
1217
|
+
new Error(
|
|
1218
|
+
"OAuth state parameter didn't match \u2014 possible CSRF or stale tab. Retry `thinkwork login`."
|
|
1219
|
+
)
|
|
1220
|
+
);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", connection: "close" });
|
|
1224
|
+
res.end(renderSuccessPage());
|
|
1225
|
+
finish(null, code);
|
|
1226
|
+
}
|
|
1227
|
+
server.on("error", (err) => {
|
|
1228
|
+
if (err.code === "EADDRINUSE") {
|
|
1229
|
+
finish(
|
|
1230
|
+
new Error(
|
|
1231
|
+
`Port ${opts.port} is in use. Stop the conflicting process (another \`thinkwork login\`?) and retry.`
|
|
1232
|
+
)
|
|
1233
|
+
);
|
|
1234
|
+
} else {
|
|
1235
|
+
finish(err);
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
server.listen(opts.port, "127.0.0.1", () => {
|
|
1239
|
+
opts.onListening?.();
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
async function exchangeCodeForTokens(cognito, redirectUri, code) {
|
|
1244
|
+
const body = new URLSearchParams({
|
|
1245
|
+
grant_type: "authorization_code",
|
|
1246
|
+
client_id: cognito.clientId,
|
|
1247
|
+
code,
|
|
1248
|
+
redirect_uri: redirectUri
|
|
1249
|
+
});
|
|
1250
|
+
const res = await fetch(`${cognito.domainUrl}/oauth2/token`, {
|
|
1251
|
+
method: "POST",
|
|
1252
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1253
|
+
body: body.toString()
|
|
1254
|
+
});
|
|
1255
|
+
if (!res.ok) {
|
|
1256
|
+
const text = await res.text().catch(() => "");
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`Token exchange failed (HTTP ${res.status}): ${text || "no body"}`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
const json = await res.json();
|
|
1262
|
+
return {
|
|
1263
|
+
idToken: json.id_token,
|
|
1264
|
+
accessToken: json.access_token,
|
|
1265
|
+
refreshToken: json.refresh_token,
|
|
1266
|
+
expiresAt: Math.floor(Date.now() / 1e3) + json.expires_in
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
async function refreshCognitoTokens(cognito, refreshToken) {
|
|
1270
|
+
const body = new URLSearchParams({
|
|
1271
|
+
grant_type: "refresh_token",
|
|
1272
|
+
client_id: cognito.clientId,
|
|
1273
|
+
refresh_token: refreshToken
|
|
1274
|
+
});
|
|
1275
|
+
const res = await fetch(`${cognito.domainUrl}/oauth2/token`, {
|
|
1276
|
+
method: "POST",
|
|
1277
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1278
|
+
body: body.toString()
|
|
1279
|
+
});
|
|
1280
|
+
if (!res.ok) {
|
|
1281
|
+
const text = await res.text().catch(() => "");
|
|
1282
|
+
throw new Error(
|
|
1283
|
+
`Token refresh failed (HTTP ${res.status}): ${text || "no body"}`
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
const json = await res.json();
|
|
1287
|
+
return {
|
|
1288
|
+
idToken: json.id_token,
|
|
1289
|
+
accessToken: json.access_token,
|
|
1290
|
+
expiresAt: Math.floor(Date.now() / 1e3) + json.expires_in
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
function decodeIdToken(idToken) {
|
|
1294
|
+
const parts = idToken.split(".");
|
|
1295
|
+
if (parts.length !== 3) {
|
|
1296
|
+
throw new Error("Malformed id_token (expected 3 parts).");
|
|
1297
|
+
}
|
|
1298
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
1299
|
+
const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
|
|
1300
|
+
const json = Buffer.from(padded, "base64").toString("utf-8");
|
|
1301
|
+
return JSON.parse(json);
|
|
1302
|
+
}
|
|
1303
|
+
function openInBrowser(url) {
|
|
1304
|
+
const platform2 = process.platform;
|
|
1305
|
+
const cmd = platform2 === "darwin" ? "open" : platform2 === "win32" ? "cmd" : "xdg-open";
|
|
1306
|
+
const args = platform2 === "win32" ? ["/c", "start", "", url] : [url];
|
|
1307
|
+
try {
|
|
1308
|
+
spawn3(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
1309
|
+
} catch {
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function renderSuccessPage() {
|
|
1313
|
+
return `<!doctype html>
|
|
1314
|
+
<html><head><meta charset="utf-8"><title>Thinkwork \u2014 signed in</title>
|
|
1315
|
+
<style>
|
|
1316
|
+
body { font-family: -apple-system, system-ui, sans-serif; background: #0a0a0a; color: #e5e5e5; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
1317
|
+
.card { padding: 2rem 3rem; background: #171717; border: 1px solid #262626; border-radius: 12px; text-align: center; max-width: 28rem; }
|
|
1318
|
+
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
|
|
1319
|
+
p { color: #a3a3a3; margin: 0; line-height: 1.5; }
|
|
1320
|
+
.check { color: #10b981; font-size: 2rem; }
|
|
1321
|
+
</style>
|
|
1322
|
+
</head><body>
|
|
1323
|
+
<div class="card">
|
|
1324
|
+
<div class="check">\u2713</div>
|
|
1325
|
+
<h1>Signed in to Thinkwork</h1>
|
|
1326
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
1327
|
+
</div>
|
|
1328
|
+
</body></html>`;
|
|
1329
|
+
}
|
|
1330
|
+
function renderErrorPage(message) {
|
|
1331
|
+
return `<!doctype html>
|
|
1332
|
+
<html><head><meta charset="utf-8"><title>Thinkwork \u2014 sign-in error</title>
|
|
1333
|
+
<style>
|
|
1334
|
+
body { font-family: -apple-system, system-ui, sans-serif; background: #0a0a0a; color: #e5e5e5; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
1335
|
+
.card { padding: 2rem 3rem; background: #171717; border: 1px solid #262626; border-radius: 12px; text-align: center; max-width: 28rem; }
|
|
1336
|
+
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
|
|
1337
|
+
p { color: #fca5a5; margin: 0; line-height: 1.5; }
|
|
1338
|
+
code { display: block; margin-top: 1rem; padding: 0.75rem; background: #0a0a0a; border-radius: 6px; color: #a3a3a3; text-align: left; white-space: pre-wrap; word-break: break-word; font-size: 0.875rem; }
|
|
1339
|
+
</style>
|
|
1340
|
+
</head><body>
|
|
1341
|
+
<div class="card">
|
|
1342
|
+
<h1>Sign-in failed</h1>
|
|
1343
|
+
<p>Return to your terminal for details.</p>
|
|
1344
|
+
<code>${escapeHtml(message)}</code>
|
|
1345
|
+
</div>
|
|
1346
|
+
</body></html>`;
|
|
1347
|
+
}
|
|
1348
|
+
function escapeHtml(s) {
|
|
1349
|
+
return s.replace(/[&<>"']/g, (c) => {
|
|
1350
|
+
switch (c) {
|
|
1351
|
+
case "&":
|
|
1352
|
+
return "&";
|
|
1353
|
+
case "<":
|
|
1354
|
+
return "<";
|
|
1355
|
+
case ">":
|
|
1356
|
+
return ">";
|
|
1357
|
+
case '"':
|
|
1358
|
+
return """;
|
|
1359
|
+
case "'":
|
|
1360
|
+
return "'";
|
|
1361
|
+
default:
|
|
1362
|
+
return c;
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/aws-discovery.ts
|
|
1368
|
+
import { execSync as execSync5 } from "child_process";
|
|
1369
|
+
function runAws2(cmd) {
|
|
1370
|
+
try {
|
|
1371
|
+
return execSync5(`aws ${cmd}`, {
|
|
1372
|
+
encoding: "utf-8",
|
|
1373
|
+
timeout: 15e3,
|
|
1374
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1375
|
+
}).trim();
|
|
1376
|
+
} catch {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function listDeployedStages(region) {
|
|
1381
|
+
const raw = runAws2(
|
|
1382
|
+
`lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
|
|
1383
|
+
);
|
|
1384
|
+
if (!raw) return [];
|
|
1385
|
+
try {
|
|
1386
|
+
const functions = JSON.parse(raw);
|
|
1387
|
+
const stages = /* @__PURE__ */ new Set();
|
|
1388
|
+
for (const fn of functions) {
|
|
1389
|
+
const m = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
|
|
1390
|
+
if (m) stages.add(m[1]);
|
|
1391
|
+
}
|
|
1392
|
+
return [...stages].sort();
|
|
1393
|
+
} catch {
|
|
1394
|
+
return [];
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function getApiEndpoint(stage, region) {
|
|
1398
|
+
const raw = runAws2(
|
|
1399
|
+
`apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
|
|
1400
|
+
);
|
|
1401
|
+
return raw && raw !== "None" ? raw : null;
|
|
1402
|
+
}
|
|
1403
|
+
function getApiAuthSecretFromLambda(stage, region) {
|
|
1404
|
+
const raw = runAws2(
|
|
1405
|
+
`lambda get-function-configuration --function-name thinkwork-${stage}-api-tenants --region ${region} --query "Environment.Variables.API_AUTH_SECRET" --output text`
|
|
1406
|
+
);
|
|
1407
|
+
return raw && raw !== "None" ? raw : null;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// src/lib/interactive.ts
|
|
1411
|
+
function isCancellation(err) {
|
|
1412
|
+
return err instanceof Error && err.name === "ExitPromptError";
|
|
1413
|
+
}
|
|
1414
|
+
function isInteractive() {
|
|
1415
|
+
return Boolean(process.stdin.isTTY);
|
|
1416
|
+
}
|
|
1417
|
+
function requireTty(label) {
|
|
1418
|
+
if (!isInteractive()) {
|
|
1419
|
+
printError(
|
|
1420
|
+
`${label} is required. Pass it as a flag or re-run in an interactive terminal.`
|
|
1421
|
+
);
|
|
1422
|
+
process.exit(1);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1005
1426
|
// src/commands/login.ts
|
|
1006
1427
|
function ask(prompt2) {
|
|
1007
1428
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
@@ -1014,7 +1435,7 @@ function ask(prompt2) {
|
|
|
1014
1435
|
}
|
|
1015
1436
|
function verifyProfile(profile) {
|
|
1016
1437
|
try {
|
|
1017
|
-
const raw =
|
|
1438
|
+
const raw = execSync6(
|
|
1018
1439
|
`aws sts get-caller-identity --profile ${profile} --output json`,
|
|
1019
1440
|
{ encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
|
|
1020
1441
|
);
|
|
@@ -1044,7 +1465,7 @@ async function pickProfile(profiles) {
|
|
|
1044
1465
|
return { kind: "cancel" };
|
|
1045
1466
|
}
|
|
1046
1467
|
const choices = profiles.map((p) => ({
|
|
1047
|
-
name: `${p.name} ${
|
|
1468
|
+
name: `${p.name} ${chalk7.dim(`(${describeType(p.type)})`)}`,
|
|
1048
1469
|
value: { kind: "existing", name: p.name }
|
|
1049
1470
|
}));
|
|
1050
1471
|
choices.push(new Separator());
|
|
@@ -1091,15 +1512,15 @@ async function runKeyEntry(targetProfile) {
|
|
|
1091
1512
|
const region = await ask(" Default region [us-east-1]: ");
|
|
1092
1513
|
const finalRegion = region || "us-east-1";
|
|
1093
1514
|
try {
|
|
1094
|
-
|
|
1515
|
+
execSync6(
|
|
1095
1516
|
`aws configure set aws_access_key_id "${accessKeyId}" --profile ${targetProfile}`,
|
|
1096
1517
|
{ stdio: "pipe" }
|
|
1097
1518
|
);
|
|
1098
|
-
|
|
1519
|
+
execSync6(
|
|
1099
1520
|
`aws configure set aws_secret_access_key "${secretAccessKey}" --profile ${targetProfile}`,
|
|
1100
1521
|
{ stdio: "pipe" }
|
|
1101
1522
|
);
|
|
1102
|
-
|
|
1523
|
+
execSync6(
|
|
1103
1524
|
`aws configure set region "${finalRegion}" --profile ${targetProfile}`,
|
|
1104
1525
|
{ stdio: "pipe" }
|
|
1105
1526
|
);
|
|
@@ -1113,7 +1534,7 @@ function runSsoLogin(targetProfile) {
|
|
|
1113
1534
|
console.log(" Launching AWS SSO login...");
|
|
1114
1535
|
console.log("");
|
|
1115
1536
|
try {
|
|
1116
|
-
|
|
1537
|
+
execSync6(`aws sso login --profile ${targetProfile}`, { stdio: "inherit" });
|
|
1117
1538
|
return true;
|
|
1118
1539
|
} catch {
|
|
1119
1540
|
printError(
|
|
@@ -1122,7 +1543,7 @@ function runSsoLogin(targetProfile) {
|
|
|
1122
1543
|
return false;
|
|
1123
1544
|
}
|
|
1124
1545
|
}
|
|
1125
|
-
function
|
|
1546
|
+
function finalizeAws(profile, mode) {
|
|
1126
1547
|
const identity = getAwsIdentity();
|
|
1127
1548
|
if (!identity) {
|
|
1128
1549
|
printError(
|
|
@@ -1142,109 +1563,361 @@ function finalize(profile, mode) {
|
|
|
1142
1563
|
` (\`thinkwork list\`, \`thinkwork deploy\`, \u2026) will use it automatically.`
|
|
1143
1564
|
);
|
|
1144
1565
|
console.log(
|
|
1145
|
-
|
|
1566
|
+
chalk7.dim(
|
|
1146
1567
|
` Override per-command with --profile <other>, or unset with \`rm ~/.thinkwork/config.json\`.`
|
|
1147
1568
|
)
|
|
1148
1569
|
);
|
|
1149
1570
|
}
|
|
1571
|
+
async function bootstrapUserAndTenant(stage, region, idToken) {
|
|
1572
|
+
const baseUrl = getApiEndpoint(stage, region);
|
|
1573
|
+
if (!baseUrl) return null;
|
|
1574
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/graphql`;
|
|
1575
|
+
const query = `mutation BootstrapLogin {
|
|
1576
|
+
bootstrapUser {
|
|
1577
|
+
tenant { id slug name }
|
|
1578
|
+
}
|
|
1579
|
+
}`;
|
|
1580
|
+
try {
|
|
1581
|
+
const res = await fetch(url, {
|
|
1582
|
+
method: "POST",
|
|
1583
|
+
headers: {
|
|
1584
|
+
"content-type": "application/json",
|
|
1585
|
+
Authorization: idToken
|
|
1586
|
+
},
|
|
1587
|
+
body: JSON.stringify({ query })
|
|
1588
|
+
});
|
|
1589
|
+
if (!res.ok) return null;
|
|
1590
|
+
const json = await res.json();
|
|
1591
|
+
const tenant = json.data?.bootstrapUser?.tenant;
|
|
1592
|
+
if (!tenant) return null;
|
|
1593
|
+
return {
|
|
1594
|
+
tenantId: tenant.id,
|
|
1595
|
+
tenantSlug: tenant.slug,
|
|
1596
|
+
tenantName: tenant.name
|
|
1597
|
+
};
|
|
1598
|
+
} catch {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
async function doCognitoLogin(opts) {
|
|
1603
|
+
printHeader("login", opts.stage);
|
|
1604
|
+
const cognito = discoverCognitoConfig(opts.stage, opts.region);
|
|
1605
|
+
if (!cognito) {
|
|
1606
|
+
printError(
|
|
1607
|
+
`Could not find a Cognito user pool for stage "${opts.stage}" in ${opts.region}. Is the stack deployed?`
|
|
1608
|
+
);
|
|
1609
|
+
process.exit(1);
|
|
1610
|
+
}
|
|
1611
|
+
console.log(` User pool: ${cognito.userPoolId}`);
|
|
1612
|
+
console.log(` Client: ${cognito.clientId}`);
|
|
1613
|
+
console.log(` Hosted UI: ${cognito.domainUrl}`);
|
|
1614
|
+
console.log(` Callback port: ${opts.port}`);
|
|
1615
|
+
try {
|
|
1616
|
+
const tokens = await loginWithCognito({
|
|
1617
|
+
cognito,
|
|
1618
|
+
port: opts.port,
|
|
1619
|
+
openBrowser: !opts.noBrowser
|
|
1620
|
+
});
|
|
1621
|
+
const claims = decodeIdToken(tokens.idToken);
|
|
1622
|
+
const bootstrap = await bootstrapUserAndTenant(
|
|
1623
|
+
opts.stage,
|
|
1624
|
+
opts.region,
|
|
1625
|
+
tokens.idToken
|
|
1626
|
+
);
|
|
1627
|
+
saveStageSession(opts.stage, {
|
|
1628
|
+
kind: "cognito",
|
|
1629
|
+
idToken: tokens.idToken,
|
|
1630
|
+
accessToken: tokens.accessToken,
|
|
1631
|
+
refreshToken: tokens.refreshToken,
|
|
1632
|
+
expiresAt: tokens.expiresAt,
|
|
1633
|
+
userPoolId: cognito.userPoolId,
|
|
1634
|
+
userPoolClientId: cognito.clientId,
|
|
1635
|
+
cognitoDomain: cognito.domain,
|
|
1636
|
+
region: cognito.region,
|
|
1637
|
+
principalId: claims.sub,
|
|
1638
|
+
email: claims.email,
|
|
1639
|
+
tenantId: bootstrap?.tenantId,
|
|
1640
|
+
tenantSlug: bootstrap?.tenantSlug
|
|
1641
|
+
});
|
|
1642
|
+
saveCliConfig({ defaultStage: opts.stage });
|
|
1643
|
+
printSuccess(`Signed in to ${opts.stage} as ${claims.email ?? claims.sub}`);
|
|
1644
|
+
if (bootstrap) {
|
|
1645
|
+
console.log(
|
|
1646
|
+
` Tenant: ${bootstrap.tenantName} (slug: ${bootstrap.tenantSlug})`
|
|
1647
|
+
);
|
|
1648
|
+
} else {
|
|
1649
|
+
printWarning(
|
|
1650
|
+
"Signed in, but could not resolve a default tenant. Commands will prompt or require --tenant <slug> until one is cached."
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
console.log("");
|
|
1654
|
+
console.log(
|
|
1655
|
+
chalk7.dim(
|
|
1656
|
+
` Token expires: ${new Date(tokens.expiresAt * 1e3).toISOString()}. Refreshed automatically.`
|
|
1657
|
+
)
|
|
1658
|
+
);
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
if (isCancellation(err)) {
|
|
1661
|
+
console.log("");
|
|
1662
|
+
console.log(" Cancelled.");
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
printError(
|
|
1666
|
+
`Sign-in failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1667
|
+
);
|
|
1668
|
+
process.exit(1);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async function doApiKeyLogin(opts) {
|
|
1672
|
+
printHeader("login", opts.stage);
|
|
1673
|
+
const baseUrl = getApiEndpoint(opts.stage, opts.region);
|
|
1674
|
+
if (!baseUrl) {
|
|
1675
|
+
printError(
|
|
1676
|
+
`Cannot discover API endpoint for stage "${opts.stage}" in ${opts.region}. Is the stack deployed?`
|
|
1677
|
+
);
|
|
1678
|
+
process.exit(1);
|
|
1679
|
+
}
|
|
1680
|
+
saveStageSession(opts.stage, {
|
|
1681
|
+
kind: "api-key",
|
|
1682
|
+
authSecret: opts.apiKey,
|
|
1683
|
+
tenantId: opts.tenantId,
|
|
1684
|
+
tenantSlug: opts.tenantSlug
|
|
1685
|
+
});
|
|
1686
|
+
saveCliConfig({ defaultStage: opts.stage });
|
|
1687
|
+
printSuccess(`Stored api-key session for stage "${opts.stage}"`);
|
|
1688
|
+
if (opts.tenantSlug) {
|
|
1689
|
+
console.log(` Default tenant: ${opts.tenantSlug}`);
|
|
1690
|
+
} else {
|
|
1691
|
+
printWarning(
|
|
1692
|
+
"No tenant cached. Commands will require --tenant <slug> or THINKWORK_TENANT."
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1150
1696
|
function registerLoginCommand(program2) {
|
|
1151
1697
|
program2.command("login").description(
|
|
1152
|
-
"
|
|
1698
|
+
"Sign in. Without --stage: configure AWS credentials (for deploy / destroy). With --stage <s>: sign in to that stack's Cognito / API and cache a session for API-backed commands."
|
|
1153
1699
|
).option(
|
|
1154
1700
|
"--profile <name>",
|
|
1155
1701
|
"AWS profile name to configure (used when entering new keys or SSO)",
|
|
1156
1702
|
"thinkwork"
|
|
1157
|
-
).option("--sso", "Skip the picker and go straight to SSO login").option("--keys", "Skip the picker and go straight to access-key entry").
|
|
1703
|
+
).option("--sso", "Skip the picker and go straight to SSO login").option("--keys", "Skip the picker and go straight to access-key entry").option(
|
|
1704
|
+
"-s, --stage <name>",
|
|
1705
|
+
"Sign in to a deployed stack instead of configuring AWS credentials"
|
|
1706
|
+
).option(
|
|
1707
|
+
"-r, --region <region>",
|
|
1708
|
+
"AWS region for the stack (defaults to us-east-1)",
|
|
1709
|
+
"us-east-1"
|
|
1710
|
+
).option(
|
|
1711
|
+
"--api-key <secret>",
|
|
1712
|
+
"Non-interactive path: store the api_auth_secret as the session for --stage <s>. Skips the browser."
|
|
1713
|
+
).option(
|
|
1714
|
+
"--tenant <slug>",
|
|
1715
|
+
"Cache this tenant slug on the session (used with --api-key, or to override the tenant chosen by bootstrapUser)."
|
|
1716
|
+
).option(
|
|
1717
|
+
"--port <number>",
|
|
1718
|
+
`Loopback port for Cognito OAuth callback. Must match a registered callback URL. Defaults to ${CLI_LOOPBACK_PORT}.`,
|
|
1719
|
+
String(CLI_LOOPBACK_PORT)
|
|
1720
|
+
).option(
|
|
1721
|
+
"--no-browser",
|
|
1722
|
+
"Don't attempt to open the browser automatically \u2014 print the URL instead."
|
|
1723
|
+
).addHelpText(
|
|
1158
1724
|
"after",
|
|
1159
1725
|
`
|
|
1160
1726
|
Examples:
|
|
1161
|
-
#
|
|
1162
|
-
# and saves it as your Thinkwork default.
|
|
1727
|
+
# Configure AWS credentials (profile picker) \u2014 used before deploy/destroy/list.
|
|
1163
1728
|
$ thinkwork login
|
|
1164
1729
|
|
|
1165
|
-
#
|
|
1166
|
-
$ thinkwork login --
|
|
1730
|
+
# Sign in to a deployed stack with Cognito (opens your browser, supports Google SSO).
|
|
1731
|
+
$ thinkwork login --stage dev
|
|
1732
|
+
|
|
1733
|
+
# Non-interactive CI login against prod using the api_auth_secret.
|
|
1734
|
+
$ thinkwork login --stage prod --api-key "$THINKWORK_API_KEY" --tenant acme
|
|
1167
1735
|
|
|
1168
|
-
#
|
|
1736
|
+
# Print the URL instead of auto-opening (useful over SSH).
|
|
1737
|
+
$ thinkwork login --stage dev --no-browser
|
|
1738
|
+
|
|
1739
|
+
# AWS SSO (no stage)
|
|
1169
1740
|
$ thinkwork login --sso --profile work-sso
|
|
1170
1741
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1742
|
+
How the session is stored:
|
|
1743
|
+
~/.thinkwork/config.json gains \`sessions["<stage>"]\` with either a Cognito
|
|
1744
|
+
id/refresh token pair or the api-key secret + tenant. Subsequent commands
|
|
1745
|
+
resolve auth from this file; Cognito tokens are refreshed transparently.
|
|
1746
|
+
|
|
1747
|
+
Registered callback URL:
|
|
1748
|
+
The Cognito admin client must list \`http://127.0.0.1:${CLI_LOOPBACK_PORT}/callback\` in its
|
|
1749
|
+
callback URLs. The default terraform module already does \u2014 if you deployed
|
|
1750
|
+
before that default existed, run \`terraform apply\` in the foundation tier
|
|
1751
|
+
to pick it up. Or use \`--api-key\` to skip the browser entirely.
|
|
1175
1752
|
`
|
|
1176
|
-
).action(
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1753
|
+
).action(
|
|
1754
|
+
async (opts) => {
|
|
1755
|
+
if (opts.stage) {
|
|
1756
|
+
const check = validateStage(opts.stage);
|
|
1757
|
+
if (!check.valid) {
|
|
1758
|
+
printError(check.error);
|
|
1759
|
+
process.exit(1);
|
|
1760
|
+
}
|
|
1761
|
+
if (opts.apiKey) {
|
|
1762
|
+
await doApiKeyLogin({
|
|
1763
|
+
stage: opts.stage,
|
|
1764
|
+
region: opts.region,
|
|
1765
|
+
apiKey: opts.apiKey,
|
|
1766
|
+
tenantSlug: opts.tenant
|
|
1767
|
+
});
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const port = Number.parseInt(opts.port, 10);
|
|
1771
|
+
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
1772
|
+
printError(`Invalid --port value: "${opts.port}".`);
|
|
1773
|
+
process.exit(1);
|
|
1774
|
+
}
|
|
1775
|
+
await doCognitoLogin({
|
|
1776
|
+
stage: opts.stage,
|
|
1777
|
+
region: opts.region,
|
|
1778
|
+
port,
|
|
1779
|
+
noBrowser: opts.browser === false
|
|
1780
|
+
});
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
printHeader("login", opts.profile);
|
|
1784
|
+
const awsOk = await ensureAwsCli();
|
|
1785
|
+
if (!awsOk) process.exit(1);
|
|
1786
|
+
if (opts.sso) {
|
|
1787
|
+
if (!runSsoLogin(opts.profile)) process.exit(1);
|
|
1788
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
1789
|
+
finalizeAws(opts.profile, "SSO");
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
if (opts.keys) {
|
|
1793
|
+
if (!await runKeyEntry(opts.profile)) process.exit(1);
|
|
1794
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
1795
|
+
finalizeAws(opts.profile, "access keys");
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
const profiles = listAwsProfiles();
|
|
1799
|
+
if (profiles.length === 0) {
|
|
1800
|
+
console.log("");
|
|
1801
|
+
console.log(chalk7.dim(" No AWS profiles found in ~/.aws/."));
|
|
1802
|
+
console.log(
|
|
1803
|
+
chalk7.dim(" Falling through to access-key entry for a new profile.")
|
|
1804
|
+
);
|
|
1805
|
+
if (!await runKeyEntry(opts.profile)) process.exit(1);
|
|
1806
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
1807
|
+
finalizeAws(opts.profile, "access keys");
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const choice = await pickProfile(profiles);
|
|
1811
|
+
if (choice.kind === "cancel") {
|
|
1812
|
+
console.log("");
|
|
1813
|
+
console.log(chalk7.dim(" Cancelled. No changes made."));
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
if (choice.kind === "keys") {
|
|
1817
|
+
if (!await runKeyEntry(opts.profile)) process.exit(1);
|
|
1818
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
1819
|
+
finalizeAws(opts.profile, "access keys");
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
if (choice.kind === "sso") {
|
|
1823
|
+
if (!runSsoLogin(opts.profile)) process.exit(1);
|
|
1824
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
1825
|
+
finalizeAws(opts.profile, "SSO");
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
const picked = choice.name;
|
|
1206
1829
|
console.log("");
|
|
1207
|
-
console.log(
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if (!runSsoLogin(opts.profile)) process.exit(1);
|
|
1218
|
-
process.env.AWS_PROFILE = opts.profile;
|
|
1219
|
-
finalize(opts.profile, "SSO");
|
|
1220
|
-
return;
|
|
1830
|
+
console.log(` Verifying "${picked}"...`);
|
|
1831
|
+
const identity = verifyProfile(picked);
|
|
1832
|
+
if (!identity) {
|
|
1833
|
+
printError(
|
|
1834
|
+
`Could not authenticate with profile "${picked}". If it's an SSO profile, try \`aws sso login --profile ${picked}\` first.`
|
|
1835
|
+
);
|
|
1836
|
+
process.exit(1);
|
|
1837
|
+
}
|
|
1838
|
+
process.env.AWS_PROFILE = picked;
|
|
1839
|
+
finalizeAws(picked, "existing profile");
|
|
1221
1840
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/commands/logout.ts
|
|
1845
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
1846
|
+
function registerLogoutCommand(program2) {
|
|
1847
|
+
program2.command("logout").description(
|
|
1848
|
+
"Forget a stored session. Touches only ~/.thinkwork/config.json; your AWS profile and Cognito pool are untouched."
|
|
1849
|
+
).option("-s, --stage <name>", "Stage whose session to forget").option("--all", "Forget every stage's session").addHelpText(
|
|
1850
|
+
"after",
|
|
1851
|
+
`
|
|
1852
|
+
Examples:
|
|
1853
|
+
# Forget the session for one stage
|
|
1854
|
+
$ thinkwork logout --stage dev
|
|
1855
|
+
|
|
1856
|
+
# Forget every saved session (doesn't affect your AWS profile)
|
|
1857
|
+
$ thinkwork logout --all
|
|
1858
|
+
|
|
1859
|
+
# Pick interactively
|
|
1860
|
+
$ thinkwork logout
|
|
1861
|
+
`
|
|
1862
|
+
).action(async (opts) => {
|
|
1863
|
+
try {
|
|
1864
|
+
if (opts.all) {
|
|
1865
|
+
saveCliConfig({ sessions: {}, defaultStage: void 0 });
|
|
1866
|
+
printHeader("logout", "(all stages)");
|
|
1867
|
+
printSuccess("Cleared every saved stack session.");
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
let stage = opts.stage;
|
|
1871
|
+
if (!stage) {
|
|
1872
|
+
const config2 = loadCliConfig();
|
|
1873
|
+
const keys = Object.keys(config2.sessions ?? {});
|
|
1874
|
+
if (keys.length === 0) {
|
|
1875
|
+
printSuccess("No sessions stored \u2014 nothing to forget.");
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (keys.length === 1) {
|
|
1879
|
+
stage = keys[0];
|
|
1880
|
+
console.log(` Only one session stored: ${stage}`);
|
|
1881
|
+
} else {
|
|
1882
|
+
requireTty("Stage");
|
|
1883
|
+
stage = await select2({
|
|
1884
|
+
message: "Forget which stage's session?",
|
|
1885
|
+
choices: keys.map((s) => ({ name: s, value: s })),
|
|
1886
|
+
loop: false
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
clearStageSession(stage);
|
|
1891
|
+
const config = loadCliConfig();
|
|
1892
|
+
if (config.defaultStage === stage) {
|
|
1893
|
+
saveCliConfig({ defaultStage: void 0 });
|
|
1894
|
+
}
|
|
1895
|
+
printHeader("logout", stage);
|
|
1896
|
+
printSuccess(`Forgot session for "${stage}".`);
|
|
1897
|
+
} catch (err) {
|
|
1898
|
+
if (isCancellation(err)) {
|
|
1899
|
+
console.log(" Cancelled.");
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1227
1902
|
printError(
|
|
1228
|
-
`
|
|
1903
|
+
`Logout failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1229
1904
|
);
|
|
1230
1905
|
process.exit(1);
|
|
1231
1906
|
}
|
|
1232
|
-
process.env.AWS_PROFILE = picked;
|
|
1233
|
-
finalize(picked, "existing profile");
|
|
1234
1907
|
});
|
|
1235
1908
|
}
|
|
1236
1909
|
|
|
1237
1910
|
// src/commands/init.ts
|
|
1238
1911
|
import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, cpSync } from "fs";
|
|
1239
1912
|
import { resolve as resolve2, join as join5, dirname as dirname2 } from "path";
|
|
1240
|
-
import { execSync as
|
|
1913
|
+
import { execSync as execSync7 } from "child_process";
|
|
1241
1914
|
import { fileURLToPath } from "url";
|
|
1242
1915
|
import { createInterface as createInterface3 } from "readline";
|
|
1243
|
-
import
|
|
1916
|
+
import chalk8 from "chalk";
|
|
1244
1917
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1245
1918
|
function ask2(prompt2, defaultVal = "") {
|
|
1246
1919
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
1247
|
-
const suffix = defaultVal ?
|
|
1920
|
+
const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
|
|
1248
1921
|
return new Promise((resolve3) => {
|
|
1249
1922
|
rl.question(` ${prompt2}${suffix}: `, (answer) => {
|
|
1250
1923
|
rl.close();
|
|
@@ -1253,7 +1926,7 @@ function ask2(prompt2, defaultVal = "") {
|
|
|
1253
1926
|
});
|
|
1254
1927
|
}
|
|
1255
1928
|
function choose(prompt2, options, defaultVal) {
|
|
1256
|
-
const optStr = options.map((o) => o === defaultVal ?
|
|
1929
|
+
const optStr = options.map((o) => o === defaultVal ? chalk8.bold(o) : chalk8.dim(o)).join(" / ");
|
|
1257
1930
|
return ask2(`${prompt2} (${optStr})`, defaultVal);
|
|
1258
1931
|
}
|
|
1259
1932
|
function generateSecret(length = 32) {
|
|
@@ -1364,21 +2037,21 @@ function registerInitCommand(program2) {
|
|
|
1364
2037
|
config.admin_url = "http://localhost:5174";
|
|
1365
2038
|
config.mobile_scheme = "thinkwork";
|
|
1366
2039
|
} else {
|
|
1367
|
-
console.log(
|
|
2040
|
+
console.log(chalk8.bold(" Configure your Thinkwork environment\n"));
|
|
1368
2041
|
const defaultRegion = identity.region !== "unknown" ? identity.region : "us-east-1";
|
|
1369
2042
|
config.region = await ask2("AWS Region", defaultRegion);
|
|
1370
2043
|
console.log("");
|
|
1371
|
-
console.log(
|
|
2044
|
+
console.log(chalk8.dim(" \u2500\u2500 Database \u2500\u2500"));
|
|
1372
2045
|
config.database_engine = await choose("Database engine", ["aurora-serverless", "rds-postgres"], "aurora-serverless");
|
|
1373
2046
|
console.log("");
|
|
1374
|
-
console.log(
|
|
1375
|
-
console.log(
|
|
1376
|
-
console.log(
|
|
1377
|
-
console.log(
|
|
2047
|
+
console.log(chalk8.dim(" \u2500\u2500 Memory \u2500\u2500"));
|
|
2048
|
+
console.log(chalk8.dim(" AgentCore managed memory is always on (automatic retention)."));
|
|
2049
|
+
console.log(chalk8.dim(" Hindsight is an optional add-on: ECS Fargate service for"));
|
|
2050
|
+
console.log(chalk8.dim(" semantic + entity-graph retrieval (~$75/mo)."));
|
|
1378
2051
|
const hindsightAnswer = await ask2("Enable Hindsight long-term memory add-on? (y/N)", "N");
|
|
1379
2052
|
config.enable_hindsight = hindsightAnswer.toLowerCase() === "y" ? "true" : "false";
|
|
1380
2053
|
console.log("");
|
|
1381
|
-
console.log(
|
|
2054
|
+
console.log(chalk8.dim(" \u2500\u2500 Auth \u2500\u2500"));
|
|
1382
2055
|
const useGoogle = await ask2("Enable Google OAuth login? (y/N)", "N");
|
|
1383
2056
|
if (useGoogle.toLowerCase() === "y") {
|
|
1384
2057
|
config.google_oauth_client_id = await ask2("Google OAuth Client ID");
|
|
@@ -1388,13 +2061,13 @@ function registerInitCommand(program2) {
|
|
|
1388
2061
|
config.google_oauth_client_secret = "";
|
|
1389
2062
|
}
|
|
1390
2063
|
console.log("");
|
|
1391
|
-
console.log(
|
|
2064
|
+
console.log(chalk8.dim(" \u2500\u2500 Frontend URLs \u2500\u2500"));
|
|
1392
2065
|
config.admin_url = await ask2("Admin UI URL", "http://localhost:5174");
|
|
1393
2066
|
config.mobile_scheme = await ask2("Mobile app URL scheme", "thinkwork");
|
|
1394
2067
|
console.log("");
|
|
1395
|
-
console.log(
|
|
1396
|
-
console.log(
|
|
1397
|
-
console.log(
|
|
2068
|
+
console.log(chalk8.dim(" \u2500\u2500 Secrets (auto-generated) \u2500\u2500"));
|
|
2069
|
+
console.log(chalk8.dim(` DB password: ${config.db_password.slice(0, 8)}...`));
|
|
2070
|
+
console.log(chalk8.dim(` API auth secret: ${config.api_auth_secret.slice(0, 16)}...`));
|
|
1398
2071
|
}
|
|
1399
2072
|
console.log("");
|
|
1400
2073
|
console.log(" Scaffolding Terraform modules...");
|
|
@@ -1599,20 +2272,20 @@ output "agentcore_memory_id" {
|
|
|
1599
2272
|
}
|
|
1600
2273
|
`);
|
|
1601
2274
|
}
|
|
1602
|
-
console.log(` Wrote ${
|
|
2275
|
+
console.log(` Wrote ${chalk8.cyan(tfDir + "/")}`);
|
|
1603
2276
|
console.log("");
|
|
1604
|
-
console.log(
|
|
1605
|
-
console.log(` ${
|
|
1606
|
-
console.log(` ${
|
|
1607
|
-
console.log(` ${
|
|
1608
|
-
console.log(` ${
|
|
1609
|
-
console.log(` ${
|
|
1610
|
-
console.log(` ${
|
|
1611
|
-
console.log(` ${
|
|
1612
|
-
console.log(
|
|
2277
|
+
console.log(chalk8.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"));
|
|
2278
|
+
console.log(` ${chalk8.bold("Stage:")} ${config.stage}`);
|
|
2279
|
+
console.log(` ${chalk8.bold("Region:")} ${config.region}`);
|
|
2280
|
+
console.log(` ${chalk8.bold("Account:")} ${config.account_id}`);
|
|
2281
|
+
console.log(` ${chalk8.bold("Database:")} ${config.database_engine}`);
|
|
2282
|
+
console.log(` ${chalk8.bold("Memory:")} managed (always on)${config.enable_hindsight === "true" ? " + hindsight" : ""}`);
|
|
2283
|
+
console.log(` ${chalk8.bold("Google OAuth:")} ${config.google_oauth_client_id ? "enabled" : "disabled"}`);
|
|
2284
|
+
console.log(` ${chalk8.bold("Directory:")} ${tfDir}`);
|
|
2285
|
+
console.log(chalk8.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"));
|
|
1613
2286
|
console.log("\n Initializing Terraform...\n");
|
|
1614
2287
|
try {
|
|
1615
|
-
|
|
2288
|
+
execSync7("terraform init", { cwd: tfDir, stdio: "inherit" });
|
|
1616
2289
|
} catch {
|
|
1617
2290
|
printWarning("Terraform init failed. Run `thinkwork doctor -s " + opts.stage + "` to check prerequisites.");
|
|
1618
2291
|
return;
|
|
@@ -1631,24 +2304,24 @@ output "agentcore_memory_id" {
|
|
|
1631
2304
|
printSuccess(`Environment "${opts.stage}" initialized`);
|
|
1632
2305
|
console.log("");
|
|
1633
2306
|
console.log(" Next steps:");
|
|
1634
|
-
console.log(` ${
|
|
1635
|
-
console.log(` ${
|
|
1636
|
-
console.log(` ${
|
|
1637
|
-
console.log(` ${
|
|
2307
|
+
console.log(` ${chalk8.cyan("1.")} thinkwork plan -s ${opts.stage} ${chalk8.dim("# Review infrastructure plan")}`);
|
|
2308
|
+
console.log(` ${chalk8.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk8.dim("# Deploy to AWS (~5 min)")}`);
|
|
2309
|
+
console.log(` ${chalk8.cyan("3.")} thinkwork bootstrap -s ${opts.stage} ${chalk8.dim("# Seed workspace files + skills")}`);
|
|
2310
|
+
console.log(` ${chalk8.cyan("4.")} thinkwork outputs -s ${opts.stage} ${chalk8.dim("# Show API URL, Cognito IDs, etc.")}`);
|
|
1638
2311
|
console.log("");
|
|
1639
2312
|
});
|
|
1640
2313
|
}
|
|
1641
2314
|
|
|
1642
2315
|
// src/commands/status.ts
|
|
1643
|
-
import { execSync as
|
|
1644
|
-
import
|
|
2316
|
+
import { execSync as execSync8 } from "child_process";
|
|
2317
|
+
import chalk9 from "chalk";
|
|
1645
2318
|
function link(url, label) {
|
|
1646
2319
|
const text = label || url;
|
|
1647
2320
|
return `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
|
|
1648
2321
|
}
|
|
1649
|
-
function
|
|
2322
|
+
function runAws3(cmd) {
|
|
1650
2323
|
try {
|
|
1651
|
-
return
|
|
2324
|
+
return execSync8(`aws ${cmd}`, {
|
|
1652
2325
|
encoding: "utf-8",
|
|
1653
2326
|
timeout: 15e3,
|
|
1654
2327
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1659,7 +2332,7 @@ function runAws(cmd) {
|
|
|
1659
2332
|
}
|
|
1660
2333
|
function discoverAwsStages(region) {
|
|
1661
2334
|
const stages = /* @__PURE__ */ new Map();
|
|
1662
|
-
const raw =
|
|
2335
|
+
const raw = runAws3(
|
|
1663
2336
|
`lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
|
|
1664
2337
|
);
|
|
1665
2338
|
if (!raw) return stages;
|
|
@@ -1674,38 +2347,38 @@ function discoverAwsStages(region) {
|
|
|
1674
2347
|
for (const [stage, info] of stages) {
|
|
1675
2348
|
const count = functions.filter((f) => f.startsWith(`thinkwork-${stage}-`)).length;
|
|
1676
2349
|
info.lambdaCount = count;
|
|
1677
|
-
const apiRaw =
|
|
2350
|
+
const apiRaw = runAws3(
|
|
1678
2351
|
`apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
|
|
1679
2352
|
);
|
|
1680
2353
|
if (apiRaw && apiRaw !== "None") info.apiEndpoint = apiRaw;
|
|
1681
|
-
const appsyncRaw =
|
|
2354
|
+
const appsyncRaw = runAws3(
|
|
1682
2355
|
`appsync list-graphql-apis --region ${region} --query "graphqlApis[?name=='thinkwork-${stage}-subscriptions'].uris.REALTIME|[0]" --output text`
|
|
1683
2356
|
);
|
|
1684
2357
|
if (appsyncRaw && appsyncRaw !== "None") info.appsyncUrl = appsyncRaw;
|
|
1685
|
-
const appsyncApiRaw =
|
|
2358
|
+
const appsyncApiRaw = runAws3(
|
|
1686
2359
|
`appsync list-graphql-apis --region ${region} --query "graphqlApis[?name=='thinkwork-${stage}-subscriptions'].uris.GRAPHQL|[0]" --output text`
|
|
1687
2360
|
);
|
|
1688
2361
|
if (appsyncApiRaw && appsyncApiRaw !== "None") info.appsyncApiUrl = appsyncApiRaw;
|
|
1689
|
-
const acRaw =
|
|
2362
|
+
const acRaw = runAws3(
|
|
1690
2363
|
`lambda get-function --function-name thinkwork-${stage}-agentcore --region ${region} --query "Configuration.State" --output text 2>/dev/null`
|
|
1691
2364
|
);
|
|
1692
2365
|
info.agentcoreStatus = acRaw || "not deployed";
|
|
1693
|
-
const bucketRaw =
|
|
2366
|
+
const bucketRaw = runAws3(
|
|
1694
2367
|
`s3api head-bucket --bucket thinkwork-${stage}-storage --region ${region} 2>/dev/null && echo "exists"`
|
|
1695
2368
|
);
|
|
1696
2369
|
info.bucketName = bucketRaw ? `thinkwork-${stage}-storage` : void 0;
|
|
1697
|
-
const ecsRaw =
|
|
2370
|
+
const ecsRaw = runAws3(
|
|
1698
2371
|
`ecs describe-services --cluster thinkwork-${stage}-cluster --services thinkwork-${stage}-hindsight --region ${region} --query "services[0].runningCount" --output text 2>/dev/null`
|
|
1699
2372
|
);
|
|
1700
2373
|
if (ecsRaw && ecsRaw !== "None" && ecsRaw !== "0") {
|
|
1701
2374
|
info.hindsightEnabled = true;
|
|
1702
|
-
const albRaw =
|
|
2375
|
+
const albRaw = runAws3(
|
|
1703
2376
|
`elbv2 describe-load-balancers --region ${region} --query "LoadBalancers[?contains(LoadBalancerName, 'tw-${stage}-hindsight')].DNSName|[0]" --output text`
|
|
1704
2377
|
);
|
|
1705
2378
|
if (albRaw && albRaw !== "None") {
|
|
1706
2379
|
info.hindsightEndpoint = `http://${albRaw}`;
|
|
1707
2380
|
try {
|
|
1708
|
-
const health =
|
|
2381
|
+
const health = execSync8(`curl -s --max-time 3 http://${albRaw}/health`, { encoding: "utf-8" }).trim();
|
|
1709
2382
|
info.hindsightHealth = health.includes("healthy") ? "healthy" : "unhealthy";
|
|
1710
2383
|
} catch {
|
|
1711
2384
|
info.hindsightHealth = "unreachable";
|
|
@@ -1714,15 +2387,15 @@ function discoverAwsStages(region) {
|
|
|
1714
2387
|
} else {
|
|
1715
2388
|
info.hindsightEnabled = false;
|
|
1716
2389
|
}
|
|
1717
|
-
const dbRaw =
|
|
2390
|
+
const dbRaw = runAws3(
|
|
1718
2391
|
`rds describe-db-clusters --region ${region} --query "DBClusters[?starts_with(DBClusterIdentifier, 'thinkwork-${stage}')].Endpoint|[0]" --output text`
|
|
1719
2392
|
);
|
|
1720
2393
|
if (dbRaw && dbRaw !== "None") info.dbEndpoint = dbRaw;
|
|
1721
|
-
const ecrRaw =
|
|
2394
|
+
const ecrRaw = runAws3(
|
|
1722
2395
|
`ecr describe-repositories --region ${region} --query "repositories[?repositoryName=='thinkwork-${stage}-agentcore'].repositoryUri|[0]" --output text`
|
|
1723
2396
|
);
|
|
1724
2397
|
if (ecrRaw && ecrRaw !== "None") info.ecrUrl = ecrRaw;
|
|
1725
|
-
const cfJson =
|
|
2398
|
+
const cfJson = runAws3(
|
|
1726
2399
|
`cloudfront list-distributions --query "DistributionList.Items[?contains(Origins.Items[0].DomainName, 'thinkwork-${stage}-')].{Origin:Origins.Items[0].DomainName,Domain:DomainName}" --output json`
|
|
1727
2400
|
);
|
|
1728
2401
|
if (cfJson) {
|
|
@@ -1739,33 +2412,33 @@ function discoverAwsStages(region) {
|
|
|
1739
2412
|
return stages;
|
|
1740
2413
|
}
|
|
1741
2414
|
function printStageDetail(info) {
|
|
1742
|
-
console.log(
|
|
1743
|
-
console.log(
|
|
1744
|
-
console.log(` ${
|
|
1745
|
-
console.log(` ${
|
|
1746
|
-
console.log(` ${
|
|
1747
|
-
console.log(` ${
|
|
1748
|
-
console.log(` ${
|
|
1749
|
-
console.log(` ${
|
|
2415
|
+
console.log(chalk9.bold.cyan(` \u2B21 ${info.stage}`));
|
|
2416
|
+
console.log(chalk9.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\u2500\u2500\u2500\u2500"));
|
|
2417
|
+
console.log(` ${chalk9.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
|
|
2418
|
+
console.log(` ${chalk9.bold("Region:")} ${info.region}`);
|
|
2419
|
+
console.log(` ${chalk9.bold("Account:")} ${info.accountId}`);
|
|
2420
|
+
console.log(` ${chalk9.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
|
|
2421
|
+
console.log(` ${chalk9.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
|
|
2422
|
+
console.log(` ${chalk9.bold("Memory:")} managed (always on)`);
|
|
1750
2423
|
const hindsightLabel = info.hindsightEnabled === void 0 ? "unknown" : info.hindsightEnabled ? info.hindsightHealth || "running" : "disabled";
|
|
1751
|
-
console.log(` ${
|
|
1752
|
-
if (info.bucketName) console.log(` ${
|
|
1753
|
-
if (info.dbEndpoint) console.log(` ${
|
|
1754
|
-
if (info.ecrUrl) console.log(` ${
|
|
2424
|
+
console.log(` ${chalk9.bold("Hindsight:")} ${hindsightLabel}`);
|
|
2425
|
+
if (info.bucketName) console.log(` ${chalk9.bold("S3 bucket:")} ${info.bucketName}`);
|
|
2426
|
+
if (info.dbEndpoint) console.log(` ${chalk9.bold("Database:")} ${info.dbEndpoint}`);
|
|
2427
|
+
if (info.ecrUrl) console.log(` ${chalk9.bold("ECR:")} ${info.ecrUrl}`);
|
|
1755
2428
|
console.log("");
|
|
1756
|
-
console.log(
|
|
2429
|
+
console.log(chalk9.bold(" URLs:"));
|
|
1757
2430
|
if (info.adminUrl) console.log(` Admin: ${link(info.adminUrl)}`);
|
|
1758
2431
|
if (info.docsUrl) console.log(` Docs: ${link(info.docsUrl)}`);
|
|
1759
2432
|
if (info.apiEndpoint) console.log(` API: ${link(info.apiEndpoint)}`);
|
|
1760
2433
|
if (info.appsyncApiUrl) console.log(` AppSync: ${link(info.appsyncApiUrl)}`);
|
|
1761
2434
|
if (info.appsyncUrl) console.log(` WebSocket: ${link(info.appsyncUrl)}`);
|
|
1762
2435
|
if (info.hindsightEndpoint) console.log(` Hindsight: ${link(info.hindsightEndpoint)}`);
|
|
1763
|
-
console.log(
|
|
2436
|
+
console.log(chalk9.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\u2500\u2500\u2500\u2500"));
|
|
1764
2437
|
const local = loadEnvironment(info.stage);
|
|
1765
2438
|
if (local) {
|
|
1766
|
-
console.log(
|
|
2439
|
+
console.log(chalk9.dim(` Terraform dir: ${local.terraformDir}`));
|
|
1767
2440
|
} else {
|
|
1768
|
-
console.log(
|
|
2441
|
+
console.log(chalk9.dim(` No local config. Run: thinkwork init -s ${info.stage}`));
|
|
1769
2442
|
}
|
|
1770
2443
|
console.log("");
|
|
1771
2444
|
}
|
|
@@ -1802,7 +2475,7 @@ and AgentCore for per-stage detail.
|
|
|
1802
2475
|
printError("AWS credentials not configured. Run `thinkwork login` first.");
|
|
1803
2476
|
process.exit(1);
|
|
1804
2477
|
}
|
|
1805
|
-
console.log(
|
|
2478
|
+
console.log(chalk9.dim(" Scanning AWS account for Thinkwork deployments...\n"));
|
|
1806
2479
|
const awsStages = discoverAwsStages(opts.region);
|
|
1807
2480
|
const localEnvs = listEnvironments();
|
|
1808
2481
|
const merged = /* @__PURE__ */ new Map();
|
|
@@ -1837,7 +2510,7 @@ and AgentCore for per-stage detail.
|
|
|
1837
2510
|
}
|
|
1838
2511
|
if (merged.size === 0) {
|
|
1839
2512
|
console.log(" No Thinkwork environments found.");
|
|
1840
|
-
console.log(` Run ${
|
|
2513
|
+
console.log(` Run ${chalk9.cyan("thinkwork init -s <stage>")} to create one.`);
|
|
1841
2514
|
console.log("");
|
|
1842
2515
|
return;
|
|
1843
2516
|
}
|
|
@@ -1848,50 +2521,11 @@ and AgentCore for per-stage detail.
|
|
|
1848
2521
|
}
|
|
1849
2522
|
|
|
1850
2523
|
// src/commands/mcp.ts
|
|
1851
|
-
import
|
|
2524
|
+
import chalk10 from "chalk";
|
|
1852
2525
|
|
|
1853
2526
|
// src/api-client.ts
|
|
1854
2527
|
import { readFileSync as readFileSync5, existsSync as existsSync8 } from "fs";
|
|
1855
|
-
import { execSync as
|
|
1856
|
-
|
|
1857
|
-
// src/aws-discovery.ts
|
|
1858
|
-
import { execSync as execSync7 } from "child_process";
|
|
1859
|
-
function runAws2(cmd) {
|
|
1860
|
-
try {
|
|
1861
|
-
return execSync7(`aws ${cmd}`, {
|
|
1862
|
-
encoding: "utf-8",
|
|
1863
|
-
timeout: 15e3,
|
|
1864
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1865
|
-
}).trim();
|
|
1866
|
-
} catch {
|
|
1867
|
-
return null;
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
function listDeployedStages(region) {
|
|
1871
|
-
const raw = runAws2(
|
|
1872
|
-
`lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
|
|
1873
|
-
);
|
|
1874
|
-
if (!raw) return [];
|
|
1875
|
-
try {
|
|
1876
|
-
const functions = JSON.parse(raw);
|
|
1877
|
-
const stages = /* @__PURE__ */ new Set();
|
|
1878
|
-
for (const fn of functions) {
|
|
1879
|
-
const m = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
|
|
1880
|
-
if (m) stages.add(m[1]);
|
|
1881
|
-
}
|
|
1882
|
-
return [...stages].sort();
|
|
1883
|
-
} catch {
|
|
1884
|
-
return [];
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
function getApiAuthSecretFromLambda(stage, region) {
|
|
1888
|
-
const raw = runAws2(
|
|
1889
|
-
`lambda get-function-configuration --function-name thinkwork-${stage}-api-tenants --region ${region} --query "Environment.Variables.API_AUTH_SECRET" --output text`
|
|
1890
|
-
);
|
|
1891
|
-
return raw && raw !== "None" ? raw : null;
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
// src/api-client.ts
|
|
2528
|
+
import { execSync as execSync9 } from "child_process";
|
|
1895
2529
|
function readTfVar2(tfvarsPath, key) {
|
|
1896
2530
|
if (!existsSync8(tfvarsPath)) return null;
|
|
1897
2531
|
const content = readFileSync5(tfvarsPath, "utf-8");
|
|
@@ -1908,9 +2542,9 @@ function resolveTfvarsPath2(stage) {
|
|
|
1908
2542
|
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
1909
2543
|
return `${cwd}/terraform.tfvars`;
|
|
1910
2544
|
}
|
|
1911
|
-
function
|
|
2545
|
+
function getApiEndpoint2(stage, region) {
|
|
1912
2546
|
try {
|
|
1913
|
-
const raw =
|
|
2547
|
+
const raw = execSync9(
|
|
1914
2548
|
`aws apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`,
|
|
1915
2549
|
{ encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
|
|
1916
2550
|
).trim();
|
|
@@ -1953,7 +2587,7 @@ function resolveApiConfig(stage, regionOverride) {
|
|
|
1953
2587
|
const tfAuthSecret = readTfVar2(tfvarsPath, "api_auth_secret");
|
|
1954
2588
|
const tfRegion = readTfVar2(tfvarsPath, "region");
|
|
1955
2589
|
const region = regionOverride || tfRegion || "us-east-1";
|
|
1956
|
-
const apiUrl =
|
|
2590
|
+
const apiUrl = getApiEndpoint2(stage, region);
|
|
1957
2591
|
if (!apiUrl) {
|
|
1958
2592
|
printError(
|
|
1959
2593
|
`Cannot discover API endpoint for stage "${stage}" in ${region}. Is the stack deployed?`
|
|
@@ -1985,14 +2619,14 @@ function registerMcpCommand(program2) {
|
|
|
1985
2619
|
try {
|
|
1986
2620
|
const { servers } = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {}, { "x-tenant-slug": opts.tenant });
|
|
1987
2621
|
if (!servers || servers.length === 0) {
|
|
1988
|
-
console.log(
|
|
2622
|
+
console.log(chalk10.dim(" No MCP servers registered."));
|
|
1989
2623
|
return;
|
|
1990
2624
|
}
|
|
1991
2625
|
console.log("");
|
|
1992
2626
|
for (const s of servers) {
|
|
1993
|
-
const status = s.enabled ?
|
|
2627
|
+
const status = s.enabled ? chalk10.green("enabled") : chalk10.dim("disabled");
|
|
1994
2628
|
const authLabel = s.authType === "per_user_oauth" ? `OAuth (${s.oauthProvider})` : s.authType === "tenant_api_key" ? "API Key" : "none";
|
|
1995
|
-
console.log(` ${
|
|
2629
|
+
console.log(` ${chalk10.bold(s.name)} ${chalk10.dim(s.slug)} ${status}`);
|
|
1996
2630
|
console.log(` URL: ${s.url}`);
|
|
1997
2631
|
console.log(` Transport: ${s.transport}`);
|
|
1998
2632
|
console.log(` Auth: ${authLabel}`);
|
|
@@ -2067,11 +2701,11 @@ function registerMcpCommand(program2) {
|
|
|
2067
2701
|
if (result.ok) {
|
|
2068
2702
|
printSuccess("Connection successful.");
|
|
2069
2703
|
if (result.tools?.length) {
|
|
2070
|
-
console.log(
|
|
2704
|
+
console.log(chalk10.bold(`
|
|
2071
2705
|
Discovered tools (${result.tools.length}):
|
|
2072
2706
|
`));
|
|
2073
2707
|
for (const t of result.tools) {
|
|
2074
|
-
console.log(` ${
|
|
2708
|
+
console.log(` ${chalk10.cyan(t.name)}${t.description ? chalk10.dim(` - ${t.description}`) : ""}`);
|
|
2075
2709
|
}
|
|
2076
2710
|
console.log("");
|
|
2077
2711
|
} else {
|
|
@@ -2127,7 +2761,7 @@ function registerMcpCommand(program2) {
|
|
|
2127
2761
|
|
|
2128
2762
|
// src/commands/tools.ts
|
|
2129
2763
|
import { createInterface as createInterface4 } from "readline";
|
|
2130
|
-
import
|
|
2764
|
+
import chalk11 from "chalk";
|
|
2131
2765
|
function prompt(question) {
|
|
2132
2766
|
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
2133
2767
|
return new Promise((resolve3) => {
|
|
@@ -2160,16 +2794,16 @@ function registerToolsCommand(program2) {
|
|
|
2160
2794
|
{ "x-tenant-slug": opts.tenant }
|
|
2161
2795
|
);
|
|
2162
2796
|
if (!rows || rows.length === 0) {
|
|
2163
|
-
console.log(
|
|
2164
|
-
console.log(
|
|
2797
|
+
console.log(chalk11.dim(" No built-in tools configured."));
|
|
2798
|
+
console.log(chalk11.dim(" Try: thinkwork tools web-search set --tenant <slug> -s <stage>"));
|
|
2165
2799
|
return;
|
|
2166
2800
|
}
|
|
2167
2801
|
console.log("");
|
|
2168
2802
|
for (const r of rows) {
|
|
2169
|
-
const status = r.enabled ?
|
|
2170
|
-
const key = r.hasSecret ?
|
|
2171
|
-
const provider = r.provider ??
|
|
2172
|
-
console.log(` ${
|
|
2803
|
+
const status = r.enabled ? chalk11.green("enabled") : chalk11.dim("disabled");
|
|
2804
|
+
const key = r.hasSecret ? chalk11.green("yes") : chalk11.red("no");
|
|
2805
|
+
const provider = r.provider ?? chalk11.dim("\u2014");
|
|
2806
|
+
console.log(` ${chalk11.bold(r.toolSlug)} ${status}`);
|
|
2173
2807
|
console.log(` Provider: ${provider}`);
|
|
2174
2808
|
console.log(` Has key: ${key}`);
|
|
2175
2809
|
if (r.lastTestedAt) {
|
|
@@ -2300,12 +2934,12 @@ function registerToolsCommand(program2) {
|
|
|
2300
2934
|
}
|
|
2301
2935
|
|
|
2302
2936
|
// src/commands/update.ts
|
|
2303
|
-
import { execSync as
|
|
2937
|
+
import { execSync as execSync10 } from "child_process";
|
|
2304
2938
|
import { realpathSync } from "fs";
|
|
2305
|
-
import
|
|
2939
|
+
import chalk12 from "chalk";
|
|
2306
2940
|
function getLatestVersion() {
|
|
2307
2941
|
try {
|
|
2308
|
-
return
|
|
2942
|
+
return execSync10("npm view thinkwork-cli version", {
|
|
2309
2943
|
encoding: "utf-8",
|
|
2310
2944
|
timeout: 1e4,
|
|
2311
2945
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2316,7 +2950,7 @@ function getLatestVersion() {
|
|
|
2316
2950
|
}
|
|
2317
2951
|
function detectInstallMethod() {
|
|
2318
2952
|
try {
|
|
2319
|
-
const which =
|
|
2953
|
+
const which = execSync10("which thinkwork", {
|
|
2320
2954
|
encoding: "utf-8",
|
|
2321
2955
|
timeout: 5e3,
|
|
2322
2956
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2344,28 +2978,28 @@ function compareVersions(a, b) {
|
|
|
2344
2978
|
function registerUpdateCommand(program2) {
|
|
2345
2979
|
program2.command("update").description("Check for and install CLI updates").option("--check", "Only check for updates, don't install").action(async (opts) => {
|
|
2346
2980
|
printHeader("update", "", null);
|
|
2347
|
-
console.log(` Current version: ${
|
|
2981
|
+
console.log(` Current version: ${chalk12.bold(VERSION)}`);
|
|
2348
2982
|
const latest = getLatestVersion();
|
|
2349
2983
|
if (!latest) {
|
|
2350
|
-
console.log(
|
|
2984
|
+
console.log(chalk12.yellow(" Could not check npm registry for updates."));
|
|
2351
2985
|
return;
|
|
2352
2986
|
}
|
|
2353
|
-
console.log(` Latest version: ${
|
|
2987
|
+
console.log(` Latest version: ${chalk12.bold(latest)}`);
|
|
2354
2988
|
console.log("");
|
|
2355
2989
|
const cmp = compareVersions(VERSION, latest);
|
|
2356
2990
|
if (cmp >= 0) {
|
|
2357
|
-
console.log(
|
|
2991
|
+
console.log(chalk12.green(" \u2713 You're on the latest version."));
|
|
2358
2992
|
console.log("");
|
|
2359
2993
|
return;
|
|
2360
2994
|
}
|
|
2361
|
-
console.log(
|
|
2995
|
+
console.log(chalk12.cyan(` Update available: ${VERSION} \u2192 ${latest}`));
|
|
2362
2996
|
console.log("");
|
|
2363
2997
|
if (opts.check) {
|
|
2364
2998
|
const method2 = detectInstallMethod();
|
|
2365
2999
|
if (method2 === "homebrew") {
|
|
2366
|
-
console.log(` Run: ${
|
|
3000
|
+
console.log(` Run: ${chalk12.cyan("brew upgrade thinkwork-ai/tap/thinkwork")}`);
|
|
2367
3001
|
} else {
|
|
2368
|
-
console.log(` Run: ${
|
|
3002
|
+
console.log(` Run: ${chalk12.cyan(`npm install -g thinkwork-cli@${latest}`)}`);
|
|
2369
3003
|
}
|
|
2370
3004
|
console.log("");
|
|
2371
3005
|
return;
|
|
@@ -2373,27 +3007,27 @@ function registerUpdateCommand(program2) {
|
|
|
2373
3007
|
const method = detectInstallMethod();
|
|
2374
3008
|
const cmd = method === "homebrew" ? "brew upgrade thinkwork-ai/tap/thinkwork" : `npm install -g thinkwork-cli@${latest}`;
|
|
2375
3009
|
console.log(` Installing via ${method}...`);
|
|
2376
|
-
console.log(
|
|
3010
|
+
console.log(chalk12.dim(` $ ${cmd}`));
|
|
2377
3011
|
console.log("");
|
|
2378
3012
|
try {
|
|
2379
|
-
|
|
3013
|
+
execSync10(cmd, { stdio: "inherit", timeout: 12e4 });
|
|
2380
3014
|
console.log("");
|
|
2381
|
-
console.log(
|
|
3015
|
+
console.log(chalk12.green(` \u2713 Upgraded to thinkwork-cli@${latest}`));
|
|
2382
3016
|
} catch {
|
|
2383
3017
|
console.log("");
|
|
2384
|
-
console.log(
|
|
2385
|
-
console.log(` ${
|
|
3018
|
+
console.log(chalk12.red(` Failed to upgrade. Try manually:`));
|
|
3019
|
+
console.log(` ${chalk12.cyan(cmd)}`);
|
|
2386
3020
|
}
|
|
2387
3021
|
console.log("");
|
|
2388
3022
|
});
|
|
2389
3023
|
}
|
|
2390
3024
|
|
|
2391
3025
|
// src/commands/user.ts
|
|
2392
|
-
import { spawn as
|
|
2393
|
-
import { input, select as
|
|
3026
|
+
import { spawn as spawn4 } from "child_process";
|
|
3027
|
+
import { input, select as select3 } from "@inquirer/prompts";
|
|
2394
3028
|
function getTerraformOutput2(cwd, key) {
|
|
2395
3029
|
return new Promise((resolve3, reject) => {
|
|
2396
|
-
const proc =
|
|
3030
|
+
const proc = spawn4("terraform", ["output", "-raw", key], {
|
|
2397
3031
|
cwd,
|
|
2398
3032
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2399
3033
|
});
|
|
@@ -2425,7 +3059,7 @@ function runAwsCognitoReset(userPoolId, username, region) {
|
|
|
2425
3059
|
"json"
|
|
2426
3060
|
];
|
|
2427
3061
|
if (region) args.push("--region", region);
|
|
2428
|
-
const proc =
|
|
3062
|
+
const proc = spawn4("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2429
3063
|
let stdout = "";
|
|
2430
3064
|
let stderr = "";
|
|
2431
3065
|
proc.stdout.on("data", (d) => stdout += d);
|
|
@@ -2433,10 +3067,10 @@ function runAwsCognitoReset(userPoolId, username, region) {
|
|
|
2433
3067
|
proc.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
|
|
2434
3068
|
});
|
|
2435
3069
|
}
|
|
2436
|
-
function
|
|
3070
|
+
function isCancellation2(err) {
|
|
2437
3071
|
return err instanceof Error && err.name === "ExitPromptError";
|
|
2438
3072
|
}
|
|
2439
|
-
function
|
|
3073
|
+
function requireTty2(label) {
|
|
2440
3074
|
if (!process.stdin.isTTY) {
|
|
2441
3075
|
printError(
|
|
2442
3076
|
`${label} is required. Pass it as a flag or re-run in an interactive terminal.`
|
|
@@ -2445,14 +3079,14 @@ function requireTty(label) {
|
|
|
2445
3079
|
}
|
|
2446
3080
|
}
|
|
2447
3081
|
async function promptEmail() {
|
|
2448
|
-
|
|
3082
|
+
requireTty2("Email");
|
|
2449
3083
|
return await input({
|
|
2450
3084
|
message: "Email address of the person to invite:",
|
|
2451
3085
|
validate: (v) => v.trim().includes("@") ? true : "That doesn't look like an email."
|
|
2452
3086
|
});
|
|
2453
3087
|
}
|
|
2454
3088
|
async function promptStage(region) {
|
|
2455
|
-
|
|
3089
|
+
requireTty2("Stage");
|
|
2456
3090
|
const stages = listDeployedStages(region);
|
|
2457
3091
|
if (stages.length === 0) {
|
|
2458
3092
|
printError(
|
|
@@ -2464,14 +3098,14 @@ async function promptStage(region) {
|
|
|
2464
3098
|
console.log(` Using the only deployed stage: ${stages[0]}`);
|
|
2465
3099
|
return stages[0];
|
|
2466
3100
|
}
|
|
2467
|
-
return await
|
|
3101
|
+
return await select3({
|
|
2468
3102
|
message: "Which stage?",
|
|
2469
3103
|
choices: stages.map((s) => ({ name: s, value: s })),
|
|
2470
3104
|
loop: false
|
|
2471
3105
|
});
|
|
2472
3106
|
}
|
|
2473
3107
|
async function promptTenant(apiUrl, authSecret) {
|
|
2474
|
-
|
|
3108
|
+
requireTty2("Tenant");
|
|
2475
3109
|
const list = await apiFetch(apiUrl, authSecret, "/api/tenants");
|
|
2476
3110
|
if (!list || list.length === 0) {
|
|
2477
3111
|
printError(
|
|
@@ -2483,7 +3117,7 @@ async function promptTenant(apiUrl, authSecret) {
|
|
|
2483
3117
|
console.log(` Using the only tenant: ${list[0].name} (${list[0].slug})`);
|
|
2484
3118
|
return list[0].slug;
|
|
2485
3119
|
}
|
|
2486
|
-
return await
|
|
3120
|
+
return await select3({
|
|
2487
3121
|
message: "Which tenant?",
|
|
2488
3122
|
choices: list.map((t) => ({
|
|
2489
3123
|
name: `${t.name} (slug: ${t.slug})`,
|
|
@@ -2502,7 +3136,7 @@ async function promptOptionalName() {
|
|
|
2502
3136
|
}
|
|
2503
3137
|
async function promptRole() {
|
|
2504
3138
|
if (!process.stdin.isTTY) return "member";
|
|
2505
|
-
return await
|
|
3139
|
+
return await select3({
|
|
2506
3140
|
message: "Role:",
|
|
2507
3141
|
choices: [
|
|
2508
3142
|
{ name: "member \u2014 regular access", value: "member" },
|
|
@@ -2611,7 +3245,7 @@ Agents / scripts that pass all flags stay non-interactive.
|
|
|
2611
3245
|
`Invited ${email} to "${tenant}" (role: ${result.body.role}). Cognito has emailed a temporary password; the user sets a new password on first sign-in.`
|
|
2612
3246
|
);
|
|
2613
3247
|
} catch (err) {
|
|
2614
|
-
if (
|
|
3248
|
+
if (isCancellation2(err)) {
|
|
2615
3249
|
console.log("");
|
|
2616
3250
|
console.log(" Cancelled.");
|
|
2617
3251
|
return;
|
|
@@ -2704,6 +3338,917 @@ FORCE_CHANGE_PASSWORD or has been disabled.
|
|
|
2704
3338
|
);
|
|
2705
3339
|
}
|
|
2706
3340
|
|
|
3341
|
+
// src/commands/me.ts
|
|
3342
|
+
import { gql } from "@urql/core";
|
|
3343
|
+
|
|
3344
|
+
// src/lib/resolve-stage.ts
|
|
3345
|
+
import { select as select4 } from "@inquirer/prompts";
|
|
3346
|
+
async function resolveStage(opts = {}) {
|
|
3347
|
+
const region = opts.region ?? "us-east-1";
|
|
3348
|
+
const validate = opts.validate ?? true;
|
|
3349
|
+
const raw = opts.flag ?? process.env.THINKWORK_STAGE ?? loadCliConfig().defaultStage ?? await pickStage(region);
|
|
3350
|
+
if (!raw) {
|
|
3351
|
+
printError(
|
|
3352
|
+
"No stage specified. Pass `--stage <name>`, set THINKWORK_STAGE, or run `thinkwork login --stage <name>`."
|
|
3353
|
+
);
|
|
3354
|
+
process.exit(1);
|
|
3355
|
+
}
|
|
3356
|
+
if (validate) {
|
|
3357
|
+
const check = validateStage(raw);
|
|
3358
|
+
if (!check.valid) {
|
|
3359
|
+
printError(check.error);
|
|
3360
|
+
process.exit(1);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
return raw;
|
|
3364
|
+
}
|
|
3365
|
+
async function pickStage(region) {
|
|
3366
|
+
const stages = listDeployedStages(region);
|
|
3367
|
+
if (stages.length === 0) {
|
|
3368
|
+
printError(
|
|
3369
|
+
`No Thinkwork deployments found in ${region}. Run \`thinkwork list\` or pass --region.`
|
|
3370
|
+
);
|
|
3371
|
+
process.exit(1);
|
|
3372
|
+
}
|
|
3373
|
+
if (stages.length === 1) {
|
|
3374
|
+
console.log(` Using the only deployed stage: ${stages[0]}`);
|
|
3375
|
+
return stages[0];
|
|
3376
|
+
}
|
|
3377
|
+
requireTty("Stage");
|
|
3378
|
+
return await select4({
|
|
3379
|
+
message: "Which stage?",
|
|
3380
|
+
choices: stages.map((s) => ({ name: s, value: s })),
|
|
3381
|
+
loop: false
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// src/lib/gql-client.ts
|
|
3386
|
+
import {
|
|
3387
|
+
Client,
|
|
3388
|
+
cacheExchange,
|
|
3389
|
+
fetchExchange
|
|
3390
|
+
} from "@urql/core";
|
|
3391
|
+
|
|
3392
|
+
// src/lib/resolve-auth.ts
|
|
3393
|
+
var REFRESH_WINDOW_SECONDS = 5 * 60;
|
|
3394
|
+
async function resolveAuth(opts) {
|
|
3395
|
+
const region = opts.region ?? "us-east-1";
|
|
3396
|
+
const session = loadStageSession(opts.stage);
|
|
3397
|
+
if (session?.kind === "cognito") {
|
|
3398
|
+
const fresh = await ensureCognitoFresh(opts.stage, session, region);
|
|
3399
|
+
return cognitoAuth(fresh);
|
|
3400
|
+
}
|
|
3401
|
+
if (opts.requireCognito) {
|
|
3402
|
+
printError(
|
|
3403
|
+
`Stage "${opts.stage}" has no Cognito session. Run \`thinkwork login --stage ${opts.stage}\` (not --api-key \u2014 this command needs a user identity).`
|
|
3404
|
+
);
|
|
3405
|
+
process.exit(1);
|
|
3406
|
+
}
|
|
3407
|
+
if (session?.kind === "api-key") {
|
|
3408
|
+
return apiKeyAuth(session);
|
|
3409
|
+
}
|
|
3410
|
+
const api = resolveApiConfig(opts.stage, region);
|
|
3411
|
+
if (!api) process.exit(1);
|
|
3412
|
+
return {
|
|
3413
|
+
mode: "api-key",
|
|
3414
|
+
headers: {
|
|
3415
|
+
Authorization: `Bearer ${api.authSecret}`
|
|
3416
|
+
}
|
|
3417
|
+
};
|
|
3418
|
+
}
|
|
3419
|
+
function cognitoAuth(session) {
|
|
3420
|
+
return {
|
|
3421
|
+
mode: "cognito",
|
|
3422
|
+
headers: {
|
|
3423
|
+
// graphql-http Lambda reads the id_token from `Authorization` (no
|
|
3424
|
+
// `Bearer ` prefix per admin's fetchOptions). Keep the same shape.
|
|
3425
|
+
Authorization: session.idToken
|
|
3426
|
+
},
|
|
3427
|
+
principalId: session.principalId,
|
|
3428
|
+
tenantId: session.tenantId,
|
|
3429
|
+
tenantSlug: session.tenantSlug
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
function apiKeyAuth(session) {
|
|
3433
|
+
const headers = {
|
|
3434
|
+
Authorization: `Bearer ${session.authSecret}`
|
|
3435
|
+
};
|
|
3436
|
+
if (session.tenantId) headers["x-tenant-id"] = session.tenantId;
|
|
3437
|
+
return {
|
|
3438
|
+
mode: "api-key",
|
|
3439
|
+
headers,
|
|
3440
|
+
tenantId: session.tenantId,
|
|
3441
|
+
tenantSlug: session.tenantSlug
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
async function ensureCognitoFresh(stage, session, region) {
|
|
3445
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
3446
|
+
const safeUntil = session.expiresAt - REFRESH_WINDOW_SECONDS;
|
|
3447
|
+
if (nowSeconds < safeUntil) return session;
|
|
3448
|
+
const cognito = discoverCognitoConfig(stage, region) ?? {
|
|
3449
|
+
userPoolId: session.userPoolId,
|
|
3450
|
+
clientId: session.userPoolClientId,
|
|
3451
|
+
domain: session.cognitoDomain,
|
|
3452
|
+
domainUrl: `https://${session.cognitoDomain}.auth.${session.region}.amazoncognito.com`,
|
|
3453
|
+
region: session.region
|
|
3454
|
+
};
|
|
3455
|
+
try {
|
|
3456
|
+
const refreshed = await refreshCognitoTokens(cognito, session.refreshToken);
|
|
3457
|
+
const next = {
|
|
3458
|
+
...session,
|
|
3459
|
+
idToken: refreshed.idToken,
|
|
3460
|
+
accessToken: refreshed.accessToken,
|
|
3461
|
+
expiresAt: refreshed.expiresAt
|
|
3462
|
+
};
|
|
3463
|
+
saveStageSession(stage, next);
|
|
3464
|
+
return next;
|
|
3465
|
+
} catch (err) {
|
|
3466
|
+
printError(
|
|
3467
|
+
`Session refresh failed for stage "${stage}": ${err instanceof Error ? err.message : String(err)}. Run \`thinkwork login --stage ${stage}\` to sign in again.`
|
|
3468
|
+
);
|
|
3469
|
+
process.exit(1);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
// src/lib/gql-client.ts
|
|
3474
|
+
async function getGqlClient(opts) {
|
|
3475
|
+
const region = opts.region ?? "us-east-1";
|
|
3476
|
+
const baseUrl = getApiEndpoint(opts.stage, region);
|
|
3477
|
+
if (!baseUrl) {
|
|
3478
|
+
printError(
|
|
3479
|
+
`Cannot discover API endpoint for stage "${opts.stage}" in ${region}. Is the stack deployed?`
|
|
3480
|
+
);
|
|
3481
|
+
process.exit(1);
|
|
3482
|
+
}
|
|
3483
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/graphql`;
|
|
3484
|
+
const auth = await resolveAuth({ stage: opts.stage, region });
|
|
3485
|
+
const client = new Client({
|
|
3486
|
+
url,
|
|
3487
|
+
exchanges: [cacheExchange, fetchExchange],
|
|
3488
|
+
fetchOptions: () => ({
|
|
3489
|
+
method: "POST",
|
|
3490
|
+
headers: {
|
|
3491
|
+
"content-type": "application/json",
|
|
3492
|
+
...auth.headers
|
|
3493
|
+
}
|
|
3494
|
+
}),
|
|
3495
|
+
// CLI calls are short-lived and we want server truth on every run —
|
|
3496
|
+
// bypass the in-memory cache to avoid stale reads between quick commands.
|
|
3497
|
+
requestPolicy: "network-only"
|
|
3498
|
+
});
|
|
3499
|
+
return {
|
|
3500
|
+
client,
|
|
3501
|
+
url,
|
|
3502
|
+
tenantId: auth.tenantId,
|
|
3503
|
+
tenantSlug: auth.tenantSlug
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// src/commands/me.ts
|
|
3508
|
+
var ME_QUERY = gql`
|
|
3509
|
+
query CliMe {
|
|
3510
|
+
me {
|
|
3511
|
+
id
|
|
3512
|
+
email
|
|
3513
|
+
name
|
|
3514
|
+
tenantId
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
`;
|
|
3518
|
+
function registerMeCommand(program2) {
|
|
3519
|
+
program2.command("me").description(
|
|
3520
|
+
"Print the identity behind the current session for a stage. Use after `thinkwork login` to verify everything works, or as a scriptable introspection (`--json | jq`)."
|
|
3521
|
+
).option("-s, --stage <name>", "Stage to introspect (defaults to the saved default stage)").option("-r, --region <region>", "AWS region", "us-east-1").addHelpText(
|
|
3522
|
+
"after",
|
|
3523
|
+
`
|
|
3524
|
+
Examples:
|
|
3525
|
+
# Check who you're signed in as on the default stage
|
|
3526
|
+
$ thinkwork me
|
|
3527
|
+
|
|
3528
|
+
# Explicit stage
|
|
3529
|
+
$ thinkwork me --stage prod
|
|
3530
|
+
|
|
3531
|
+
# Machine-readable \u2014 pipe to jq
|
|
3532
|
+
$ thinkwork me --stage dev --json | jq .tenantSlug
|
|
3533
|
+
`
|
|
3534
|
+
).action(async (opts) => {
|
|
3535
|
+
const stage = await resolveStage({ flag: opts.stage, region: opts.region });
|
|
3536
|
+
const session = loadStageSession(stage);
|
|
3537
|
+
if (!session) {
|
|
3538
|
+
printError(
|
|
3539
|
+
`Not signed in to stage "${stage}". Run \`thinkwork login --stage ${stage}\`.`
|
|
3540
|
+
);
|
|
3541
|
+
process.exit(1);
|
|
3542
|
+
}
|
|
3543
|
+
if (!isJsonMode()) printHeader("me", stage);
|
|
3544
|
+
const { client, tenantSlug } = await getGqlClient({ stage, region: opts.region });
|
|
3545
|
+
let me = null;
|
|
3546
|
+
try {
|
|
3547
|
+
const res = await client.query(ME_QUERY, {}).toPromise();
|
|
3548
|
+
if (res.error) throw res.error;
|
|
3549
|
+
me = res.data?.me ?? null;
|
|
3550
|
+
} catch (err) {
|
|
3551
|
+
printError(
|
|
3552
|
+
`me query failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3553
|
+
);
|
|
3554
|
+
process.exit(1);
|
|
3555
|
+
}
|
|
3556
|
+
if (isJsonMode()) {
|
|
3557
|
+
printJson({
|
|
3558
|
+
stage,
|
|
3559
|
+
mode: session.kind,
|
|
3560
|
+
user: me,
|
|
3561
|
+
tenant: { id: me?.tenantId, slug: tenantSlug }
|
|
3562
|
+
});
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
printKeyValue([
|
|
3566
|
+
["Stage", stage],
|
|
3567
|
+
["Mode", session.kind],
|
|
3568
|
+
["User ID", me?.id],
|
|
3569
|
+
["Email", me?.email ?? (session.kind === "cognito" ? session.email : void 0)],
|
|
3570
|
+
["Name", me?.name ?? void 0],
|
|
3571
|
+
["Tenant ID", me?.tenantId ?? session.tenantId],
|
|
3572
|
+
["Tenant slug", tenantSlug ?? session.tenantSlug]
|
|
3573
|
+
]);
|
|
3574
|
+
});
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
// src/lib/stub.ts
|
|
3578
|
+
import chalk13 from "chalk";
|
|
3579
|
+
var ROADMAP_URL = "https://github.com/thinkwork-ai/thinkwork/blob/main/apps/cli/README.md#roadmap";
|
|
3580
|
+
function notYetImplemented(commandPath, phase) {
|
|
3581
|
+
const label = chalk13.yellow(`\u29D7 not yet implemented`);
|
|
3582
|
+
const line = chalk13.bold(`thinkwork ${commandPath}`);
|
|
3583
|
+
process.stderr.write(
|
|
3584
|
+
`
|
|
3585
|
+
${label}: ${line} ships in Phase ${phase}.
|
|
3586
|
+
${chalk13.dim(`See the roadmap: ${ROADMAP_URL}`)}
|
|
3587
|
+
|
|
3588
|
+
`
|
|
3589
|
+
);
|
|
3590
|
+
process.exit(2);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
// src/commands/thread.ts
|
|
3594
|
+
function registerThreadCommand(program2) {
|
|
3595
|
+
const thread = program2.command("thread").alias("threads").description(
|
|
3596
|
+
"Create, list, update, and comment on threads (tasks, chats, bugs, questions) in a tenant."
|
|
3597
|
+
);
|
|
3598
|
+
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("--status <status>", "Filter: BACKLOG | TODO | IN_PROGRESS | IN_REVIEW | BLOCKED | DONE | CANCELLED").option("--priority <p>", "Filter: LOW | MEDIUM | HIGH | URGENT").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 title/body").option("--limit <n>", "Max rows (default 50)", "50").option("--archived", "Include archived threads").addHelpText(
|
|
3599
|
+
"after",
|
|
3600
|
+
`
|
|
3601
|
+
Examples:
|
|
3602
|
+
# Open work on the default stage/tenant
|
|
3603
|
+
$ thinkwork thread list --status IN_PROGRESS
|
|
3604
|
+
|
|
3605
|
+
# Pipe to jq
|
|
3606
|
+
$ thinkwork thread list --json | jq '.[] | select(.priority=="URGENT")'
|
|
3607
|
+
|
|
3608
|
+
# Everything assigned to me
|
|
3609
|
+
$ thinkwork thread list --assignee me
|
|
3610
|
+
`
|
|
3611
|
+
).action(() => notYetImplemented("thread list", 1));
|
|
3612
|
+
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(
|
|
3613
|
+
"after",
|
|
3614
|
+
`
|
|
3615
|
+
Examples:
|
|
3616
|
+
$ thinkwork thread get thr-abc123
|
|
3617
|
+
$ thinkwork thread get 42 # by issue number
|
|
3618
|
+
$ thinkwork thread get 42 --json | jq .assignee
|
|
3619
|
+
`
|
|
3620
|
+
).action(() => notYetImplemented("thread get", 1));
|
|
3621
|
+
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("--type <type>", "TASK | CHAT | BUG | QUESTION", "TASK").option("--priority <p>", "LOW | MEDIUM | HIGH | URGENT", "MEDIUM").option("--assignee <id>", "Assign on create (user or agent ID)").option("--body <text>", "Description body (markdown)").option("--due <iso>", "Due date as ISO-8601").option("--label <name...>", "Attach label(s) by name (repeatable)").addHelpText(
|
|
3622
|
+
"after",
|
|
3623
|
+
`
|
|
3624
|
+
Examples:
|
|
3625
|
+
# Fully interactive \u2014 walkthrough prompts for title, type, priority, assignee.
|
|
3626
|
+
$ thinkwork thread create
|
|
3627
|
+
|
|
3628
|
+
# Scripted
|
|
3629
|
+
$ thinkwork thread create "Investigate latency spike" \\
|
|
3630
|
+
--priority HIGH --assignee agt-obs-1 --label ops --label oncall
|
|
3631
|
+
|
|
3632
|
+
# Mix: pass the title, prompt for the rest.
|
|
3633
|
+
$ thinkwork thread create "Investigate latency spike"
|
|
3634
|
+
`
|
|
3635
|
+
).action(() => notYetImplemented("thread create", 1));
|
|
3636
|
+
thread.command("update <id>").description("Update a thread's title, status, priority, assignee, labels, or due date.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--title <t>", "Rename").option("--status <s>", "Move to a new status").option("--priority <p>", "LOW | MEDIUM | HIGH | URGENT").option("--assignee <id>", "Reassign (user or agent ID)").option("--due <iso>", "Due date").option("--body <text>", "Replace description body").addHelpText(
|
|
3637
|
+
"after",
|
|
3638
|
+
`
|
|
3639
|
+
Examples:
|
|
3640
|
+
$ thinkwork thread update thr-abc --status IN_REVIEW
|
|
3641
|
+
$ thinkwork thread update thr-abc --assignee agt-ops --priority URGENT
|
|
3642
|
+
`
|
|
3643
|
+
).action(() => notYetImplemented("thread update", 1));
|
|
3644
|
+
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(
|
|
3645
|
+
"after",
|
|
3646
|
+
`
|
|
3647
|
+
Examples:
|
|
3648
|
+
$ thinkwork thread close thr-abc
|
|
3649
|
+
$ thinkwork thread close thr-abc --comment "fixed in #124"
|
|
3650
|
+
`
|
|
3651
|
+
).action(() => notYetImplemented("thread close", 1));
|
|
3652
|
+
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));
|
|
3653
|
+
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(
|
|
3654
|
+
"after",
|
|
3655
|
+
`
|
|
3656
|
+
Examples:
|
|
3657
|
+
$ thinkwork thread checkout thr-abc --agent agt-fixer
|
|
3658
|
+
`
|
|
3659
|
+
).action(() => notYetImplemented("thread checkout", 1));
|
|
3660
|
+
thread.command("release <id>").description("Release a checked-out thread, optionally moving it to a new status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--status <s>", "Status to release into").action(() => notYetImplemented("thread release", 1));
|
|
3661
|
+
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(
|
|
3662
|
+
"after",
|
|
3663
|
+
`
|
|
3664
|
+
Examples:
|
|
3665
|
+
$ thinkwork thread comment thr-abc "Looks good, shipping"
|
|
3666
|
+
$ thinkwork thread comment thr-abc --file /tmp/review.md
|
|
3667
|
+
$ thinkwork thread comment thr-abc # prompts interactively
|
|
3668
|
+
`
|
|
3669
|
+
).action(() => notYetImplemented("thread comment", 1));
|
|
3670
|
+
thread.command("label <assign|remove> <threadId> <labelId>").description("Attach or detach a label on a thread. Labels are managed via `thinkwork label`.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
3671
|
+
"after",
|
|
3672
|
+
`
|
|
3673
|
+
Examples:
|
|
3674
|
+
$ thinkwork thread label assign thr-abc lbl-ops
|
|
3675
|
+
$ thinkwork thread label remove thr-abc lbl-ops
|
|
3676
|
+
`
|
|
3677
|
+
).action(() => notYetImplemented("thread label", 1));
|
|
3678
|
+
thread.command("escalate <id>").description("Escalate a thread to another agent (carries context, records actor).").requiredOption("--to-agent <id>", "Agent to escalate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--reason <text>", "Reason note (appears in activity log)").action(() => notYetImplemented("thread escalate", 1));
|
|
3679
|
+
thread.command("delegate <id>").description("Delegate ownership to another agent without the 'escalation' semantics.").requiredOption("--to-agent <id>", "Agent to delegate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread delegate", 1));
|
|
3680
|
+
thread.command("delete <id>").description("Permanently delete a thread (not just close). Requires confirmation.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip the confirmation prompt").addHelpText(
|
|
3681
|
+
"after",
|
|
3682
|
+
`
|
|
3683
|
+
Examples:
|
|
3684
|
+
# Prompts 'Are you sure?'
|
|
3685
|
+
$ thinkwork thread delete thr-abc
|
|
3686
|
+
|
|
3687
|
+
# Scripted / destructive-no-prompt
|
|
3688
|
+
$ thinkwork thread delete thr-abc --yes
|
|
3689
|
+
`
|
|
3690
|
+
).action(() => notYetImplemented("thread delete", 1));
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// src/commands/message.ts
|
|
3694
|
+
function registerMessageCommand(program2) {
|
|
3695
|
+
const msg = program2.command("message").alias("messages").alias("msg").description("Send and list messages inside a thread.");
|
|
3696
|
+
msg.command("send <threadId> [content]").description("Send a message 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 message content from a file").option("--as-agent <id>", "Send as a specific agent (api-key auth only)").addHelpText(
|
|
3697
|
+
"after",
|
|
3698
|
+
`
|
|
3699
|
+
Examples:
|
|
3700
|
+
$ thinkwork message send thr-abc "Investigating now"
|
|
3701
|
+
$ thinkwork message send thr-abc --file notes.md
|
|
3702
|
+
$ thinkwork message send thr-abc # interactive
|
|
3703
|
+
`
|
|
3704
|
+
).action(() => notYetImplemented("message send", 1));
|
|
3705
|
+
msg.command("list <threadId>").alias("ls").description("List messages in a thread (paginated).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max messages to return", "50").option("--cursor <c>", "Pagination cursor from a previous page").addHelpText(
|
|
3706
|
+
"after",
|
|
3707
|
+
`
|
|
3708
|
+
Examples:
|
|
3709
|
+
$ thinkwork message list thr-abc
|
|
3710
|
+
$ thinkwork message list thr-abc --limit 10 --json | jq '.[].author'
|
|
3711
|
+
`
|
|
3712
|
+
).action(() => notYetImplemented("message list", 1));
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
// src/commands/label.ts
|
|
3716
|
+
function registerLabelCommand(program2) {
|
|
3717
|
+
const label = program2.command("label").alias("labels").description("Manage tenant-wide thread labels (tags).");
|
|
3718
|
+
label.command("list").alias("ls").description("List all labels in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("label list", 1));
|
|
3719
|
+
label.command("create [name]").description("Create a new label. Prompts for missing fields in TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--color <hex>", "Label color as a hex string (e.g. #10b981)").option("--description <text>", "What the label is for").addHelpText(
|
|
3720
|
+
"after",
|
|
3721
|
+
`
|
|
3722
|
+
Examples:
|
|
3723
|
+
$ thinkwork label create ops --color "#10b981"
|
|
3724
|
+
$ thinkwork label create # interactive
|
|
3725
|
+
`
|
|
3726
|
+
).action(() => notYetImplemented("label create", 1));
|
|
3727
|
+
label.command("update <id>").description("Rename or recolor an existing label.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--color <hex>").option("--description <text>").action(() => notYetImplemented("label update", 1));
|
|
3728
|
+
label.command("delete <id>").description("Delete a label. Any thread assignments are removed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip the confirmation prompt").action(() => notYetImplemented("label delete", 1));
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
// src/commands/inbox.ts
|
|
3732
|
+
function registerInboxCommand(program2) {
|
|
3733
|
+
const inbox = program2.command("inbox").description("View and act on approval requests routed to you or your workspace.");
|
|
3734
|
+
inbox.command("list").alias("ls").description("List inbox items, optionally filtered by status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option(
|
|
3735
|
+
"--status <s>",
|
|
3736
|
+
"PENDING | APPROVED | REJECTED | REVISION_REQUESTED | EXPIRED | CANCELLED (default: PENDING)",
|
|
3737
|
+
"PENDING"
|
|
3738
|
+
).option("--entity-type <type>", "Filter by entity type (thread, agent, artifact, \u2026)").option("--entity-id <id>", "Filter by entity ID").option("--mine", "Only items routed to the caller").addHelpText(
|
|
3739
|
+
"after",
|
|
3740
|
+
`
|
|
3741
|
+
Examples:
|
|
3742
|
+
# What's waiting for me to approve?
|
|
3743
|
+
$ thinkwork inbox list --mine
|
|
3744
|
+
|
|
3745
|
+
# All pending approvals in the tenant
|
|
3746
|
+
$ thinkwork inbox list
|
|
3747
|
+
|
|
3748
|
+
# Closed items for audit
|
|
3749
|
+
$ thinkwork inbox list --status APPROVED --json
|
|
3750
|
+
`
|
|
3751
|
+
).action(() => notYetImplemented("inbox list", 1));
|
|
3752
|
+
inbox.command("get <id>").description("Fetch one inbox item with its comments, links, and history.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox get", 1));
|
|
3753
|
+
inbox.command("approve <id>").description("Approve an inbox item. Downstream agents resume.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--notes <text>", "Approval notes (stored on the decision)").addHelpText(
|
|
3754
|
+
"after",
|
|
3755
|
+
`
|
|
3756
|
+
Examples:
|
|
3757
|
+
$ thinkwork inbox approve ibx-abc
|
|
3758
|
+
$ thinkwork inbox approve ibx-abc --notes "Budget confirmed."
|
|
3759
|
+
`
|
|
3760
|
+
).action(() => notYetImplemented("inbox approve", 1));
|
|
3761
|
+
inbox.command("reject <id>").description("Reject an inbox item. Downstream agents stop.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--notes <text>", "Rejection reason").action(() => notYetImplemented("inbox reject", 1));
|
|
3762
|
+
inbox.command("request-revision <id>").description("Ask for changes \u2014 the agent gets the item back with your notes.").requiredOption("--notes <text>", "What needs to change").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox request-revision", 1));
|
|
3763
|
+
inbox.command("resubmit <id>").description("Resubmit a revised inbox item for approval.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--notes <text>", "What changed").action(() => notYetImplemented("inbox resubmit", 1));
|
|
3764
|
+
inbox.command("cancel <id>").description("Cancel a pending approval request (originator or admin).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox cancel", 1));
|
|
3765
|
+
inbox.command("comment <id> [content]").description("Add a comment to an inbox item without deciding on it yet.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--file <path>", "Read comment body from a file").action(() => notYetImplemented("inbox comment", 1));
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
// src/commands/agent.ts
|
|
3769
|
+
function registerAgentCommand(program2) {
|
|
3770
|
+
const agent = program2.command("agent").alias("agents").description("Manage agents \u2014 create, configure, inspect, budget, and key-rotate.");
|
|
3771
|
+
agent.command("list").alias("ls").description("List agents in a tenant. Cognito users see paired agents; admins see all.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--status <s>", "IDLE | BUSY | OFFLINE | ERROR").option("--type <t>", "Filter by agent type (HUMAN_PAIR, TEAM_AGENT, SUB_AGENT, \u2026)").option("--include-system", "Include internal system agents").option("--all", "Admin-only: list every agent in the tenant (not just paired ones)").addHelpText(
|
|
3772
|
+
"after",
|
|
3773
|
+
`
|
|
3774
|
+
Examples:
|
|
3775
|
+
# Agents you're paired with
|
|
3776
|
+
$ thinkwork agent list
|
|
3777
|
+
|
|
3778
|
+
# Tenant-wide (admin only)
|
|
3779
|
+
$ thinkwork agent list --all
|
|
3780
|
+
|
|
3781
|
+
# Offline agents only, as JSON
|
|
3782
|
+
$ thinkwork agent list --status OFFLINE --json
|
|
3783
|
+
`
|
|
3784
|
+
).action(() => notYetImplemented("agent list", 2));
|
|
3785
|
+
agent.command("get <id>").description("Fetch one agent with its skills, capabilities, budget, and recent activity.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent get", 2));
|
|
3786
|
+
agent.command("create [name]").description("Create a new agent. Prompts walkthrough for missing fields in TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--template <id>", "Clone from an existing template (strongly recommended)").option("--role <role>", "Role description shown to users").option("--type <type>", "TEAM_AGENT | SUB_AGENT | HUMAN_PAIR").option("--parent <agentId>", "Parent agent (for SUB_AGENT)").option("--reports-to <agentId>", "Reporting manager (for org-chart display)").option("--system-prompt <text>", "Raw system-prompt override (use with care)").option("--system-prompt-file <path>", "Load the system prompt from a file").option("--model <id>", "Model ID override (see `thinkwork config models`)").addHelpText(
|
|
3787
|
+
"after",
|
|
3788
|
+
`
|
|
3789
|
+
Examples:
|
|
3790
|
+
# Fully interactive
|
|
3791
|
+
$ thinkwork agent create
|
|
3792
|
+
|
|
3793
|
+
# From a template (recommended)
|
|
3794
|
+
$ thinkwork agent create "Ops Analyst" --template tpl-ops-analyst
|
|
3795
|
+
|
|
3796
|
+
# Scripted, raw system prompt
|
|
3797
|
+
$ thinkwork agent create "Bot" --role "on-call summarizer" \\
|
|
3798
|
+
--type TEAM_AGENT --model claude-sonnet-4-6 \\
|
|
3799
|
+
--system-prompt-file prompts/bot.md
|
|
3800
|
+
`
|
|
3801
|
+
).action(() => notYetImplemented("agent create", 2));
|
|
3802
|
+
agent.command("update <id>").description("Update any mutable agent field.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--role <r>").option("--type <t>").option("--parent <agentId>").option("--reports-to <agentId>").option("--system-prompt <text>").option("--system-prompt-file <path>").option("--model <id>").action(() => notYetImplemented("agent update", 2));
|
|
3803
|
+
agent.command("delete <id>").description("Archive (soft-delete) an agent. Existing threads stay; no new work routed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("agent delete", 2));
|
|
3804
|
+
agent.command("status <id> <status>").description("Manually set agent status. Useful to pause/resume.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
3805
|
+
"after",
|
|
3806
|
+
`
|
|
3807
|
+
Examples:
|
|
3808
|
+
$ thinkwork agent status agt-ops IDLE
|
|
3809
|
+
$ thinkwork agent status agt-ops OFFLINE
|
|
3810
|
+
`
|
|
3811
|
+
).action(() => notYetImplemented("agent status", 2));
|
|
3812
|
+
agent.command("unpause <id>").description("Resume an agent paused by a budget policy trigger.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent unpause", 2));
|
|
3813
|
+
const capabilities = agent.command("capabilities").alias("cap").description("Toggle built-in capabilities (email inbox, web search, etc.).");
|
|
3814
|
+
capabilities.command("set <agentId>").description("Enable/disable capabilities on an agent.").requiredOption("--capability <name>", "Capability name (email, web-search, file-upload, \u2026)").option("--enabled", "Enable (default if flag present)").option("--disabled", "Disable").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent capabilities set", 2));
|
|
3815
|
+
const skills = agent.command("skills").description("Attach or configure MCP-backed skills on an agent.");
|
|
3816
|
+
skills.command("set <agentId>").description("Enable/disable/configure a skill for an agent.").requiredOption("--skill <id>", "Skill ID (see `thinkwork skill list`)").option("--enabled", "Enable").option("--disabled", "Disable").option("--config <json>", "Inline JSON config for the skill").option("--rate-limit <rps>", "Rate limit in requests/sec").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent skills set", 2));
|
|
3817
|
+
const budget = agent.command("budget").description("Per-agent spend caps \u2014 pause or alert when exceeded.");
|
|
3818
|
+
budget.command("set <agentId>").description("Set or update an agent's budget policy.").requiredOption("--limit-usd <amount>", "USD ceiling for the window").option("--window <w>", "daily | weekly | monthly", "monthly").option("--action <a>", "PAUSE | ALERT", "PAUSE").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent budget set", 2));
|
|
3819
|
+
budget.command("clear <agentId>").description("Remove an agent's budget policy (falls back to tenant-wide).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent budget clear", 2));
|
|
3820
|
+
const apiKey = agent.command("api-key").description("Agent API keys \u2014 service-to-service credentials tied to one agent.");
|
|
3821
|
+
apiKey.command("list <agentId>").description("List API keys for an agent (metadata only; plaintext shown on create).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent api-key list", 2));
|
|
3822
|
+
apiKey.command("create <agentId>").description("Generate a new API key. The plaintext is printed once \u2014 save it.").requiredOption("--name <n>", "Human label for the key (e.g. 'GitHub Actions')").option("--expires <iso>", "Expiration (ISO-8601). Omit for no expiry.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
3823
|
+
"after",
|
|
3824
|
+
`
|
|
3825
|
+
Examples:
|
|
3826
|
+
$ thinkwork agent api-key create agt-ops --name "GitHub Actions"
|
|
3827
|
+
$ thinkwork agent api-key create agt-ops --name "nightly" --expires 2026-12-31T00:00:00Z --json
|
|
3828
|
+
`
|
|
3829
|
+
).action(() => notYetImplemented("agent api-key create", 2));
|
|
3830
|
+
apiKey.command("revoke <keyId>").description("Revoke an API key. Subsequent calls return 401.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("agent api-key revoke", 2));
|
|
3831
|
+
const email = agent.command("email").description("Inbound email addresses \u2014 let an agent receive email + optionally reply.");
|
|
3832
|
+
email.command("enable <agentId>").description("Enable inbound email for an agent.").option("--local-part <x>", "Custom localpart (e.g. ops@)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent email enable", 2));
|
|
3833
|
+
email.command("disable <agentId>").description("Disable inbound email for an agent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent email disable", 2));
|
|
3834
|
+
email.command("allowlist <agentId> <senders...>").description("Replace the allowlist of sender email addresses for an agent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
3835
|
+
"after",
|
|
3836
|
+
`
|
|
3837
|
+
Examples:
|
|
3838
|
+
$ thinkwork agent email allowlist agt-ops oncall@example.com pagerduty@example.com
|
|
3839
|
+
`
|
|
3840
|
+
).action(() => notYetImplemented("agent email allowlist", 2));
|
|
3841
|
+
const version = agent.command("version").description("Agent configuration version history.");
|
|
3842
|
+
version.command("list <agentId>").description("List version snapshots of an agent's config (prompt, model, skills, \u2026).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max versions", "20").action(() => notYetImplemented("agent version list", 2));
|
|
3843
|
+
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));
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
// src/commands/template.ts
|
|
3847
|
+
function registerTemplateCommand(program2) {
|
|
3848
|
+
const tpl = program2.command("template").alias("templates").description("Manage agent templates \u2014 reusable configs you spawn agents from.");
|
|
3849
|
+
tpl.command("list").alias("ls").description("List templates in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template list", 2));
|
|
3850
|
+
tpl.command("get <id>").description("Fetch one template with its linked agents.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template get", 2));
|
|
3851
|
+
tpl.command("create [name]").description("Create a new template from a set of config defaults.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from-agent <id>", "Clone config from an existing agent").option("--system-prompt-file <path>", "Prompt markdown path").option("--model <id>", "Default model").option("--description <text>", "What this template is for").addHelpText(
|
|
3852
|
+
"after",
|
|
3853
|
+
`
|
|
3854
|
+
Examples:
|
|
3855
|
+
# Capture an existing agent's config as a template
|
|
3856
|
+
$ thinkwork template create "Ops Analyst" --from-agent agt-ops-1
|
|
3857
|
+
|
|
3858
|
+
# Fresh template
|
|
3859
|
+
$ thinkwork template create --system-prompt-file prompts/ops.md --model claude-sonnet-4-6
|
|
3860
|
+
`
|
|
3861
|
+
).action(() => notYetImplemented("template create", 2));
|
|
3862
|
+
tpl.command("update <id>").description("Update a template. Linked agents are NOT auto-synced \u2014 use `template sync-*`.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--system-prompt-file <path>").option("--model <id>").option("--description <text>").action(() => notYetImplemented("template update", 2));
|
|
3863
|
+
tpl.command("delete <id>").description("Delete a template. Linked agents are unaffected; they just stop being in sync.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("template delete", 2));
|
|
3864
|
+
tpl.command("diff <templateId> <agentId>").description("Show what would change if we synced <agentId> to <templateId>.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template diff", 2));
|
|
3865
|
+
tpl.command("sync-agent <templateId> <agentId>").description("Apply template changes to one linked agent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("template sync-agent", 2));
|
|
3866
|
+
tpl.command("sync-all <templateId>").description("Apply template changes to every linked agent. Requires confirmation.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").addHelpText(
|
|
3867
|
+
"after",
|
|
3868
|
+
`
|
|
3869
|
+
Examples:
|
|
3870
|
+
# Preview first
|
|
3871
|
+
$ thinkwork template diff tpl-ops agt-ops-1
|
|
3872
|
+
|
|
3873
|
+
# Apply to one agent
|
|
3874
|
+
$ thinkwork template sync-agent tpl-ops agt-ops-1
|
|
3875
|
+
|
|
3876
|
+
# Apply to every linked agent
|
|
3877
|
+
$ thinkwork template sync-all tpl-ops
|
|
3878
|
+
`
|
|
3879
|
+
).action(() => notYetImplemented("template sync-all", 2));
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
// src/commands/tenant.ts
|
|
3883
|
+
function registerTenantCommand(program2) {
|
|
3884
|
+
const tenant = program2.command("tenant").alias("tenants").description("Manage tenants (workspaces) \u2014 create, rename, and configure plans / defaults.");
|
|
3885
|
+
tenant.command("list").alias("ls").description("List tenants the caller can see.").option("-s, --stage <name>", "Deployment stage").action(() => notYetImplemented("tenant list", 2));
|
|
3886
|
+
tenant.command("get <idOrSlug>").description("Fetch one tenant by ID or slug.").option("-s, --stage <name>", "Deployment stage").action(() => notYetImplemented("tenant get", 2));
|
|
3887
|
+
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(
|
|
3888
|
+
"after",
|
|
3889
|
+
`
|
|
3890
|
+
Examples:
|
|
3891
|
+
$ thinkwork tenant create "Acme Corp" --slug acme --plan team
|
|
3892
|
+
`
|
|
3893
|
+
).action(() => notYetImplemented("tenant create", 2));
|
|
3894
|
+
tenant.command("update <id>").description("Update tenant name, plan, or issue-prefix.").option("-s, --stage <name>", "Deployment stage").option("--name <n>").option("--plan <plan>").option("--issue-prefix <prefix>").action(() => notYetImplemented("tenant update", 2));
|
|
3895
|
+
const settings = tenant.command("settings").description("Tenant-wide defaults \u2014 model, budget, auto-close, feature flags.");
|
|
3896
|
+
settings.command("get [tenant]").description("Print the current TenantSettings (human) or the full object (--json).").option("-s, --stage <name>", "Deployment stage").action(() => notYetImplemented("tenant settings get", 2));
|
|
3897
|
+
settings.command("set [tenant]").description("Set one or more TenantSettings fields. Each --<field> flag is independent.").option("-s, --stage <name>", "Deployment stage").option("--default-model <id>").option("--monthly-budget-usd <n>").option("--max-agents <n>").option("--auto-close-after-days <n>").option("--feature <key=value...>", "Toggle a feature flag (repeatable)").addHelpText(
|
|
3898
|
+
"after",
|
|
3899
|
+
`
|
|
3900
|
+
Examples:
|
|
3901
|
+
$ thinkwork tenant settings set --default-model claude-sonnet-4-6
|
|
3902
|
+
$ thinkwork tenant settings set --monthly-budget-usd 5000 --feature hindsight=true
|
|
3903
|
+
`
|
|
3904
|
+
).action(() => notYetImplemented("tenant settings set", 2));
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
// src/commands/member.ts
|
|
3908
|
+
function registerMemberCommand(program2) {
|
|
3909
|
+
const mem = program2.command("member").alias("members").description("List and manage tenant members (users + agents with access).");
|
|
3910
|
+
mem.command("list").alias("ls").description("List every member of the current tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--principal-type <t>", "Filter: USER | AGENT").option("--role <r>", "Filter by role (member, admin, owner)").action(() => notYetImplemented("member list", 2));
|
|
3911
|
+
mem.command("invite [email]").description("Invite a user by email. GraphQL path (sends invite email, creates Cognito user).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--role <role>", "member | admin | owner", "member").option("--name <n>", "Optional display name").addHelpText(
|
|
3912
|
+
"after",
|
|
3913
|
+
`
|
|
3914
|
+
Examples:
|
|
3915
|
+
$ thinkwork member invite alice@example.com --role admin
|
|
3916
|
+
$ thinkwork member invite # interactive
|
|
3917
|
+
`
|
|
3918
|
+
).action(() => notYetImplemented("member invite", 2));
|
|
3919
|
+
mem.command("update <memberId>").description("Change a member's role or status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--role <r>").option("--status <s>", "active | suspended").action(() => notYetImplemented("member update", 2));
|
|
3920
|
+
mem.command("remove <memberId>").description("Remove a member from the tenant. The underlying Cognito user is NOT deleted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("member remove", 2));
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
// src/commands/team.ts
|
|
3924
|
+
function registerTeamCommand(program2) {
|
|
3925
|
+
const team = program2.command("team").alias("teams").description("Manage teams within a tenant.");
|
|
3926
|
+
team.command("list").alias("ls").description("List teams in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team list", 2));
|
|
3927
|
+
team.command("get <id>").description("Fetch one team with its members and agents.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team get", 2));
|
|
3928
|
+
team.command("create [name]").description("Create a new team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--description <text>").option("--budget-usd <n>", "Optional sub-budget").addHelpText(
|
|
3929
|
+
"after",
|
|
3930
|
+
`
|
|
3931
|
+
Examples:
|
|
3932
|
+
$ thinkwork team create "Ops" --description "24/7 on-call" --budget-usd 2000
|
|
3933
|
+
`
|
|
3934
|
+
).action(() => notYetImplemented("team create", 2));
|
|
3935
|
+
team.command("update <id>").description("Update team name, description, status, or budget.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--status <s>", "active | archived").option("--budget-usd <n>").action(() => notYetImplemented("team update", 2));
|
|
3936
|
+
team.command("delete <id>").description("Delete (archive) a team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("team delete", 2));
|
|
3937
|
+
team.command("add-agent <teamId> <agentId>").description("Add an agent to a team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team add-agent", 2));
|
|
3938
|
+
team.command("remove-agent <teamId> <agentId>").description("Remove an agent from a team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team remove-agent", 2));
|
|
3939
|
+
team.command("add-user <teamId> <userId>").description("Add a user to a team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team add-user", 2));
|
|
3940
|
+
team.command("remove-user <teamId> <userId>").description("Remove a user from a team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team remove-user", 2));
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
// src/commands/kb.ts
|
|
3944
|
+
function registerKbCommand(program2) {
|
|
3945
|
+
const kb = program2.command("kb").alias("knowledge-base").description("Manage knowledge bases (RAG stores) and attach them to agents.");
|
|
3946
|
+
kb.command("list").alias("ls").description("List knowledge bases in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("kb list", 2));
|
|
3947
|
+
kb.command("get <id>").description("Fetch one knowledge base with its S3 source + sync status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("kb get", 2));
|
|
3948
|
+
kb.command("create [name]").description("Create a new knowledge base. Interactive prompts for missing fields.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--s3-uri <uri>", "S3 source location (s3://bucket/prefix)").option("--description <text>").option("--embedding-model <id>", "Bedrock embedding model ID").addHelpText(
|
|
3949
|
+
"after",
|
|
3950
|
+
`
|
|
3951
|
+
Examples:
|
|
3952
|
+
$ thinkwork kb create "Runbooks" --s3-uri s3://ops-docs/runbooks
|
|
3953
|
+
$ thinkwork kb create # interactive
|
|
3954
|
+
`
|
|
3955
|
+
).action(() => notYetImplemented("kb create", 2));
|
|
3956
|
+
kb.command("update <id>").description("Update knowledge base metadata (name, description). Source changes need re-create.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").action(() => notYetImplemented("kb update", 2));
|
|
3957
|
+
kb.command("delete <id>").description("Delete a knowledge base. Embeddings + index are destroyed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("kb delete", 2));
|
|
3958
|
+
kb.command("sync <id>").description("Re-embed from S3. Idempotent; safe to re-run.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--wait", "Block until the sync finishes").action(() => notYetImplemented("kb sync", 2));
|
|
3959
|
+
kb.command("attach <kbId>").description("Attach a knowledge base to an agent.").requiredOption("--agent <id>", "Agent ID").option("--config <json>", "Retrieval config (topK, score threshold, \u2026)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
3960
|
+
"after",
|
|
3961
|
+
`
|
|
3962
|
+
Examples:
|
|
3963
|
+
$ thinkwork kb attach kb-runbooks --agent agt-oncall
|
|
3964
|
+
$ thinkwork kb attach kb-runbooks --agent agt-oncall --config '{"topK":5}'
|
|
3965
|
+
`
|
|
3966
|
+
).action(() => notYetImplemented("kb attach", 2));
|
|
3967
|
+
kb.command("detach <kbId>").description("Detach a knowledge base from an agent.").requiredOption("--agent <id>", "Agent ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("kb detach", 2));
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
// src/commands/routine.ts
|
|
3971
|
+
function registerRoutineCommand(program2) {
|
|
3972
|
+
const routine = program2.command("routine").alias("routines").description("Manage routines \u2014 saved workflows, their triggers, and past runs.");
|
|
3973
|
+
routine.command("list").alias("ls").description("List routines in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Filter by agent").option("--team <id>", "Filter by team").option("--status <s>", "ACTIVE | PAUSED | ARCHIVED").action(() => notYetImplemented("routine list", 3));
|
|
3974
|
+
routine.command("get <id>").description("Fetch one routine with its triggers and recent runs.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("routine get", 3));
|
|
3975
|
+
routine.command("create [name]").description("Create a new routine. Walkthrough for missing fields in TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent that runs the routine").option("--team <id>", "Team to route runs to (instead of a single agent)").option("--description <text>").option("--config <json>", "Inline routine config JSON").option("--config-file <path>", "Load routine config from a JSON file").addHelpText(
|
|
3976
|
+
"after",
|
|
3977
|
+
`
|
|
3978
|
+
Examples:
|
|
3979
|
+
$ thinkwork routine create "Nightly digest" --agent agt-editor --config-file routines/digest.json
|
|
3980
|
+
$ thinkwork routine create # interactive walkthrough
|
|
3981
|
+
`
|
|
3982
|
+
).action(() => notYetImplemented("routine create", 3));
|
|
3983
|
+
routine.command("update <id>").description("Update a routine.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--status <s>", "ACTIVE | PAUSED | ARCHIVED").option("--agent <id>").option("--team <id>").option("--config-file <path>").action(() => notYetImplemented("routine update", 3));
|
|
3984
|
+
routine.command("delete <id>").description("Delete a routine. Past runs and triggers are removed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("routine delete", 3));
|
|
3985
|
+
routine.command("trigger <id>").description("Trigger a routine run now (ad-hoc, outside its scheduled cadence).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--wait", "Block until the run finishes, then print result").option("--input <json>", "Optional input payload").action(() => notYetImplemented("routine trigger", 3));
|
|
3986
|
+
const run2 = routine.command("run").description("Inspect routine run history.");
|
|
3987
|
+
run2.command("list <routineId>").alias("ls").description("List recent runs of a routine.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max rows", "25").option("--cursor <c>", "Pagination cursor").action(() => notYetImplemented("routine run list", 3));
|
|
3988
|
+
run2.command("get <runId>").description("Fetch one run with its step outputs.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("routine run get", 3));
|
|
3989
|
+
const trigger = routine.command("trigger-config").description("Manage a routine's triggers (cron, webhook, event).");
|
|
3990
|
+
trigger.command("set <routineId>").description("Set or replace a trigger for a routine.").requiredOption("--type <t>", "CRON | WEBHOOK | EVENT").option("--schedule <cron>", "Cron expression (for CRON triggers)").option("--event <name>", "Event name (for EVENT triggers)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
3991
|
+
"after",
|
|
3992
|
+
`
|
|
3993
|
+
Examples:
|
|
3994
|
+
$ thinkwork routine trigger-config set rtn-digest --type CRON --schedule "0 9 * * *"
|
|
3995
|
+
`
|
|
3996
|
+
).action(() => notYetImplemented("routine trigger-config set", 3));
|
|
3997
|
+
trigger.command("delete <triggerId>").description("Remove a trigger.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("routine trigger-config delete", 3));
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
// src/commands/scheduled-job.ts
|
|
4001
|
+
function registerScheduledJobCommand(program2) {
|
|
4002
|
+
const job = program2.command("scheduled-job").alias("cron").description("Manage AWS-Scheduler-backed recurring agent jobs (wakeups on a cadence).");
|
|
4003
|
+
job.command("list").alias("ls").description("List scheduled jobs for the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Filter by agent").option("--routine <id>", "Filter by routine").option("--enabled <bool>", "true | false").action(() => notYetImplemented("scheduled-job list", 3));
|
|
4004
|
+
job.command("get <id>").description("Fetch one scheduled job.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("scheduled-job get", 3));
|
|
4005
|
+
job.command("create [name]").description("Create a new scheduled job. Supports cron() or rate() schedules.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent to wake up").option("--routine <id>", "Or: routine to trigger").option("--schedule <expr>", "EventBridge schedule (cron(\u2026) or rate(\u2026))").option("--timezone <tz>", "IANA timezone (default: UTC)", "UTC").option("--payload <json>", "Payload to pass to the agent/routine").option("--disabled", "Create in disabled state (enable later with update)").addHelpText(
|
|
4006
|
+
"after",
|
|
4007
|
+
`
|
|
4008
|
+
Examples:
|
|
4009
|
+
$ thinkwork scheduled-job create "Daily ops digest" \\
|
|
4010
|
+
--agent agt-editor --schedule "cron(0 9 * * ? *)" --timezone America/New_York
|
|
4011
|
+
|
|
4012
|
+
# rate() \u2014 note rate means "every N time from creation", NOT wall-clock.
|
|
4013
|
+
$ thinkwork scheduled-job create "Hourly check" --agent agt-check --schedule "rate(1 hour)"
|
|
4014
|
+
`
|
|
4015
|
+
).action(() => notYetImplemented("scheduled-job create", 3));
|
|
4016
|
+
job.command("update <id>").description("Update a scheduled job's schedule, payload, or enabled state.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--schedule <expr>").option("--timezone <tz>").option("--payload <json>").option("--enable").option("--disable").action(() => notYetImplemented("scheduled-job update", 3));
|
|
4017
|
+
job.command("delete <id>").description("Delete a scheduled job. The underlying EventBridge rule is removed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("scheduled-job delete", 3));
|
|
4018
|
+
job.command("run <id>").description("Trigger a scheduled job immediately (ad-hoc; ignores the schedule).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--wait", "Block until the run completes").action(() => notYetImplemented("scheduled-job run", 3));
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
// src/commands/turn.ts
|
|
4022
|
+
function registerTurnCommand(program2) {
|
|
4023
|
+
const turn = program2.command("turn").alias("turns").description("Inspect and cancel agent invocations (thread turns).");
|
|
4024
|
+
turn.command("list").alias("ls").description("List recent thread turns across the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Filter by agent").option("--routine <id>", "Filter by routine").option("--trigger <id>", "Filter by trigger ID").option("--thread <id>", "Filter by thread").option("--status <s>", "QUEUED | RUNNING | SUCCEEDED | FAILED | CANCELLED").option("--limit <n>", "Max rows", "50").addHelpText(
|
|
4025
|
+
"after",
|
|
4026
|
+
`
|
|
4027
|
+
Examples:
|
|
4028
|
+
# What's running right now?
|
|
4029
|
+
$ thinkwork turn list --status RUNNING
|
|
4030
|
+
|
|
4031
|
+
# Recent failures for one agent
|
|
4032
|
+
$ thinkwork turn list --agent agt-ops --status FAILED --limit 20
|
|
4033
|
+
`
|
|
4034
|
+
).action(() => notYetImplemented("turn list", 3));
|
|
4035
|
+
turn.command("get <id>").description("Fetch one thread turn with its event stream.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("turn get", 3));
|
|
4036
|
+
turn.command("cancel <id>").description("Cancel an in-progress thread turn. No-op if already finished.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("turn cancel", 3));
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
// src/commands/wakeup.ts
|
|
4040
|
+
function registerWakeupCommand(program2) {
|
|
4041
|
+
const wake = program2.command("wakeup").alias("wakeups").description("View and create agent wakeup requests (deferred/enqueued invocations).");
|
|
4042
|
+
wake.command("list").alias("ls").description("List queued wakeups in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("wakeup list", 3));
|
|
4043
|
+
wake.command("create").description("Queue a wakeup for an agent (ad-hoc or deferred).").requiredOption("--agent <id>", "Target agent").option("--thread <id>", "Thread to operate on (optional)").option("--delay-seconds <n>", "Wait N seconds before firing", "0").option("--payload <json>", "Optional input payload").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
4044
|
+
"after",
|
|
4045
|
+
`
|
|
4046
|
+
Examples:
|
|
4047
|
+
$ thinkwork wakeup create --agent agt-ops --thread thr-abc
|
|
4048
|
+
$ thinkwork wakeup create --agent agt-ops --delay-seconds 900 # fire in 15 min
|
|
4049
|
+
`
|
|
4050
|
+
).action(() => notYetImplemented("wakeup create", 3));
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
// src/commands/webhook.ts
|
|
4054
|
+
function registerWebhookCommand(program2) {
|
|
4055
|
+
const wh = program2.command("webhook").alias("webhooks").description("Manage inbound webhooks that dispatch to agents or routines.");
|
|
4056
|
+
wh.command("list").alias("ls").description("List webhooks in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--enabled <bool>", "true | false").option("--target-type <t>", "AGENT | ROUTINE").action(() => notYetImplemented("webhook list", 3));
|
|
4057
|
+
wh.command("get <id>").description("Fetch one webhook including its secret prefix + rate limit.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("webhook get", 3));
|
|
4058
|
+
wh.command("create [name]").description("Create a new webhook. The full secret is printed once.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--target-type <t>", "AGENT | ROUTINE").option("--target-id <id>", "ID of the agent or routine").option("--rate-limit <rpm>", "Max requests per minute").option("--allowed-ips <csv>", "Restrict to a CIDR list").option("--disabled", "Create in disabled state").addHelpText(
|
|
4059
|
+
"after",
|
|
4060
|
+
`
|
|
4061
|
+
Examples:
|
|
4062
|
+
$ thinkwork webhook create "GitHub PR opened" \\
|
|
4063
|
+
--target-type AGENT --target-id agt-reviewer --rate-limit 30
|
|
4064
|
+
|
|
4065
|
+
# CI use \u2014 capture the secret on create
|
|
4066
|
+
$ thinkwork webhook create "CI" --target-type ROUTINE --target-id rtn-ci --json | jq -r .secret
|
|
4067
|
+
`
|
|
4068
|
+
).action(() => notYetImplemented("webhook create", 3));
|
|
4069
|
+
wh.command("update <id>").description("Update a webhook's target, rate limit, or enabled state.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--target-type <t>").option("--target-id <id>").option("--rate-limit <rpm>").option("--allowed-ips <csv>").option("--enable").option("--disable").action(() => notYetImplemented("webhook update", 3));
|
|
4070
|
+
wh.command("delete <id>").description("Delete a webhook (its URL stops working immediately).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("webhook delete", 3));
|
|
4071
|
+
wh.command("test <id>").description("Send a synthetic payload to the webhook and print the resulting run ID.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--payload <json>").action(() => notYetImplemented("webhook test", 3));
|
|
4072
|
+
wh.command("rotate <id>").description("Generate a new secret for an existing webhook. Old secret is invalidated.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("webhook rotate", 3));
|
|
4073
|
+
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));
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
// src/commands/connector.ts
|
|
4077
|
+
function registerConnectorCommand(program2) {
|
|
4078
|
+
const conn = program2.command("connector").alias("connectors").description("Manage third-party integrations (Slack, GitHub, Linear, \u2026).");
|
|
4079
|
+
conn.command("list").alias("ls").description("List available connectors and which are enabled for this tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--enabled-only", "Only show tenant-enabled connectors").action(() => notYetImplemented("connector list", 3));
|
|
4080
|
+
conn.command("get <slug>").description("Fetch one connector with its config schema + current status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("connector get", 3));
|
|
4081
|
+
conn.command("enable <slug>").description("Enable a connector. Opens the OAuth flow in your browser when required.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--config <json>", "Inline config JSON (for API-key style connectors)").option("--config-file <path>", "Load config from a file").addHelpText(
|
|
4082
|
+
"after",
|
|
4083
|
+
`
|
|
4084
|
+
Examples:
|
|
4085
|
+
# OAuth connector (Slack)
|
|
4086
|
+
$ thinkwork connector enable slack
|
|
4087
|
+
|
|
4088
|
+
# API-key connector (inline)
|
|
4089
|
+
$ thinkwork connector enable linear --config '{"apiKey":"lin_\u2026"}'
|
|
4090
|
+
`
|
|
4091
|
+
).action(() => notYetImplemented("connector enable", 3));
|
|
4092
|
+
conn.command("disable <slug>").description("Disable a connector. Stored credentials are cleared.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("connector disable", 3));
|
|
4093
|
+
conn.command("test <slug>").description("Round-trip the connector's credentials against its API. Prints pass/fail + latency.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("connector test", 3));
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
// src/commands/skill.ts
|
|
4097
|
+
function registerSkillCommand(program2) {
|
|
4098
|
+
const skill = program2.command("skill").alias("skills").description("Browse the catalog, install, upgrade, or publish custom skills.");
|
|
4099
|
+
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));
|
|
4100
|
+
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));
|
|
4101
|
+
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(
|
|
4102
|
+
"after",
|
|
4103
|
+
`
|
|
4104
|
+
Examples:
|
|
4105
|
+
$ thinkwork skill install web-search
|
|
4106
|
+
$ thinkwork skill install pagerduty --version 1.4.2
|
|
4107
|
+
`
|
|
4108
|
+
).action(() => notYetImplemented("skill install", 3));
|
|
4109
|
+
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));
|
|
4110
|
+
skill.command("create [slug]").description("Publish a custom tenant-scoped skill (walkthrough for missing fields in TTY).").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(
|
|
4111
|
+
"after",
|
|
4112
|
+
`
|
|
4113
|
+
Examples:
|
|
4114
|
+
$ thinkwork skill create my-skill --manifest-file ./skills/my-skill.json
|
|
4115
|
+
$ thinkwork skill create # interactive
|
|
4116
|
+
`
|
|
4117
|
+
).action(() => notYetImplemented("skill create", 3));
|
|
4118
|
+
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));
|
|
4119
|
+
skill.command("delete <slug>").description("Delete a custom skill. Public catalog skills are uninstalled via this too.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("skill delete", 3));
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
// src/commands/memory.ts
|
|
4123
|
+
function registerMemoryCommand(program2) {
|
|
4124
|
+
const memory = program2.command("memory").description("Inspect, search, and edit an agent's memory records and graph.");
|
|
4125
|
+
memory.command("list").alias("ls").description("List memory records for an agent in a namespace.").requiredOption("--agent <id>", "Agent (assistant) ID").option(
|
|
4126
|
+
"--namespace <ns>",
|
|
4127
|
+
"Memory namespace (semantic | preferences | episodes | reflections)",
|
|
4128
|
+
"semantic"
|
|
4129
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory list", 4));
|
|
4130
|
+
memory.command("search").description("Search an agent's memory by query string.").requiredOption("--agent <id>", "Agent (assistant) ID").requiredOption("--query <q>", "Search query").option("--strategy <s>", "Retrieval strategy (semantic | keyword | hybrid)", "semantic").option("--limit <n>", "Max results", "10").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
4131
|
+
"after",
|
|
4132
|
+
`
|
|
4133
|
+
Examples:
|
|
4134
|
+
$ thinkwork memory search --agent agt-ops --query "escalation procedure"
|
|
4135
|
+
$ thinkwork memory search --agent agt-ops --query "p0 runbook" --strategy hybrid --json
|
|
4136
|
+
`
|
|
4137
|
+
).action(() => notYetImplemented("memory search", 4));
|
|
4138
|
+
memory.command("get <recordId>").description("Fetch one memory record in full.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory get", 4));
|
|
4139
|
+
memory.command("update <recordId>").description("Replace a memory record's content.").requiredOption("--content <text>", "New content").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory update", 4));
|
|
4140
|
+
memory.command("delete <recordId>").description("Remove a memory record.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("memory delete", 4));
|
|
4141
|
+
memory.command("graph").description("Print the agent's memory graph (summary in human mode; full JSON with --json).").requiredOption("--agent <id>", "Agent (assistant) ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory graph", 4));
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
// src/commands/recipe.ts
|
|
4145
|
+
function registerRecipeCommand(program2) {
|
|
4146
|
+
const recipe = program2.command("recipe").alias("recipes").description("Manage saved MCP tool invocations (parameterized one-click actions).");
|
|
4147
|
+
recipe.command("list").alias("ls").description("List recipes in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--thread <id>", "Filter by thread scope").option("--agent <id>", "Filter by agent scope").option("--limit <n>", "Max rows", "25").action(() => notYetImplemented("recipe list", 4));
|
|
4148
|
+
recipe.command("get <id>").description("Fetch one recipe.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("recipe get", 4));
|
|
4149
|
+
recipe.command("create [name]").description("Save a new recipe. Walkthrough for missing fields in TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--tool <slug>", "MCP tool slug").option("--params <json>", "Default parameters").option("--scope <s>", "tenant | agent | thread", "tenant").option("--agent <id>", "Required if --scope=agent").option("--thread <id>", "Required if --scope=thread").addHelpText(
|
|
4150
|
+
"after",
|
|
4151
|
+
`
|
|
4152
|
+
Examples:
|
|
4153
|
+
$ thinkwork recipe create "Create PagerDuty incident" \\
|
|
4154
|
+
--tool pagerduty.create_incident --params '{"urgency":"high"}'
|
|
4155
|
+
`
|
|
4156
|
+
).action(() => notYetImplemented("recipe create", 4));
|
|
4157
|
+
recipe.command("update <id>").description("Update a recipe's name, tool, or default params.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--tool <slug>").option("--params <json>").action(() => notYetImplemented("recipe update", 4));
|
|
4158
|
+
recipe.command("delete <id>").description("Delete a recipe.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("recipe delete", 4));
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
// src/commands/artifact.ts
|
|
4162
|
+
function registerArtifactCommand(program2) {
|
|
4163
|
+
const art = program2.command("artifact").alias("artifacts").description("List and fetch agent-produced artifacts (notes, reports, data-views, plans, drafts).");
|
|
4164
|
+
art.command("list").alias("ls").description("List artifacts in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--thread <id>", "Filter by thread").option("--agent <id>", "Filter by producing agent").option(
|
|
4165
|
+
"--type <t>",
|
|
4166
|
+
"DATA_VIEW | NOTE | REPORT | PLAN | DRAFT | DIGEST"
|
|
4167
|
+
).option("--status <s>", "DRAFT | FINAL | SUPERSEDED").option("--limit <n>", "Max rows", "25").option("--cursor <c>", "Pagination cursor").addHelpText(
|
|
4168
|
+
"after",
|
|
4169
|
+
`
|
|
4170
|
+
Examples:
|
|
4171
|
+
$ thinkwork artifact list --agent agt-editor --type REPORT
|
|
4172
|
+
$ thinkwork artifact list --thread thr-abc --json
|
|
4173
|
+
`
|
|
4174
|
+
).action(() => notYetImplemented("artifact list", 4));
|
|
4175
|
+
art.command("get <id>").description("Fetch one artifact. Human mode prints a preview; --json returns the full content.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--raw", "Print only the markdown body to stdout (for piping to pandoc / bat / less)").action(() => notYetImplemented("artifact get", 4));
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
// src/commands/cost.ts
|
|
4179
|
+
function registerCostCommand(program2) {
|
|
4180
|
+
const cost = program2.command("cost").description("Spend reports \u2014 totals, per-agent, per-model, and daily series.");
|
|
4181
|
+
cost.command("summary").description("Total spend for the tenant over an optional date range.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from <iso>", "Start date (ISO-8601)").option("--to <iso>", "End date (ISO-8601)").addHelpText(
|
|
4182
|
+
"after",
|
|
4183
|
+
`
|
|
4184
|
+
Examples:
|
|
4185
|
+
# MTD spend
|
|
4186
|
+
$ thinkwork cost summary
|
|
4187
|
+
|
|
4188
|
+
# Specific window
|
|
4189
|
+
$ thinkwork cost summary --from 2026-04-01 --to 2026-04-30 --json
|
|
4190
|
+
`
|
|
4191
|
+
).action(() => notYetImplemented("cost summary", 5));
|
|
4192
|
+
cost.command("by-agent").description("Spend broken down by agent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from <iso>").option("--to <iso>").option("--sort <field>", "cost | requests (default cost)", "cost").action(() => notYetImplemented("cost by-agent", 5));
|
|
4193
|
+
cost.command("by-model").description("Spend broken down by model ID (tokens + cost).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from <iso>").option("--to <iso>").action(() => notYetImplemented("cost by-model", 5));
|
|
4194
|
+
cost.command("series").description("Daily cost series.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--days <n>", "Days of history", "30").action(() => notYetImplemented("cost series", 5));
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
// src/commands/budget.ts
|
|
4198
|
+
function registerBudgetCommand(program2) {
|
|
4199
|
+
const budget = program2.command("budget").alias("budgets").description("Manage budget policies (tenant-wide or per-agent) and inspect current status.");
|
|
4200
|
+
budget.command("list").alias("ls").description("List budget policies in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("budget list", 5));
|
|
4201
|
+
budget.command("status").description("Show each budget's current spend vs. limit.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("budget status", 5));
|
|
4202
|
+
budget.command("upsert").description("Create or update a budget policy.").requiredOption("--limit-usd <amount>", "USD ceiling for the window").option("--scope <s>", "tenant | agent", "tenant").option("--agent <id>", "Required if --scope=agent").option("--window <w>", "daily | weekly | monthly", "monthly").option("--action <a>", "PAUSE | ALERT", "PAUSE").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
4203
|
+
"after",
|
|
4204
|
+
`
|
|
4205
|
+
Examples:
|
|
4206
|
+
# Tenant-wide $5k/month pause
|
|
4207
|
+
$ thinkwork budget upsert --limit-usd 5000 --window monthly --action PAUSE
|
|
4208
|
+
|
|
4209
|
+
# Per-agent alert-only
|
|
4210
|
+
$ thinkwork budget upsert --scope agent --agent agt-ops --limit-usd 500 --action ALERT
|
|
4211
|
+
`
|
|
4212
|
+
).action(() => notYetImplemented("budget upsert", 5));
|
|
4213
|
+
budget.command("delete <id>").description("Remove a budget policy.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("budget delete", 5));
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
// src/commands/performance.ts
|
|
4217
|
+
function registerPerformanceCommand(program2) {
|
|
4218
|
+
const perf = program2.command("performance").alias("perf").description("Observability: per-agent invocations, errors, p95 latency, and cost.");
|
|
4219
|
+
perf.command("agents").description("Performance summary for every agent in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from <iso>").option("--to <iso>").option("--sort <f>", "cost | errors | latency | requests", "errors").action(() => notYetImplemented("performance agents", 5));
|
|
4220
|
+
perf.command("agent <id>").description("Performance detail for one agent (including daily time-series).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--days <n>", "Time-series history", "14").action(() => notYetImplemented("performance agent", 5));
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
// src/commands/trace.ts
|
|
4224
|
+
function registerTraceCommand(program2) {
|
|
4225
|
+
const trace = program2.command("trace").description("Inspect LLM invocations (traces) for a thread or turn.");
|
|
4226
|
+
trace.command("thread <threadId>").description("All LLM invocations across every turn of one thread.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--since <iso>").action(() => notYetImplemented("trace thread", 5));
|
|
4227
|
+
trace.command("turn <turnId>").description("LLM invocations for a single thread-turn (verbose \u2014 prompt + response + metadata).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--raw", "Print raw prompts + responses as a JSON array (useful for piping)").addHelpText(
|
|
4228
|
+
"after",
|
|
4229
|
+
`
|
|
4230
|
+
Examples:
|
|
4231
|
+
$ thinkwork trace turn ttn-abc --json | jq '.[].model'
|
|
4232
|
+
$ thinkwork trace turn ttn-abc --raw | jq '.[].response'
|
|
4233
|
+
`
|
|
4234
|
+
).action(() => notYetImplemented("trace turn", 5));
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
// src/commands/dashboard.ts
|
|
4238
|
+
function registerDashboardCommand(program2) {
|
|
4239
|
+
program2.command("dashboard").alias("overview").description("One-screen snapshot of the tenant \u2014 agents, open threads, approvals, spend.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
4240
|
+
"after",
|
|
4241
|
+
`
|
|
4242
|
+
Examples:
|
|
4243
|
+
# Print the dashboard for the default tenant
|
|
4244
|
+
$ thinkwork dashboard
|
|
4245
|
+
|
|
4246
|
+
# Check a specific stage
|
|
4247
|
+
$ thinkwork dashboard --stage prod
|
|
4248
|
+
`
|
|
4249
|
+
).action(() => notYetImplemented("dashboard", 5));
|
|
4250
|
+
}
|
|
4251
|
+
|
|
2707
4252
|
// src/cli.ts
|
|
2708
4253
|
var program = new Command();
|
|
2709
4254
|
program.name("thinkwork").description(
|
|
@@ -2711,20 +4256,27 @@ program.name("thinkwork").description(
|
|
|
2711
4256
|
).version(VERSION, "-v, --version", "Print the CLI version").option(
|
|
2712
4257
|
"-p, --profile <name>",
|
|
2713
4258
|
"AWS profile to use (sets AWS_PROFILE for Terraform and AWS CLI)"
|
|
4259
|
+
).option(
|
|
4260
|
+
"--json",
|
|
4261
|
+
"Emit machine-readable JSON on stdout. Warnings/spinners stay on stderr."
|
|
2714
4262
|
);
|
|
2715
4263
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
2716
4264
|
const explicit = actionCommand.opts().profile ?? program.opts().profile;
|
|
2717
4265
|
if (explicit) {
|
|
2718
4266
|
process.env.AWS_PROFILE = explicit;
|
|
2719
|
-
|
|
4267
|
+
} else if (!process.env.AWS_PROFILE) {
|
|
4268
|
+
const fallback = loadCliConfig().defaultProfile;
|
|
4269
|
+
if (fallback) process.env.AWS_PROFILE = fallback;
|
|
2720
4270
|
}
|
|
2721
|
-
|
|
2722
|
-
const
|
|
2723
|
-
|
|
4271
|
+
const jsonGlobal = Boolean(program.opts().json);
|
|
4272
|
+
const jsonLocal = Boolean(actionCommand.opts().json);
|
|
4273
|
+
setJsonMode(jsonGlobal || jsonLocal);
|
|
2724
4274
|
});
|
|
2725
4275
|
registerLoginCommand(program);
|
|
4276
|
+
registerLogoutCommand(program);
|
|
2726
4277
|
registerInitCommand(program);
|
|
2727
4278
|
registerDoctorCommand(program);
|
|
4279
|
+
registerMeCommand(program);
|
|
2728
4280
|
registerPlanCommand(program);
|
|
2729
4281
|
registerDeployCommand(program);
|
|
2730
4282
|
registerBootstrapCommand(program);
|
|
@@ -2736,4 +4288,34 @@ registerMcpCommand(program);
|
|
|
2736
4288
|
registerToolsCommand(program);
|
|
2737
4289
|
registerUpdateCommand(program);
|
|
2738
4290
|
registerUserCommand(program);
|
|
4291
|
+
registerThreadCommand(program);
|
|
4292
|
+
registerMessageCommand(program);
|
|
4293
|
+
registerLabelCommand(program);
|
|
4294
|
+
registerInboxCommand(program);
|
|
4295
|
+
registerAgentCommand(program);
|
|
4296
|
+
registerTemplateCommand(program);
|
|
4297
|
+
registerTenantCommand(program);
|
|
4298
|
+
registerMemberCommand(program);
|
|
4299
|
+
registerTeamCommand(program);
|
|
4300
|
+
registerKbCommand(program);
|
|
4301
|
+
registerRoutineCommand(program);
|
|
4302
|
+
registerScheduledJobCommand(program);
|
|
4303
|
+
registerTurnCommand(program);
|
|
4304
|
+
registerWakeupCommand(program);
|
|
4305
|
+
registerWebhookCommand(program);
|
|
4306
|
+
registerConnectorCommand(program);
|
|
4307
|
+
registerSkillCommand(program);
|
|
4308
|
+
registerMemoryCommand(program);
|
|
4309
|
+
registerRecipeCommand(program);
|
|
4310
|
+
registerArtifactCommand(program);
|
|
4311
|
+
registerCostCommand(program);
|
|
4312
|
+
registerBudgetCommand(program);
|
|
4313
|
+
registerPerformanceCommand(program);
|
|
4314
|
+
registerTraceCommand(program);
|
|
4315
|
+
registerDashboardCommand(program);
|
|
4316
|
+
for (const cmd of program.commands) {
|
|
4317
|
+
if (!cmd.options.some((o) => o.long === "--json")) {
|
|
4318
|
+
cmd.option("--json", "Emit machine-readable JSON on stdout.");
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
2739
4321
|
program.parse();
|