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 +42 -8
- package/dist/cli.js +338 -52
- package/dist/terraform/modules/app/hindsight-memory/main.tf +1 -1
- package/package.json +1 -1
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
|
|
46
|
-
| `thinkwork init -s <stage>` | Initialize a new environment
|
|
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
|
|
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:
|
|
742
|
-
const secretJson =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
|
850
|
-
import { resolve as resolve2, join as
|
|
851
|
-
import { execSync as
|
|
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
|
|
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 ?
|
|
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 ?
|
|
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 (
|
|
1007
|
+
if (existsSync5(join3(bundled, "modules"))) return bundled;
|
|
881
1008
|
const repoTf = resolve2(__dirname, "..", "..", "..", "..", "terraform");
|
|
882
|
-
if (
|
|
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 =
|
|
947
|
-
const tfvarsPath =
|
|
948
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
980
|
-
console.log(
|
|
981
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
999
|
-
console.log(
|
|
1000
|
-
console.log(
|
|
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
|
-
|
|
1142
|
+
mkdirSync3(tfDir, { recursive: true });
|
|
1012
1143
|
const copyDirs = ["modules", "examples"];
|
|
1013
1144
|
for (const dir of copyDirs) {
|
|
1014
|
-
const src =
|
|
1015
|
-
const dst =
|
|
1016
|
-
if (
|
|
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 =
|
|
1021
|
-
if (
|
|
1022
|
-
cpSync(schemaPath,
|
|
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 =
|
|
1027
|
-
if (!
|
|
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 ${
|
|
1236
|
+
console.log(` Wrote ${chalk5.cyan(tfDir + "/")}`);
|
|
1106
1237
|
console.log("");
|
|
1107
|
-
console.log(
|
|
1108
|
-
console.log(` ${
|
|
1109
|
-
console.log(` ${
|
|
1110
|
-
console.log(` ${
|
|
1111
|
-
console.log(` ${
|
|
1112
|
-
console.log(` ${
|
|
1113
|
-
console.log(` ${
|
|
1114
|
-
console.log(` ${
|
|
1115
|
-
console.log(
|
|
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
|
-
|
|
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(` ${
|
|
1138
|
-
console.log(` ${
|
|
1139
|
-
console.log(` ${
|
|
1140
|
-
console.log(` ${
|
|
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();
|