thinkwork-cli 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,27 +23,32 @@ thinkwork login
23
23
  # 2. Check prerequisites
24
24
  thinkwork doctor -s dev
25
25
 
26
- # 3. Initialize a new environment
26
+ # 3. Initialize a new environment (interactive)
27
27
  thinkwork init -s dev
28
28
 
29
29
  # 4. Review the plan
30
30
  thinkwork plan -s dev
31
31
 
32
- # 5. Deploy
32
+ # 5. Deploy (~5 min)
33
33
  thinkwork deploy -s dev
34
34
 
35
35
  # 6. Seed workspace files + skill catalog
36
36
  thinkwork bootstrap -s dev
37
+
38
+ # 7. Show what was deployed
39
+ thinkwork outputs -s dev
37
40
  ```
38
41
 
42
+ No repo clone required — `thinkwork init` scaffolds all Terraform modules from the npm package.
43
+
39
44
  ## Commands
40
45
 
41
46
  ### Setup
42
47
 
43
48
  | Command | Description |
44
49
  |---------|-------------|
45
- | `thinkwork login` | Configure AWS credentials (access keys or SSO) |
46
- | `thinkwork init -s <stage>` | Initialize a new environment (generates tfvars, runs terraform init) |
50
+ | `thinkwork login` | Configure AWS credentials (access keys or `--sso`) |
51
+ | `thinkwork init -s <stage>` | Initialize a new environment generates terraform.tfvars, scaffolds Terraform modules, runs `terraform init` |
47
52
  | `thinkwork doctor -s <stage>` | Check prerequisites (AWS CLI, Terraform, credentials, Bedrock access) |
48
53
 
49
54
  ### Deploy
@@ -60,6 +65,8 @@ thinkwork bootstrap -s dev
60
65
  | Command | Description |
61
66
  |---------|-------------|
62
67
  | `thinkwork outputs -s <stage>` | Show deployment outputs (API URL, Cognito IDs, etc.) |
68
+ | `thinkwork config list` | List all initialized environments |
69
+ | `thinkwork config list -s <stage>` | Show full config for an environment (secrets masked) |
63
70
  | `thinkwork config get <key> -s <stage>` | Read a configuration value |
64
71
  | `thinkwork config set <key> <value> -s <stage>` | Update a configuration value |
65
72
 
@@ -70,23 +77,48 @@ thinkwork bootstrap -s dev
70
77
  -p, --profile <name> AWS profile to use
71
78
  -c, --component <tier> Component tier: foundation, data, app, or all (default: all)
72
79
  -y, --yes Skip confirmation prompts (for CI)
80
+ --defaults Skip interactive prompts in init (use all defaults)
73
81
  -v, --version Print CLI version
74
82
  -h, --help Show help
75
83
  ```
76
84
 
85
+ ## Interactive Init
86
+
87
+ `thinkwork init` walks you through all configuration options:
88
+
89
+ - **AWS Region** — where to deploy (default: us-east-1)
90
+ - **Database engine** — `aurora-serverless` (production) or `rds-postgres` (dev, cheaper)
91
+ - **Memory engine** — `managed` (built-in) or `hindsight` (ECS Fargate with semantic + graph retrieval)
92
+ - **Google OAuth** — optional social login for Cognito
93
+ - **Admin UI URL** — callback URL for the admin dashboard
94
+ - **Mobile app scheme** — deep link scheme for the mobile app
95
+ - **Secrets** — DB password and API auth secret are auto-generated
96
+
97
+ For CI, use `--defaults` to skip prompts:
98
+
99
+ ```bash
100
+ thinkwork init -s staging --defaults
101
+ ```
102
+
103
+ ## Environment Registry
104
+
105
+ All initialized environments are saved to `~/.thinkwork/environments/<stage>/config.json`. This means:
106
+
107
+ - **No `cd` required** — all commands auto-resolve the terraform directory
108
+ - **List all stages** — `thinkwork config list` shows a table of all environments
109
+ - **Inspect any stage** — `thinkwork config list -s dev` shows full config
110
+
77
111
  ## Examples
78
112
 
79
113
  ### Switch memory engine
80
114
 
81
115
  ```bash
82
- # Switch from managed to Hindsight memory
83
116
  thinkwork config set memory-engine hindsight -s dev --apply
84
117
  ```
85
118
 
86
119
  ### Deploy a specific tier
87
120
 
88
121
  ```bash
89
- # Only deploy the app tier (Lambda functions, API Gateway)
90
122
  thinkwork deploy -s dev -c app
91
123
  ```
92
124
 
@@ -100,7 +132,9 @@ thinkwork deploy -s dev --profile my-org
100
132
  ### CI/CD (non-interactive)
101
133
 
102
134
  ```bash
135
+ thinkwork init -s prod --defaults
103
136
  thinkwork deploy -s prod -y
137
+ thinkwork bootstrap -s prod
104
138
  ```
105
139
 
106
140
  ## Prerequisites
@@ -112,14 +146,14 @@ thinkwork deploy -s prod -y
112
146
 
113
147
  ## What Gets Deployed
114
148
 
115
- Thinkwork provisions a complete AI agent stack:
149
+ Thinkwork provisions a complete AI agent stack (~250 AWS resources):
116
150
 
117
151
  - **Compute**: Lambda functions (39 handlers), AgentCore container (Lambda + ECR)
118
152
  - **Database**: Aurora Serverless PostgreSQL with pgvector
119
153
  - **Auth**: Cognito user pool (admin + mobile clients)
120
154
  - **API**: API Gateway (REST + GraphQL), AppSync (WebSocket subscriptions)
121
155
  - **Storage**: S3 (workspace files, skills, knowledge bases)
122
- - **Memory**: Managed (built-in) or Hindsight (ECS Fargate, opt-in)
156
+ - **Memory**: Managed (built-in) or Hindsight (ECS Fargate with semantic + BM25 + entity graph retrieval)
123
157
 
124
158
  ## License
125
159
 
package/dist/cli.js CHANGED
@@ -738,8 +738,8 @@ function registerBootstrapCommand(program2) {
738
738
  bucket = await getTerraformOutput(cwd, "bucket_name");
739
739
  dbEndpoint = await getTerraformOutput(cwd, "db_cluster_endpoint");
740
740
  const secretArn = await getTerraformOutput(cwd, "db_secret_arn");
741
- const { execSync: execSync5 } = await import("child_process");
742
- const secretJson = execSync5(
741
+ const { execSync: execSync7 } = await import("child_process");
742
+ const secretJson = execSync7(
743
743
  `aws secretsmanager get-secret-value --secret-id "${secretArn}" --query SecretString --output text`,
744
744
  { encoding: "utf-8" }
745
745
  ).trim();
@@ -762,8 +762,131 @@ function registerBootstrapCommand(program2) {
762
762
  }
763
763
 
764
764
  // src/commands/login.ts
765
- import { execSync as execSync3 } from "child_process";
765
+ import { execSync as execSync4 } from "child_process";
766
766
  import { createInterface as createInterface2 } from "readline";
767
+
768
+ // src/prerequisites.ts
769
+ import { execSync as execSync3 } from "child_process";
770
+ import { mkdirSync as mkdirSync2, createWriteStream, chmodSync } from "fs";
771
+ import { join as join2 } from "path";
772
+ import { homedir as homedir2, platform, arch } from "os";
773
+ import chalk4 from "chalk";
774
+ function run(cmd, opts) {
775
+ try {
776
+ return execSync3(cmd, {
777
+ encoding: "utf-8",
778
+ timeout: 3e4,
779
+ stdio: opts?.silent ? ["pipe", "pipe", "pipe"] : void 0
780
+ }).trim();
781
+ } catch {
782
+ return null;
783
+ }
784
+ }
785
+ function isInstalled(cmd) {
786
+ return run(`which ${cmd}`, { silent: true }) !== null;
787
+ }
788
+ function hasBrew() {
789
+ return isInstalled("brew");
790
+ }
791
+ async function ensureAwsCli() {
792
+ if (isInstalled("aws")) return true;
793
+ console.log(` ${chalk4.yellow("\u2192")} AWS CLI not found. Installing...`);
794
+ const os = platform();
795
+ if (os === "darwin" && hasBrew()) {
796
+ const result = run("brew install awscli");
797
+ if (result !== null && isInstalled("aws")) {
798
+ console.log(` ${chalk4.green("\u2713")} AWS CLI installed via Homebrew`);
799
+ return true;
800
+ }
801
+ }
802
+ if (os === "linux") {
803
+ try {
804
+ const tmpDir = join2(homedir2(), ".thinkwork", "tmp");
805
+ mkdirSync2(tmpDir, { recursive: true });
806
+ const zipPath = join2(tmpDir, "awscliv2.zip");
807
+ console.log(" Downloading AWS CLI...");
808
+ run(`curl -sL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "${zipPath}"`);
809
+ run(`cd "${tmpDir}" && unzip -qo "${zipPath}"`);
810
+ run(`"${tmpDir}/aws/install" --install-dir "${homedir2()}/.thinkwork/aws-cli" --bin-dir "${homedir2()}/.local/bin" --update`);
811
+ process.env.PATH = `${homedir2()}/.local/bin:${process.env.PATH}`;
812
+ if (isInstalled("aws")) {
813
+ console.log(` ${chalk4.green("\u2713")} AWS CLI installed to ~/.local/bin/aws`);
814
+ return true;
815
+ }
816
+ } catch {
817
+ }
818
+ }
819
+ if (os === "darwin") {
820
+ try {
821
+ const tmpDir = join2(homedir2(), ".thinkwork", "tmp");
822
+ mkdirSync2(tmpDir, { recursive: true });
823
+ const pkgPath = join2(tmpDir, "AWSCLIV2.pkg");
824
+ console.log(" Downloading AWS CLI...");
825
+ run(`curl -sL "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "${pkgPath}"`);
826
+ run(`installer -pkg "${pkgPath}" -target CurrentUserHomeDirectory 2>/dev/null || sudo installer -pkg "${pkgPath}" -target /`);
827
+ if (isInstalled("aws")) {
828
+ console.log(` ${chalk4.green("\u2713")} AWS CLI installed`);
829
+ return true;
830
+ }
831
+ } catch {
832
+ }
833
+ }
834
+ console.log(` ${chalk4.red("\u2717")} Could not auto-install AWS CLI.`);
835
+ console.log(` Install manually: ${chalk4.cyan("https://aws.amazon.com/cli/")}`);
836
+ return false;
837
+ }
838
+ async function ensureTerraform() {
839
+ if (isInstalled("terraform")) return true;
840
+ console.log(` ${chalk4.yellow("\u2192")} Terraform not found. Installing...`);
841
+ const os = platform();
842
+ if ((os === "darwin" || os === "linux") && hasBrew()) {
843
+ const result = run("brew install hashicorp/tap/terraform");
844
+ if (result !== null && isInstalled("terraform")) {
845
+ console.log(` ${chalk4.green("\u2713")} Terraform installed via Homebrew`);
846
+ return true;
847
+ }
848
+ }
849
+ const tfVersion = "1.12.1";
850
+ const osName = os === "darwin" ? "darwin" : "linux";
851
+ const archName = arch() === "arm64" ? "arm64" : "amd64";
852
+ const url = `https://releases.hashicorp.com/terraform/${tfVersion}/terraform_${tfVersion}_${osName}_${archName}.zip`;
853
+ try {
854
+ const tmpDir = join2(homedir2(), ".thinkwork", "tmp");
855
+ const binDir = join2(homedir2(), ".local", "bin");
856
+ mkdirSync2(tmpDir, { recursive: true });
857
+ mkdirSync2(binDir, { recursive: true });
858
+ const zipPath = join2(tmpDir, "terraform.zip");
859
+ console.log(` Downloading Terraform ${tfVersion}...`);
860
+ run(`curl -sL "${url}" -o "${zipPath}"`);
861
+ run(`unzip -qo "${zipPath}" -d "${binDir}"`);
862
+ chmodSync(join2(binDir, "terraform"), 493);
863
+ if (!process.env.PATH?.includes(binDir)) {
864
+ process.env.PATH = `${binDir}:${process.env.PATH}`;
865
+ }
866
+ if (isInstalled("terraform")) {
867
+ console.log(` ${chalk4.green("\u2713")} Terraform ${tfVersion} installed to ~/.local/bin/terraform`);
868
+ return true;
869
+ }
870
+ } catch {
871
+ }
872
+ console.log(` ${chalk4.red("\u2717")} Could not auto-install Terraform.`);
873
+ console.log(` Install manually: ${chalk4.cyan("https://developer.hashicorp.com/terraform/install")}`);
874
+ return false;
875
+ }
876
+ async function ensurePrerequisites() {
877
+ console.log(chalk4.dim(" Checking prerequisites...\n"));
878
+ const awsOk = await ensureAwsCli();
879
+ const tfOk = await ensureTerraform();
880
+ if (awsOk && tfOk) {
881
+ console.log("");
882
+ return true;
883
+ }
884
+ console.log("");
885
+ console.log(` ${chalk4.red("Missing prerequisites.")} Install them and try again.`);
886
+ return false;
887
+ }
888
+
889
+ // src/commands/login.ts
767
890
  function ask(prompt) {
768
891
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
769
892
  return new Promise((resolve3) => {
@@ -776,6 +899,10 @@ function ask(prompt) {
776
899
  function registerLoginCommand(program2) {
777
900
  program2.command("login").description("Configure AWS credentials for Thinkwork deployments").option("--profile <name>", "AWS profile name to configure", "thinkwork").option("--sso", "Use AWS SSO (Identity Center) login").action(async (opts) => {
778
901
  printHeader("login", opts.profile);
902
+ const awsOk = await ensureAwsCli();
903
+ if (!awsOk) {
904
+ process.exit(1);
905
+ }
779
906
  const existing = getAwsIdentity();
780
907
  if (existing) {
781
908
  console.log(` Already authenticated:`);
@@ -793,7 +920,7 @@ function registerLoginCommand(program2) {
793
920
  console.log(" Launching AWS SSO login...");
794
921
  console.log("");
795
922
  try {
796
- execSync3(`aws sso login --profile ${opts.profile}`, {
923
+ execSync4(`aws sso login --profile ${opts.profile}`, {
797
924
  stdio: "inherit"
798
925
  });
799
926
  process.env.AWS_PROFILE = opts.profile;
@@ -824,9 +951,9 @@ function registerLoginCommand(program2) {
824
951
  const region = await ask(" Default region [us-east-1]: ");
825
952
  const finalRegion = region || "us-east-1";
826
953
  try {
827
- execSync3(`aws configure set aws_access_key_id "${accessKeyId}" --profile ${opts.profile}`, { stdio: "pipe" });
828
- execSync3(`aws configure set aws_secret_access_key "${secretAccessKey}" --profile ${opts.profile}`, { stdio: "pipe" });
829
- execSync3(`aws configure set region "${finalRegion}" --profile ${opts.profile}`, { stdio: "pipe" });
954
+ execSync4(`aws configure set aws_access_key_id "${accessKeyId}" --profile ${opts.profile}`, { stdio: "pipe" });
955
+ execSync4(`aws configure set aws_secret_access_key "${secretAccessKey}" --profile ${opts.profile}`, { stdio: "pipe" });
956
+ execSync4(`aws configure set region "${finalRegion}" --profile ${opts.profile}`, { stdio: "pipe" });
830
957
  } catch (err) {
831
958
  printError(`Failed to save credentials: ${err}`);
832
959
  process.exit(1);
@@ -846,16 +973,16 @@ function registerLoginCommand(program2) {
846
973
  }
847
974
 
848
975
  // src/commands/init.ts
849
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, cpSync } from "fs";
850
- import { resolve as resolve2, join as join2, dirname } from "path";
851
- import { execSync as execSync4 } from "child_process";
976
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, cpSync } from "fs";
977
+ import { resolve as resolve2, join as join3, dirname } from "path";
978
+ import { execSync as execSync5 } from "child_process";
852
979
  import { fileURLToPath } from "url";
853
980
  import { createInterface as createInterface3 } from "readline";
854
- import chalk4 from "chalk";
981
+ import chalk5 from "chalk";
855
982
  var __dirname = dirname(fileURLToPath(import.meta.url));
856
983
  function ask2(prompt, defaultVal = "") {
857
984
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
858
- const suffix = defaultVal ? chalk4.dim(` [${defaultVal}]`) : "";
985
+ const suffix = defaultVal ? chalk5.dim(` [${defaultVal}]`) : "";
859
986
  return new Promise((resolve3) => {
860
987
  rl.question(` ${prompt}${suffix}: `, (answer) => {
861
988
  rl.close();
@@ -864,7 +991,7 @@ function ask2(prompt, defaultVal = "") {
864
991
  });
865
992
  }
866
993
  function choose(prompt, options, defaultVal) {
867
- const optStr = options.map((o) => o === defaultVal ? chalk4.bold(o) : chalk4.dim(o)).join(" / ");
994
+ const optStr = options.map((o) => o === defaultVal ? chalk5.bold(o) : chalk5.dim(o)).join(" / ");
868
995
  return ask2(`${prompt} (${optStr})`, defaultVal);
869
996
  }
870
997
  function generateSecret(length = 32) {
@@ -877,9 +1004,9 @@ function generateSecret(length = 32) {
877
1004
  }
878
1005
  function findBundledTerraform() {
879
1006
  const bundled = resolve2(__dirname, "..", "terraform");
880
- if (existsSync4(join2(bundled, "modules"))) return bundled;
1007
+ if (existsSync5(join3(bundled, "modules"))) return bundled;
881
1008
  const repoTf = resolve2(__dirname, "..", "..", "..", "..", "terraform");
882
- if (existsSync4(join2(repoTf, "modules"))) return repoTf;
1009
+ if (existsSync5(join3(repoTf, "modules"))) return repoTf;
883
1010
  throw new Error(
884
1011
  "Terraform modules not found. The CLI package may be incomplete.\nTry reinstalling: npm install -g thinkwork-cli@latest"
885
1012
  );
@@ -938,14 +1065,18 @@ function registerInitCommand(program2) {
938
1065
  }
939
1066
  const identity = getAwsIdentity();
940
1067
  printHeader("init", opts.stage, identity);
1068
+ const prereqsOk = await ensurePrerequisites();
1069
+ if (!prereqsOk) {
1070
+ process.exit(1);
1071
+ }
941
1072
  if (!identity) {
942
1073
  printError("AWS credentials not configured. Run `thinkwork login` first.");
943
1074
  process.exit(1);
944
1075
  }
945
1076
  const targetDir = resolve2(opts.dir);
946
- const tfDir = join2(targetDir, "terraform");
947
- const tfvarsPath = join2(tfDir, "terraform.tfvars");
948
- if (existsSync4(tfvarsPath)) {
1077
+ const tfDir = join3(targetDir, "terraform");
1078
+ const tfvarsPath = join3(tfDir, "terraform.tfvars");
1079
+ if (existsSync5(tfvarsPath)) {
949
1080
  printWarning(`terraform.tfvars already exists at ${tfvarsPath}`);
950
1081
  const overwrite = await ask2("Overwrite?", "N");
951
1082
  if (overwrite.toLowerCase() !== "y") {
@@ -969,19 +1100,19 @@ function registerInitCommand(program2) {
969
1100
  config.admin_url = "http://localhost:5174";
970
1101
  config.mobile_scheme = "thinkwork";
971
1102
  } else {
972
- console.log(chalk4.bold(" Configure your Thinkwork environment\n"));
1103
+ console.log(chalk5.bold(" Configure your Thinkwork environment\n"));
973
1104
  const defaultRegion = identity.region !== "unknown" ? identity.region : "us-east-1";
974
1105
  config.region = await ask2("AWS Region", defaultRegion);
975
1106
  console.log("");
976
- console.log(chalk4.dim(" \u2500\u2500 Database \u2500\u2500"));
1107
+ console.log(chalk5.dim(" \u2500\u2500 Database \u2500\u2500"));
977
1108
  config.database_engine = await choose("Database engine", ["aurora-serverless", "rds-postgres"], "aurora-serverless");
978
1109
  console.log("");
979
- console.log(chalk4.dim(" \u2500\u2500 Memory \u2500\u2500"));
980
- console.log(chalk4.dim(" managed = Built-in AgentCore memory (remember/recall/forget)"));
981
- console.log(chalk4.dim(" hindsight = ECS Fargate service with semantic + graph retrieval"));
1110
+ console.log(chalk5.dim(" \u2500\u2500 Memory \u2500\u2500"));
1111
+ console.log(chalk5.dim(" managed = Built-in AgentCore memory (remember/recall/forget)"));
1112
+ console.log(chalk5.dim(" hindsight = ECS Fargate service with semantic + graph retrieval"));
982
1113
  config.memory_engine = await choose("Memory engine", ["managed", "hindsight"], "managed");
983
1114
  console.log("");
984
- console.log(chalk4.dim(" \u2500\u2500 Auth \u2500\u2500"));
1115
+ console.log(chalk5.dim(" \u2500\u2500 Auth \u2500\u2500"));
985
1116
  const useGoogle = await ask2("Enable Google OAuth login? (y/N)", "N");
986
1117
  if (useGoogle.toLowerCase() === "y") {
987
1118
  config.google_oauth_client_id = await ask2("Google OAuth Client ID");
@@ -991,13 +1122,13 @@ function registerInitCommand(program2) {
991
1122
  config.google_oauth_client_secret = "";
992
1123
  }
993
1124
  console.log("");
994
- console.log(chalk4.dim(" \u2500\u2500 Frontend URLs \u2500\u2500"));
1125
+ console.log(chalk5.dim(" \u2500\u2500 Frontend URLs \u2500\u2500"));
995
1126
  config.admin_url = await ask2("Admin UI URL", "http://localhost:5174");
996
1127
  config.mobile_scheme = await ask2("Mobile app URL scheme", "thinkwork");
997
1128
  console.log("");
998
- console.log(chalk4.dim(" \u2500\u2500 Secrets (auto-generated) \u2500\u2500"));
999
- console.log(chalk4.dim(` DB password: ${config.db_password.slice(0, 8)}...`));
1000
- console.log(chalk4.dim(` API auth secret: ${config.api_auth_secret.slice(0, 16)}...`));
1129
+ console.log(chalk5.dim(" \u2500\u2500 Secrets (auto-generated) \u2500\u2500"));
1130
+ console.log(chalk5.dim(` DB password: ${config.db_password.slice(0, 8)}...`));
1131
+ console.log(chalk5.dim(` API auth secret: ${config.api_auth_secret.slice(0, 16)}...`));
1001
1132
  }
1002
1133
  console.log("");
1003
1134
  console.log(" Scaffolding Terraform modules...");
@@ -1008,23 +1139,23 @@ function registerInitCommand(program2) {
1008
1139
  printError(String(err));
1009
1140
  process.exit(1);
1010
1141
  }
1011
- mkdirSync2(tfDir, { recursive: true });
1142
+ mkdirSync3(tfDir, { recursive: true });
1012
1143
  const copyDirs = ["modules", "examples"];
1013
1144
  for (const dir of copyDirs) {
1014
- const src = join2(bundledTf, dir);
1015
- const dst = join2(tfDir, dir);
1016
- if (existsSync4(src) && !existsSync4(dst)) {
1145
+ const src = join3(bundledTf, dir);
1146
+ const dst = join3(tfDir, dir);
1147
+ if (existsSync5(src) && !existsSync5(dst)) {
1017
1148
  cpSync(src, dst, { recursive: true });
1018
1149
  }
1019
1150
  }
1020
- const schemaPath = join2(bundledTf, "schema.graphql");
1021
- if (existsSync4(schemaPath) && !existsSync4(join2(tfDir, "schema.graphql"))) {
1022
- cpSync(schemaPath, join2(tfDir, "schema.graphql"));
1151
+ const schemaPath = join3(bundledTf, "schema.graphql");
1152
+ if (existsSync5(schemaPath) && !existsSync5(join3(tfDir, "schema.graphql"))) {
1153
+ cpSync(schemaPath, join3(tfDir, "schema.graphql"));
1023
1154
  }
1024
1155
  const tfvars = buildTfvars(config);
1025
1156
  writeFileSync3(tfvarsPath, tfvars);
1026
- const mainTfPath = join2(tfDir, "main.tf");
1027
- if (!existsSync4(mainTfPath)) {
1157
+ const mainTfPath = join3(tfDir, "main.tf");
1158
+ if (!existsSync5(mainTfPath)) {
1028
1159
  writeFileSync3(mainTfPath, `################################################################################
1029
1160
  # Thinkwork \u2014 ${config.stage}
1030
1161
  # Generated by: thinkwork init -s ${config.stage}
@@ -1102,20 +1233,20 @@ output "memory_engine" { value = module.thinkwork.memory_engine }
1102
1233
  output "hindsight_endpoint" { value = module.thinkwork.hindsight_endpoint }
1103
1234
  `);
1104
1235
  }
1105
- console.log(` Wrote ${chalk4.cyan(tfDir + "/")}`);
1236
+ console.log(` Wrote ${chalk5.cyan(tfDir + "/")}`);
1106
1237
  console.log("");
1107
- 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"));
1108
- console.log(` ${chalk4.bold("Stage:")} ${config.stage}`);
1109
- console.log(` ${chalk4.bold("Region:")} ${config.region}`);
1110
- console.log(` ${chalk4.bold("Account:")} ${config.account_id}`);
1111
- console.log(` ${chalk4.bold("Database:")} ${config.database_engine}`);
1112
- console.log(` ${chalk4.bold("Memory:")} ${config.memory_engine}`);
1113
- console.log(` ${chalk4.bold("Google OAuth:")} ${config.google_oauth_client_id ? "enabled" : "disabled"}`);
1114
- console.log(` ${chalk4.bold("Directory:")} ${tfDir}`);
1115
- 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"));
1238
+ console.log(chalk5.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"));
1239
+ console.log(` ${chalk5.bold("Stage:")} ${config.stage}`);
1240
+ console.log(` ${chalk5.bold("Region:")} ${config.region}`);
1241
+ console.log(` ${chalk5.bold("Account:")} ${config.account_id}`);
1242
+ console.log(` ${chalk5.bold("Database:")} ${config.database_engine}`);
1243
+ console.log(` ${chalk5.bold("Memory:")} ${config.memory_engine}`);
1244
+ console.log(` ${chalk5.bold("Google OAuth:")} ${config.google_oauth_client_id ? "enabled" : "disabled"}`);
1245
+ console.log(` ${chalk5.bold("Directory:")} ${tfDir}`);
1246
+ console.log(chalk5.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"));
1116
1247
  console.log("\n Initializing Terraform...\n");
1117
1248
  try {
1118
- execSync4("terraform init", { cwd: tfDir, stdio: "inherit" });
1249
+ execSync5("terraform init", { cwd: tfDir, stdio: "inherit" });
1119
1250
  } catch {
1120
1251
  printWarning("Terraform init failed. Run `thinkwork doctor -s " + opts.stage + "` to check prerequisites.");
1121
1252
  return;
@@ -1134,10 +1265,164 @@ output "hindsight_endpoint" { value = module.thinkwork.hindsight_endpoint }
1134
1265
  printSuccess(`Environment "${opts.stage}" initialized`);
1135
1266
  console.log("");
1136
1267
  console.log(" Next steps:");
1137
- console.log(` ${chalk4.cyan("1.")} thinkwork plan -s ${opts.stage} ${chalk4.dim("# Review infrastructure plan")}`);
1138
- console.log(` ${chalk4.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk4.dim("# Deploy to AWS (~5 min)")}`);
1139
- console.log(` ${chalk4.cyan("3.")} thinkwork bootstrap -s ${opts.stage} ${chalk4.dim("# Seed workspace files + skills")}`);
1140
- console.log(` ${chalk4.cyan("4.")} thinkwork outputs -s ${opts.stage} ${chalk4.dim("# Show API URL, Cognito IDs, etc.")}`);
1268
+ console.log(` ${chalk5.cyan("1.")} thinkwork plan -s ${opts.stage} ${chalk5.dim("# Review infrastructure plan")}`);
1269
+ console.log(` ${chalk5.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk5.dim("# Deploy to AWS (~5 min)")}`);
1270
+ console.log(` ${chalk5.cyan("3.")} thinkwork bootstrap -s ${opts.stage} ${chalk5.dim("# Seed workspace files + skills")}`);
1271
+ console.log(` ${chalk5.cyan("4.")} thinkwork outputs -s ${opts.stage} ${chalk5.dim("# Show API URL, Cognito IDs, etc.")}`);
1272
+ console.log("");
1273
+ });
1274
+ }
1275
+
1276
+ // src/commands/status.ts
1277
+ import { execSync as execSync6 } from "child_process";
1278
+ import chalk6 from "chalk";
1279
+ function runAws(cmd) {
1280
+ try {
1281
+ return execSync6(`aws ${cmd}`, {
1282
+ encoding: "utf-8",
1283
+ timeout: 15e3,
1284
+ stdio: ["pipe", "pipe", "pipe"]
1285
+ }).trim();
1286
+ } catch {
1287
+ return null;
1288
+ }
1289
+ }
1290
+ function discoverAwsStages(region) {
1291
+ const stages = /* @__PURE__ */ new Map();
1292
+ const raw = runAws(
1293
+ `lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
1294
+ );
1295
+ if (!raw) return stages;
1296
+ const functions = JSON.parse(raw);
1297
+ for (const fn of functions) {
1298
+ const match = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
1299
+ if (match) {
1300
+ const stage = match[1];
1301
+ stages.set(stage, { stage, source: "aws", region });
1302
+ }
1303
+ }
1304
+ for (const [stage, info] of stages) {
1305
+ const count = functions.filter((f) => f.startsWith(`thinkwork-${stage}-`)).length;
1306
+ info.lambdaCount = count;
1307
+ const apiRaw = runAws(
1308
+ `apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
1309
+ );
1310
+ if (apiRaw && apiRaw !== "None") info.apiEndpoint = apiRaw;
1311
+ const acRaw = runAws(
1312
+ `lambda get-function --function-name thinkwork-${stage}-agentcore --region ${region} --query "Configuration.State" --output text 2>/dev/null`
1313
+ );
1314
+ info.agentcoreStatus = acRaw || "not deployed";
1315
+ const bucketRaw = runAws(
1316
+ `s3api head-bucket --bucket thinkwork-${stage}-storage --region ${region} 2>/dev/null && echo "exists"`
1317
+ );
1318
+ info.bucketName = bucketRaw ? `thinkwork-${stage}-storage` : void 0;
1319
+ const ecsRaw = runAws(
1320
+ `ecs describe-services --cluster thinkwork-${stage}-cluster --services thinkwork-${stage}-hindsight --region ${region} --query "services[0].runningCount" --output text 2>/dev/null`
1321
+ );
1322
+ if (ecsRaw && ecsRaw !== "None" && ecsRaw !== "0") {
1323
+ info.memoryEngine = "hindsight";
1324
+ const albRaw = runAws(
1325
+ `elbv2 describe-load-balancers --region ${region} --query "LoadBalancers[?contains(LoadBalancerName, 'tw-${stage}-hindsight')].DNSName|[0]" --output text`
1326
+ );
1327
+ if (albRaw && albRaw !== "None") {
1328
+ try {
1329
+ const health = execSync6(`curl -s --max-time 3 http://${albRaw}/health`, { encoding: "utf-8" }).trim();
1330
+ info.hindsightHealth = health.includes("healthy") ? "healthy" : "unhealthy";
1331
+ } catch {
1332
+ info.hindsightHealth = "unreachable";
1333
+ }
1334
+ }
1335
+ } else {
1336
+ info.memoryEngine = "managed";
1337
+ }
1338
+ }
1339
+ return stages;
1340
+ }
1341
+ function registerStatusCommand(program2) {
1342
+ program2.command("status").description("Show all Thinkwork environments (AWS + local)").option("-s, --stage <name>", "Show details for a specific stage").option("--region <region>", "AWS region to scan", "us-east-1").action(async (opts) => {
1343
+ const identity = getAwsIdentity();
1344
+ printHeader("status", opts.stage || "all", identity);
1345
+ if (!identity) {
1346
+ printError("AWS credentials not configured. Run `thinkwork login` first.");
1347
+ process.exit(1);
1348
+ }
1349
+ console.log(chalk6.dim(" Scanning AWS account for Thinkwork deployments...\n"));
1350
+ const awsStages = discoverAwsStages(opts.region);
1351
+ const localEnvs = listEnvironments();
1352
+ const merged = /* @__PURE__ */ new Map();
1353
+ for (const [stage, info] of awsStages) {
1354
+ const local = localEnvs.find((e) => e.stage === stage);
1355
+ merged.set(stage, {
1356
+ stage,
1357
+ source: local ? "both" : "aws",
1358
+ region: opts.region,
1359
+ accountId: identity.account,
1360
+ ...info
1361
+ });
1362
+ }
1363
+ for (const env of localEnvs) {
1364
+ if (!merged.has(env.stage)) {
1365
+ merged.set(env.stage, {
1366
+ stage: env.stage,
1367
+ source: "local",
1368
+ region: env.region,
1369
+ accountId: env.accountId
1370
+ });
1371
+ }
1372
+ }
1373
+ if (opts.stage) {
1374
+ const info = merged.get(opts.stage);
1375
+ if (!info) {
1376
+ printError(`No environment "${opts.stage}" found in AWS or local config.`);
1377
+ process.exit(1);
1378
+ }
1379
+ console.log(chalk6.bold.cyan(` \u2B21 ${info.stage}`));
1380
+ 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\u2500\u2500\u2500\u2500"));
1381
+ console.log(` ${chalk6.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
1382
+ console.log(` ${chalk6.bold("Region:")} ${info.region}`);
1383
+ console.log(` ${chalk6.bold("Account:")} ${info.accountId}`);
1384
+ if (info.apiEndpoint) console.log(` ${chalk6.bold("API:")} ${info.apiEndpoint}`);
1385
+ if (info.lambdaCount) console.log(` ${chalk6.bold("Lambda fns:")} ${info.lambdaCount}`);
1386
+ console.log(` ${chalk6.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
1387
+ console.log(` ${chalk6.bold("Memory:")} ${info.memoryEngine || "unknown"}`);
1388
+ if (info.hindsightHealth) console.log(` ${chalk6.bold("Hindsight:")} ${info.hindsightHealth}`);
1389
+ if (info.bucketName) console.log(` ${chalk6.bold("S3 bucket:")} ${info.bucketName}`);
1390
+ 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\u2500\u2500\u2500\u2500"));
1391
+ const local = loadEnvironment(opts.stage);
1392
+ if (local) {
1393
+ console.log("");
1394
+ console.log(chalk6.dim(` Terraform dir: ${local.terraformDir}`));
1395
+ } else {
1396
+ console.log("");
1397
+ console.log(chalk6.dim(` No local config. Run: thinkwork init -s ${opts.stage}`));
1398
+ }
1399
+ console.log("");
1400
+ return;
1401
+ }
1402
+ if (merged.size === 0) {
1403
+ console.log(" No Thinkwork environments found.");
1404
+ console.log(` Run ${chalk6.cyan("thinkwork init -s <stage>")} to create one.`);
1405
+ console.log("");
1406
+ return;
1407
+ }
1408
+ console.log(chalk6.bold(" Environments"));
1409
+ 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1410
+ console.log(
1411
+ chalk6.dim(" ") + "Stage".padEnd(16) + "Source".padEnd(10) + "Lambdas".padEnd(10) + "AgentCore".padEnd(14) + "Memory".padEnd(14) + "API"
1412
+ );
1413
+ 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1414
+ for (const [, info] of [...merged].sort((a, b) => a[0].localeCompare(b[0]))) {
1415
+ const sourceBadge = info.source === "both" ? chalk6.green("\u25CF") : info.source === "aws" ? chalk6.yellow("\u25CF") : chalk6.dim("\u25CB");
1416
+ const acStatus = info.agentcoreStatus === "Active" ? chalk6.green("active") : info.agentcoreStatus === "not deployed" ? chalk6.dim("\u2014") : chalk6.yellow(info.agentcoreStatus || "\u2014");
1417
+ const memBadge = info.memoryEngine === "hindsight" ? info.hindsightHealth === "healthy" ? chalk6.magenta("hindsight \u2713") : chalk6.yellow("hindsight ?") : chalk6.dim(info.memoryEngine || "\u2014");
1418
+ console.log(
1419
+ ` ${sourceBadge} ` + chalk6.bold(info.stage.padEnd(14)) + (info.source === "both" ? "aws+cli" : info.source).padEnd(10) + String(info.lambdaCount || "\u2014").padEnd(10) + acStatus.padEnd(22) + memBadge.padEnd(22) + chalk6.dim(info.apiEndpoint || "\u2014")
1420
+ );
1421
+ }
1422
+ 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1423
+ console.log(chalk6.dim(` ${merged.size} environment(s) `) + chalk6.green("\u25CF") + chalk6.dim(" aws+cli ") + chalk6.yellow("\u25CF") + chalk6.dim(" aws only ") + chalk6.dim("\u25CB local only"));
1424
+ console.log("");
1425
+ console.log(` Details: ${chalk6.cyan("thinkwork status -s <stage>")}`);
1141
1426
  console.log("");
1142
1427
  });
1143
1428
  }
@@ -1163,6 +1448,7 @@ registerPlanCommand(program);
1163
1448
  registerDeployCommand(program);
1164
1449
  registerBootstrapCommand(program);
1165
1450
  registerDestroyCommand(program);
1451
+ registerStatusCommand(program);
1166
1452
  registerOutputsCommand(program);
1167
1453
  registerConfigCommand(program);
1168
1454
  program.parse();
@@ -34,7 +34,7 @@ variable "database_url" {
34
34
  variable "image_tag" {
35
35
  description = "Hindsight Docker image tag (ghcr.io/vectorize-io/hindsight:<tag>)"
36
36
  type = string
37
- default = "0.4.22"
37
+ default = "0.5.0"
38
38
  }
39
39
 
40
40
  data "aws_region" "current" {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkwork-cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Thinkwork CLI — deploy, manage, and interact with your Thinkwork stack",
5
5
  "license": "MIT",
6
6
  "type": "module",