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 chalk from "chalk";
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(chalk.bold.cyan(" \u2B21 Thinkwork") + chalk.dim(` \u2014 ${command}`));
182
- console.log(chalk.dim(` Stage: ${chalk.white(stage)}`));
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(chalk.dim(` AWS: ${chalk.white(identity.account)} / ${chalk.white(identity.region)}`));
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 = chalk.dim(`[${index + 1}/${total}]`);
191
- console.log(` ${progress} ${chalk.bold(label)}`);
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
- ${chalk.green("\u2713")} ${chalk.bold(message)}`);
234
+ ${chalk2.green("\u2713")} ${chalk2.bold(message)}`);
196
235
  }
197
236
  function printError(message) {
198
237
  console.log(`
199
- ${chalk.red("\u2717")} ${chalk.bold.red(message)}`);
238
+ ${chalk2.red("\u2717")} ${chalk2.bold.red(message)}`);
200
239
  }
201
240
  function printWarning(message) {
202
- console.log(` ${chalk.yellow("\u26A0")} ${message}`);
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(chalk.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"));
208
- console.log(` ${chalk.bold("Command:")} ${command}`);
209
- console.log(` ${chalk.bold("Stage:")} ${stage}`);
210
- console.log(` ${chalk.bold("Tiers:")} ${tiers.map((t) => TIER_LABELS[t] ?? t).join(" \u2192 ")}`);
211
- console.log(` ${chalk.bold("Time:")} ${elapsed}s`);
212
- console.log(chalk.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"));
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 chalk2 from "chalk";
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 ? chalk2.green("\u2713") : chalk2.red("\u2717");
465
- const detail = result.pass ? chalk2.dim(result.detail) : chalk2.yellow(result.detail);
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
- ${chalk2.green.bold("All checks passed.")}`);
510
+ ${chalk3.green.bold("All checks passed.")}`);
472
511
  } else {
473
512
  console.log(`
474
- ${chalk2.yellow.bold("Some checks failed.")} Fix the issues above before deploying.`);
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 chalk3 from "chalk";
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(chalk3.bold.cyan(` \u2B21 ${env.stage}`));
608
- console.log(chalk3.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"));
609
- console.log(` ${chalk3.bold("Region:")} ${env.region}`);
610
- console.log(` ${chalk3.bold("Account:")} ${env.accountId}`);
611
- console.log(` ${chalk3.bold("Database:")} ${env.databaseEngine}`);
612
- console.log(` ${chalk3.bold("Memory:")} managed (always on)${env.enableHindsight ? " + hindsight" : ""}`);
613
- console.log(` ${chalk3.bold("Terraform dir:")} ${env.terraformDir}`);
614
- console.log(` ${chalk3.bold("Created:")} ${env.createdAt}`);
615
- console.log(` ${chalk3.bold("Updated:")} ${env.updatedAt}`);
616
- console.log(chalk3.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"));
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(chalk3.dim(" terraform.tfvars:"));
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(` ${chalk3.dim(masked)}`);
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 ${chalk3.cyan("thinkwork init -s <stage>")} to create one.`);
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(chalk3.bold(" Environments"));
651
- console.log(chalk3.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"));
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 ? chalk3.magenta("managed+hindsight") : chalk3.dim("managed");
654
- const dbBadge = env.databaseEngine === "rds-postgres" ? chalk3.yellow("rds") : chalk3.dim("aurora");
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
- ` ${chalk3.bold.cyan(env.stage.padEnd(16))}${env.region.padEnd(14)}${env.accountId.padEnd(16)}${dbBadge.padEnd(20)}${memBadge}`
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(chalk3.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"));
660
- console.log(chalk3.dim(` ${envs.length} environment(s)`));
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: ${chalk3.cyan("thinkwork config list -s <stage>")}`);
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: execSync10 } = await import("child_process");
780
- const secretJson = execSync10(
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 execSync4 } from "child_process";
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 chalk5 from "chalk";
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 chalk4 from "chalk";
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(` ${chalk4.yellow("\u2192")} AWS CLI not found. Installing...`);
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(` ${chalk4.green("\u2713")} AWS CLI installed via Homebrew`);
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(` ${chalk4.green("\u2713")} AWS CLI installed to ~/.local/bin/aws`);
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(` ${chalk4.green("\u2713")} AWS CLI installed`);
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(` ${chalk4.red("\u2717")} Could not auto-install AWS CLI.`);
951
- console.log(` Install manually: ${chalk4.cyan("https://aws.amazon.com/cli/")}`);
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(` ${chalk4.yellow("\u2192")} Terraform not found. Installing...`);
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(` ${chalk4.green("\u2713")} Terraform installed via Homebrew`);
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(` ${chalk4.green("\u2713")} Terraform ${tfVersion} installed to ~/.local/bin/terraform`);
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(` ${chalk4.red("\u2717")} Could not auto-install Terraform.`);
989
- console.log(` Install manually: ${chalk4.cyan("https://developer.hashicorp.com/terraform/install")}`);
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(chalk4.dim(" Checking prerequisites...\n"));
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(` ${chalk4.red("Missing prerequisites.")} Install them and try again.`);
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 "&amp;";
1353
+ case "<":
1354
+ return "&lt;";
1355
+ case ">":
1356
+ return "&gt;";
1357
+ case '"':
1358
+ return "&quot;";
1359
+ case "'":
1360
+ return "&#39;";
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 = execSync4(
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} ${chalk5.dim(`(${describeType(p.type)})`)}`,
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
- execSync4(
1515
+ execSync6(
1095
1516
  `aws configure set aws_access_key_id "${accessKeyId}" --profile ${targetProfile}`,
1096
1517
  { stdio: "pipe" }
1097
1518
  );
1098
- execSync4(
1519
+ execSync6(
1099
1520
  `aws configure set aws_secret_access_key "${secretAccessKey}" --profile ${targetProfile}`,
1100
1521
  { stdio: "pipe" }
1101
1522
  );
1102
- execSync4(
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
- execSync4(`aws sso login --profile ${targetProfile}`, { stdio: "inherit" });
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 finalize(profile, mode) {
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
- chalk5.dim(
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
- "Configure AWS credentials for Thinkwork. Picks from existing ~/.aws profiles by default; falls back to new keys or SSO."
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").addHelpText(
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
- # Interactive picker \u2014 lists profiles from ~/.aws, verifies the one you pick,
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
- # Skip the picker, enter fresh access keys into a named profile
1166
- $ thinkwork login --keys --profile thinkwork
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
- # Skip the picker, log in via AWS SSO
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
- After login, commands resolve the AWS profile in this order:
1172
- 1. --profile <name> (per-command override)
1173
- 2. $AWS_PROFILE env var
1174
- 3. defaultProfile from ~/.thinkwork/config.json (set by this command)
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(async (opts) => {
1177
- printHeader("login", opts.profile);
1178
- const awsOk = await ensureAwsCli();
1179
- if (!awsOk) process.exit(1);
1180
- if (opts.sso) {
1181
- if (!runSsoLogin(opts.profile)) process.exit(1);
1182
- process.env.AWS_PROFILE = opts.profile;
1183
- finalize(opts.profile, "SSO");
1184
- return;
1185
- }
1186
- if (opts.keys) {
1187
- if (!await runKeyEntry(opts.profile)) process.exit(1);
1188
- process.env.AWS_PROFILE = opts.profile;
1189
- finalize(opts.profile, "access keys");
1190
- return;
1191
- }
1192
- const profiles = listAwsProfiles();
1193
- if (profiles.length === 0) {
1194
- console.log("");
1195
- console.log(chalk5.dim(" No AWS profiles found in ~/.aws/."));
1196
- console.log(
1197
- chalk5.dim(" Falling through to access-key entry for a new profile.")
1198
- );
1199
- if (!await runKeyEntry(opts.profile)) process.exit(1);
1200
- process.env.AWS_PROFILE = opts.profile;
1201
- finalize(opts.profile, "access keys");
1202
- return;
1203
- }
1204
- const choice = await pickProfile(profiles);
1205
- if (choice.kind === "cancel") {
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(chalk5.dim(" Cancelled. No changes made."));
1208
- return;
1209
- }
1210
- if (choice.kind === "keys") {
1211
- if (!await runKeyEntry(opts.profile)) process.exit(1);
1212
- process.env.AWS_PROFILE = opts.profile;
1213
- finalize(opts.profile, "access keys");
1214
- return;
1215
- }
1216
- if (choice.kind === "sso") {
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
- const picked = choice.name;
1223
- console.log("");
1224
- console.log(` Verifying "${picked}"...`);
1225
- const identity = verifyProfile(picked);
1226
- if (!identity) {
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
- `Could not authenticate with profile "${picked}". If it's an SSO profile, try \`aws sso login --profile ${picked}\` first.`
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 execSync5 } from "child_process";
1913
+ import { execSync as execSync7 } from "child_process";
1241
1914
  import { fileURLToPath } from "url";
1242
1915
  import { createInterface as createInterface3 } from "readline";
1243
- import chalk6 from "chalk";
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 ? chalk6.dim(` [${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 ? chalk6.bold(o) : chalk6.dim(o)).join(" / ");
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(chalk6.bold(" Configure your Thinkwork environment\n"));
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(chalk6.dim(" \u2500\u2500 Database \u2500\u2500"));
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(chalk6.dim(" \u2500\u2500 Memory \u2500\u2500"));
1375
- console.log(chalk6.dim(" AgentCore managed memory is always on (automatic retention)."));
1376
- console.log(chalk6.dim(" Hindsight is an optional add-on: ECS Fargate service for"));
1377
- console.log(chalk6.dim(" semantic + entity-graph retrieval (~$75/mo)."));
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(chalk6.dim(" \u2500\u2500 Auth \u2500\u2500"));
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(chalk6.dim(" \u2500\u2500 Frontend URLs \u2500\u2500"));
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(chalk6.dim(" \u2500\u2500 Secrets (auto-generated) \u2500\u2500"));
1396
- console.log(chalk6.dim(` DB password: ${config.db_password.slice(0, 8)}...`));
1397
- console.log(chalk6.dim(` API auth secret: ${config.api_auth_secret.slice(0, 16)}...`));
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 ${chalk6.cyan(tfDir + "/")}`);
2275
+ console.log(` Wrote ${chalk8.cyan(tfDir + "/")}`);
1603
2276
  console.log("");
1604
- console.log(chalk6.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"));
1605
- console.log(` ${chalk6.bold("Stage:")} ${config.stage}`);
1606
- console.log(` ${chalk6.bold("Region:")} ${config.region}`);
1607
- console.log(` ${chalk6.bold("Account:")} ${config.account_id}`);
1608
- console.log(` ${chalk6.bold("Database:")} ${config.database_engine}`);
1609
- console.log(` ${chalk6.bold("Memory:")} managed (always on)${config.enable_hindsight === "true" ? " + hindsight" : ""}`);
1610
- console.log(` ${chalk6.bold("Google OAuth:")} ${config.google_oauth_client_id ? "enabled" : "disabled"}`);
1611
- console.log(` ${chalk6.bold("Directory:")} ${tfDir}`);
1612
- console.log(chalk6.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"));
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
- execSync5("terraform init", { cwd: tfDir, stdio: "inherit" });
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(` ${chalk6.cyan("1.")} thinkwork plan -s ${opts.stage} ${chalk6.dim("# Review infrastructure plan")}`);
1635
- console.log(` ${chalk6.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk6.dim("# Deploy to AWS (~5 min)")}`);
1636
- console.log(` ${chalk6.cyan("3.")} thinkwork bootstrap -s ${opts.stage} ${chalk6.dim("# Seed workspace files + skills")}`);
1637
- console.log(` ${chalk6.cyan("4.")} thinkwork outputs -s ${opts.stage} ${chalk6.dim("# Show API URL, Cognito IDs, etc.")}`);
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 execSync6 } from "child_process";
1644
- import chalk7 from "chalk";
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 runAws(cmd) {
2322
+ function runAws3(cmd) {
1650
2323
  try {
1651
- return execSync6(`aws ${cmd}`, {
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 = runAws(
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 = runAws(
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 = runAws(
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 = runAws(
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 = runAws(
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 = runAws(
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 = runAws(
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 = runAws(
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 = execSync6(`curl -s --max-time 3 http://${albRaw}/health`, { encoding: "utf-8" }).trim();
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 = runAws(
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 = runAws(
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 = runAws(
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(chalk7.bold.cyan(` \u2B21 ${info.stage}`));
1743
- console.log(chalk7.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"));
1744
- console.log(` ${chalk7.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
1745
- console.log(` ${chalk7.bold("Region:")} ${info.region}`);
1746
- console.log(` ${chalk7.bold("Account:")} ${info.accountId}`);
1747
- console.log(` ${chalk7.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
1748
- console.log(` ${chalk7.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
1749
- console.log(` ${chalk7.bold("Memory:")} managed (always on)`);
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(` ${chalk7.bold("Hindsight:")} ${hindsightLabel}`);
1752
- if (info.bucketName) console.log(` ${chalk7.bold("S3 bucket:")} ${info.bucketName}`);
1753
- if (info.dbEndpoint) console.log(` ${chalk7.bold("Database:")} ${info.dbEndpoint}`);
1754
- if (info.ecrUrl) console.log(` ${chalk7.bold("ECR:")} ${info.ecrUrl}`);
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(chalk7.bold(" URLs:"));
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(chalk7.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"));
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(chalk7.dim(` Terraform dir: ${local.terraformDir}`));
2439
+ console.log(chalk9.dim(` Terraform dir: ${local.terraformDir}`));
1767
2440
  } else {
1768
- console.log(chalk7.dim(` No local config. Run: thinkwork init -s ${info.stage}`));
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(chalk7.dim(" Scanning AWS account for Thinkwork deployments...\n"));
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 ${chalk7.cyan("thinkwork init -s <stage>")} to create one.`);
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 chalk8 from "chalk";
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 execSync8 } from "child_process";
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 getApiEndpoint(stage, region) {
2545
+ function getApiEndpoint2(stage, region) {
1912
2546
  try {
1913
- const raw = execSync8(
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 = getApiEndpoint(stage, region);
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(chalk8.dim(" No MCP servers registered."));
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 ? chalk8.green("enabled") : chalk8.dim("disabled");
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(` ${chalk8.bold(s.name)} ${chalk8.dim(s.slug)} ${status}`);
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(chalk8.bold(`
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(` ${chalk8.cyan(t.name)}${t.description ? chalk8.dim(` - ${t.description}`) : ""}`);
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 chalk9 from "chalk";
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(chalk9.dim(" No built-in tools configured."));
2164
- console.log(chalk9.dim(" Try: thinkwork tools web-search set --tenant <slug> -s <stage>"));
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 ? chalk9.green("enabled") : chalk9.dim("disabled");
2170
- const key = r.hasSecret ? chalk9.green("yes") : chalk9.red("no");
2171
- const provider = r.provider ?? chalk9.dim("\u2014");
2172
- console.log(` ${chalk9.bold(r.toolSlug)} ${status}`);
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 execSync9 } from "child_process";
2937
+ import { execSync as execSync10 } from "child_process";
2304
2938
  import { realpathSync } from "fs";
2305
- import chalk10 from "chalk";
2939
+ import chalk12 from "chalk";
2306
2940
  function getLatestVersion() {
2307
2941
  try {
2308
- return execSync9("npm view thinkwork-cli version", {
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 = execSync9("which thinkwork", {
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: ${chalk10.bold(VERSION)}`);
2981
+ console.log(` Current version: ${chalk12.bold(VERSION)}`);
2348
2982
  const latest = getLatestVersion();
2349
2983
  if (!latest) {
2350
- console.log(chalk10.yellow(" Could not check npm registry for updates."));
2984
+ console.log(chalk12.yellow(" Could not check npm registry for updates."));
2351
2985
  return;
2352
2986
  }
2353
- console.log(` Latest version: ${chalk10.bold(latest)}`);
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(chalk10.green(" \u2713 You're on the latest version."));
2991
+ console.log(chalk12.green(" \u2713 You're on the latest version."));
2358
2992
  console.log("");
2359
2993
  return;
2360
2994
  }
2361
- console.log(chalk10.cyan(` Update available: ${VERSION} \u2192 ${latest}`));
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: ${chalk10.cyan("brew upgrade thinkwork-ai/tap/thinkwork")}`);
3000
+ console.log(` Run: ${chalk12.cyan("brew upgrade thinkwork-ai/tap/thinkwork")}`);
2367
3001
  } else {
2368
- console.log(` Run: ${chalk10.cyan(`npm install -g thinkwork-cli@${latest}`)}`);
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(chalk10.dim(` $ ${cmd}`));
3010
+ console.log(chalk12.dim(` $ ${cmd}`));
2377
3011
  console.log("");
2378
3012
  try {
2379
- execSync9(cmd, { stdio: "inherit", timeout: 12e4 });
3013
+ execSync10(cmd, { stdio: "inherit", timeout: 12e4 });
2380
3014
  console.log("");
2381
- console.log(chalk10.green(` \u2713 Upgraded to thinkwork-cli@${latest}`));
3015
+ console.log(chalk12.green(` \u2713 Upgraded to thinkwork-cli@${latest}`));
2382
3016
  } catch {
2383
3017
  console.log("");
2384
- console.log(chalk10.red(` Failed to upgrade. Try manually:`));
2385
- console.log(` ${chalk10.cyan(cmd)}`);
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 spawn3 } from "child_process";
2393
- import { input, select as select2 } from "@inquirer/prompts";
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 = spawn3("terraform", ["output", "-raw", key], {
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 = spawn3("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
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 isCancellation(err) {
3070
+ function isCancellation2(err) {
2437
3071
  return err instanceof Error && err.name === "ExitPromptError";
2438
3072
  }
2439
- function requireTty(label) {
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
- requireTty("Email");
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
- requireTty("Stage");
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 select2({
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
- requireTty("Tenant");
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 select2({
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 select2({
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 (isCancellation(err)) {
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
- return;
4267
+ } else if (!process.env.AWS_PROFILE) {
4268
+ const fallback = loadCliConfig().defaultProfile;
4269
+ if (fallback) process.env.AWS_PROFILE = fallback;
2720
4270
  }
2721
- if (process.env.AWS_PROFILE) return;
2722
- const fallback = loadCliConfig().defaultProfile;
2723
- if (fallback) process.env.AWS_PROFILE = fallback;
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();