thinkwork-cli 0.2.0 → 0.3.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/README.md +42 -8
- package/dist/cli.js +337 -70
- 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
|
@@ -485,19 +485,67 @@ function registerOutputsCommand(program2) {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
// src/commands/config.ts
|
|
488
|
-
import { readFileSync, writeFileSync, existsSync as
|
|
488
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
489
|
+
import chalk3 from "chalk";
|
|
490
|
+
|
|
491
|
+
// src/environments.ts
|
|
492
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
|
|
493
|
+
import { join } from "path";
|
|
494
|
+
import { homedir } from "os";
|
|
495
|
+
var THINKWORK_HOME = join(homedir(), ".thinkwork");
|
|
496
|
+
var ENVIRONMENTS_DIR = join(THINKWORK_HOME, "environments");
|
|
497
|
+
function ensureDir(dir) {
|
|
498
|
+
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
|
|
499
|
+
}
|
|
500
|
+
function saveEnvironment(config) {
|
|
501
|
+
ensureDir(ENVIRONMENTS_DIR);
|
|
502
|
+
const envDir = join(ENVIRONMENTS_DIR, config.stage);
|
|
503
|
+
ensureDir(envDir);
|
|
504
|
+
writeFileSync(
|
|
505
|
+
join(envDir, "config.json"),
|
|
506
|
+
JSON.stringify(config, null, 2) + "\n"
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
function loadEnvironment(stage) {
|
|
510
|
+
const configPath = join(ENVIRONMENTS_DIR, stage, "config.json");
|
|
511
|
+
if (!existsSync2(configPath)) return null;
|
|
512
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
513
|
+
}
|
|
514
|
+
function listEnvironments() {
|
|
515
|
+
if (!existsSync2(ENVIRONMENTS_DIR)) return [];
|
|
516
|
+
return readdirSync(ENVIRONMENTS_DIR).filter((name) => {
|
|
517
|
+
return existsSync2(join(ENVIRONMENTS_DIR, name, "config.json"));
|
|
518
|
+
}).map((name) => {
|
|
519
|
+
return JSON.parse(
|
|
520
|
+
readFileSync(join(ENVIRONMENTS_DIR, name, "config.json"), "utf-8")
|
|
521
|
+
);
|
|
522
|
+
}).sort((a, b) => a.stage.localeCompare(b.stage));
|
|
523
|
+
}
|
|
524
|
+
function resolveTerraformDir(stage) {
|
|
525
|
+
const env = loadEnvironment(stage);
|
|
526
|
+
if (env?.terraformDir && existsSync2(env.terraformDir)) {
|
|
527
|
+
return env.terraformDir;
|
|
528
|
+
}
|
|
529
|
+
const envVar = process.env.THINKWORK_TERRAFORM_DIR;
|
|
530
|
+
if (envVar && existsSync2(envVar)) return envVar;
|
|
531
|
+
const cwdTf = join(process.cwd(), "terraform");
|
|
532
|
+
if (existsSync2(join(cwdTf, "main.tf"))) return cwdTf;
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/commands/config.ts
|
|
489
537
|
var VALID_MEMORY_ENGINES = ["managed", "hindsight"];
|
|
490
538
|
function readTfVar(tfvarsPath, key) {
|
|
491
|
-
if (!
|
|
492
|
-
const content =
|
|
539
|
+
if (!existsSync3(tfvarsPath)) return null;
|
|
540
|
+
const content = readFileSync2(tfvarsPath, "utf-8");
|
|
493
541
|
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
|
|
494
542
|
return match ? match[1] : null;
|
|
495
543
|
}
|
|
496
544
|
function setTfVar(tfvarsPath, key, value) {
|
|
497
|
-
if (!
|
|
545
|
+
if (!existsSync3(tfvarsPath)) {
|
|
498
546
|
throw new Error(`terraform.tfvars not found at ${tfvarsPath}`);
|
|
499
547
|
}
|
|
500
|
-
let content =
|
|
548
|
+
let content = readFileSync2(tfvarsPath, "utf-8");
|
|
501
549
|
const regex = new RegExp(`^(${key}\\s*=\\s*)"[^"]*"`, "m");
|
|
502
550
|
if (regex.test(content)) {
|
|
503
551
|
content = content.replace(regex, `$1"${value}"`);
|
|
@@ -506,19 +554,93 @@ function setTfVar(tfvarsPath, key, value) {
|
|
|
506
554
|
${key} = "${value}"
|
|
507
555
|
`;
|
|
508
556
|
}
|
|
509
|
-
|
|
557
|
+
writeFileSync2(tfvarsPath, content);
|
|
558
|
+
}
|
|
559
|
+
function resolveTfvarsPath(stage) {
|
|
560
|
+
const tfDir = resolveTerraformDir(stage);
|
|
561
|
+
if (tfDir) {
|
|
562
|
+
const direct = `${tfDir}/terraform.tfvars`;
|
|
563
|
+
if (existsSync3(direct)) return direct;
|
|
564
|
+
}
|
|
565
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
566
|
+
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
567
|
+
return `${cwd}/terraform.tfvars`;
|
|
510
568
|
}
|
|
511
569
|
function registerConfigCommand(program2) {
|
|
512
570
|
const config = program2.command("config").description("View or change stack configuration");
|
|
571
|
+
config.command("list").description("List all environments, or show config for a specific stage").option("-s, --stage <name>", "Show config for a specific stage").action((opts) => {
|
|
572
|
+
if (opts.stage) {
|
|
573
|
+
const env = loadEnvironment(opts.stage);
|
|
574
|
+
if (!env) {
|
|
575
|
+
printError(`Environment "${opts.stage}" not found. Run \`thinkwork init -s ${opts.stage}\` first.`);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
console.log("");
|
|
579
|
+
console.log(chalk3.bold.cyan(` \u2B21 ${env.stage}`));
|
|
580
|
+
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"));
|
|
581
|
+
console.log(` ${chalk3.bold("Region:")} ${env.region}`);
|
|
582
|
+
console.log(` ${chalk3.bold("Account:")} ${env.accountId}`);
|
|
583
|
+
console.log(` ${chalk3.bold("Database:")} ${env.databaseEngine}`);
|
|
584
|
+
console.log(` ${chalk3.bold("Memory:")} ${env.memoryEngine}`);
|
|
585
|
+
console.log(` ${chalk3.bold("Terraform dir:")} ${env.terraformDir}`);
|
|
586
|
+
console.log(` ${chalk3.bold("Created:")} ${env.createdAt}`);
|
|
587
|
+
console.log(` ${chalk3.bold("Updated:")} ${env.updatedAt}`);
|
|
588
|
+
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"));
|
|
589
|
+
const tfvarsPath = `${env.terraformDir}/terraform.tfvars`;
|
|
590
|
+
if (existsSync3(tfvarsPath)) {
|
|
591
|
+
console.log("");
|
|
592
|
+
console.log(chalk3.dim(" terraform.tfvars:"));
|
|
593
|
+
const content = readFileSync2(tfvarsPath, "utf-8");
|
|
594
|
+
for (const line of content.split("\n")) {
|
|
595
|
+
if (line.trim() && !line.trim().startsWith("#")) {
|
|
596
|
+
const masked = line.replace(
|
|
597
|
+
/^(db_password\s*=\s*)".*"/,
|
|
598
|
+
'$1"********"'
|
|
599
|
+
).replace(
|
|
600
|
+
/^(api_auth_secret\s*=\s*)".*"/,
|
|
601
|
+
'$1"********"'
|
|
602
|
+
).replace(
|
|
603
|
+
/^(google_oauth_client_secret\s*=\s*)".*"/,
|
|
604
|
+
'$1"********"'
|
|
605
|
+
);
|
|
606
|
+
console.log(` ${chalk3.dim(masked)}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
console.log("");
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const envs = listEnvironments();
|
|
614
|
+
if (envs.length === 0) {
|
|
615
|
+
console.log("");
|
|
616
|
+
console.log(" No environments found.");
|
|
617
|
+
console.log(` Run ${chalk3.cyan("thinkwork init -s <stage>")} to create one.`);
|
|
618
|
+
console.log("");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
console.log("");
|
|
622
|
+
console.log(chalk3.bold(" Environments"));
|
|
623
|
+
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"));
|
|
624
|
+
for (const env of envs) {
|
|
625
|
+
const memBadge = env.memoryEngine === "hindsight" ? chalk3.magenta("hindsight") : chalk3.dim("managed");
|
|
626
|
+
const dbBadge = env.databaseEngine === "rds-postgres" ? chalk3.yellow("rds") : chalk3.dim("aurora");
|
|
627
|
+
console.log(
|
|
628
|
+
` ${chalk3.bold.cyan(env.stage.padEnd(16))}${env.region.padEnd(14)}${env.accountId.padEnd(16)}${dbBadge.padEnd(20)}${memBadge}`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
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"));
|
|
632
|
+
console.log(chalk3.dim(` ${envs.length} environment(s)`));
|
|
633
|
+
console.log("");
|
|
634
|
+
console.log(` Show details: ${chalk3.cyan("thinkwork config list -s <stage>")}`);
|
|
635
|
+
console.log("");
|
|
636
|
+
});
|
|
513
637
|
config.command("get <key>").description("Get a configuration value (e.g. memory-engine)").requiredOption("-s, --stage <name>", "Deployment stage").action((key, opts) => {
|
|
514
638
|
const stageCheck = validateStage(opts.stage);
|
|
515
639
|
if (!stageCheck.valid) {
|
|
516
640
|
printError(stageCheck.error);
|
|
517
641
|
process.exit(1);
|
|
518
642
|
}
|
|
519
|
-
const
|
|
520
|
-
const cwd = resolveTierDir(terraformDir, opts.stage, "app");
|
|
521
|
-
const tfvarsPath = `${cwd}/terraform.tfvars`;
|
|
643
|
+
const tfvarsPath = resolveTfvarsPath(opts.stage);
|
|
522
644
|
const tfKey = key.replace(/-/g, "_");
|
|
523
645
|
const value = readTfVar(tfvarsPath, tfKey);
|
|
524
646
|
if (value === null) {
|
|
@@ -527,7 +649,7 @@ function registerConfigCommand(program2) {
|
|
|
527
649
|
console.log(` ${key} = ${value}`);
|
|
528
650
|
}
|
|
529
651
|
});
|
|
530
|
-
config.command("set <key> <value>").description("Set a configuration value and optionally deploy
|
|
652
|
+
config.command("set <key> <value>").description("Set a configuration value and optionally deploy").requiredOption("-s, --stage <name>", "Deployment stage").option("--apply", "Run terraform apply after changing the value").action(async (key, value, opts) => {
|
|
531
653
|
const stageCheck = validateStage(opts.stage);
|
|
532
654
|
if (!stageCheck.valid) {
|
|
533
655
|
printError(stageCheck.error);
|
|
@@ -540,18 +662,21 @@ function registerConfigCommand(program2) {
|
|
|
540
662
|
}
|
|
541
663
|
const identity = getAwsIdentity();
|
|
542
664
|
printHeader("config set", opts.stage, identity);
|
|
543
|
-
const
|
|
544
|
-
const cwd = resolveTierDir(terraformDir, opts.stage, "app");
|
|
545
|
-
const tfvarsPath = `${cwd}/terraform.tfvars`;
|
|
665
|
+
const tfvarsPath = resolveTfvarsPath(opts.stage);
|
|
546
666
|
const oldValue = readTfVar(tfvarsPath, tfKey);
|
|
547
667
|
setTfVar(tfvarsPath, tfKey, value);
|
|
548
668
|
console.log(` ${key}: ${oldValue ?? "(unset)"} \u2192 ${value}`);
|
|
549
669
|
if (opts.apply) {
|
|
670
|
+
const tfDir = resolveTerraformDir(opts.stage);
|
|
671
|
+
if (!tfDir) {
|
|
672
|
+
printError("Cannot find terraform directory. Run `thinkwork init` first.");
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
550
675
|
console.log("");
|
|
551
676
|
console.log(" Applying configuration change...");
|
|
552
|
-
await ensureInit(
|
|
553
|
-
await ensureWorkspace(
|
|
554
|
-
const code = await runTerraform(
|
|
677
|
+
await ensureInit(tfDir);
|
|
678
|
+
await ensureWorkspace(tfDir, opts.stage);
|
|
679
|
+
const code = await runTerraform(tfDir, [
|
|
555
680
|
"apply",
|
|
556
681
|
"-auto-approve",
|
|
557
682
|
`-var=stage=${opts.stage}`
|
|
@@ -613,8 +738,8 @@ function registerBootstrapCommand(program2) {
|
|
|
613
738
|
bucket = await getTerraformOutput(cwd, "bucket_name");
|
|
614
739
|
dbEndpoint = await getTerraformOutput(cwd, "db_cluster_endpoint");
|
|
615
740
|
const secretArn = await getTerraformOutput(cwd, "db_secret_arn");
|
|
616
|
-
const { execSync:
|
|
617
|
-
const secretJson =
|
|
741
|
+
const { execSync: execSync6 } = await import("child_process");
|
|
742
|
+
const secretJson = execSync6(
|
|
618
743
|
`aws secretsmanager get-secret-value --secret-id "${secretArn}" --query SecretString --output text`,
|
|
619
744
|
{ encoding: "utf-8" }
|
|
620
745
|
).trim();
|
|
@@ -637,8 +762,131 @@ function registerBootstrapCommand(program2) {
|
|
|
637
762
|
}
|
|
638
763
|
|
|
639
764
|
// src/commands/login.ts
|
|
640
|
-
import { execSync as
|
|
765
|
+
import { execSync as execSync4 } from "child_process";
|
|
641
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
|
|
642
890
|
function ask(prompt) {
|
|
643
891
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
644
892
|
return new Promise((resolve3) => {
|
|
@@ -651,6 +899,10 @@ function ask(prompt) {
|
|
|
651
899
|
function registerLoginCommand(program2) {
|
|
652
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) => {
|
|
653
901
|
printHeader("login", opts.profile);
|
|
902
|
+
const awsOk = await ensureAwsCli();
|
|
903
|
+
if (!awsOk) {
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
654
906
|
const existing = getAwsIdentity();
|
|
655
907
|
if (existing) {
|
|
656
908
|
console.log(` Already authenticated:`);
|
|
@@ -668,7 +920,7 @@ function registerLoginCommand(program2) {
|
|
|
668
920
|
console.log(" Launching AWS SSO login...");
|
|
669
921
|
console.log("");
|
|
670
922
|
try {
|
|
671
|
-
|
|
923
|
+
execSync4(`aws sso login --profile ${opts.profile}`, {
|
|
672
924
|
stdio: "inherit"
|
|
673
925
|
});
|
|
674
926
|
process.env.AWS_PROFILE = opts.profile;
|
|
@@ -699,9 +951,9 @@ function registerLoginCommand(program2) {
|
|
|
699
951
|
const region = await ask(" Default region [us-east-1]: ");
|
|
700
952
|
const finalRegion = region || "us-east-1";
|
|
701
953
|
try {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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" });
|
|
705
957
|
} catch (err) {
|
|
706
958
|
printError(`Failed to save credentials: ${err}`);
|
|
707
959
|
process.exit(1);
|
|
@@ -721,16 +973,16 @@ function registerLoginCommand(program2) {
|
|
|
721
973
|
}
|
|
722
974
|
|
|
723
975
|
// src/commands/init.ts
|
|
724
|
-
import { existsSync as
|
|
725
|
-
import { resolve as resolve2, join, dirname } from "path";
|
|
726
|
-
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";
|
|
727
979
|
import { fileURLToPath } from "url";
|
|
728
980
|
import { createInterface as createInterface3 } from "readline";
|
|
729
|
-
import
|
|
981
|
+
import chalk5 from "chalk";
|
|
730
982
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
731
983
|
function ask2(prompt, defaultVal = "") {
|
|
732
984
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
733
|
-
const suffix = defaultVal ?
|
|
985
|
+
const suffix = defaultVal ? chalk5.dim(` [${defaultVal}]`) : "";
|
|
734
986
|
return new Promise((resolve3) => {
|
|
735
987
|
rl.question(` ${prompt}${suffix}: `, (answer) => {
|
|
736
988
|
rl.close();
|
|
@@ -739,7 +991,7 @@ function ask2(prompt, defaultVal = "") {
|
|
|
739
991
|
});
|
|
740
992
|
}
|
|
741
993
|
function choose(prompt, options, defaultVal) {
|
|
742
|
-
const optStr = options.map((o) => o === defaultVal ?
|
|
994
|
+
const optStr = options.map((o) => o === defaultVal ? chalk5.bold(o) : chalk5.dim(o)).join(" / ");
|
|
743
995
|
return ask2(`${prompt} (${optStr})`, defaultVal);
|
|
744
996
|
}
|
|
745
997
|
function generateSecret(length = 32) {
|
|
@@ -752,9 +1004,9 @@ function generateSecret(length = 32) {
|
|
|
752
1004
|
}
|
|
753
1005
|
function findBundledTerraform() {
|
|
754
1006
|
const bundled = resolve2(__dirname, "..", "terraform");
|
|
755
|
-
if (
|
|
1007
|
+
if (existsSync5(join3(bundled, "modules"))) return bundled;
|
|
756
1008
|
const repoTf = resolve2(__dirname, "..", "..", "..", "..", "terraform");
|
|
757
|
-
if (
|
|
1009
|
+
if (existsSync5(join3(repoTf, "modules"))) return repoTf;
|
|
758
1010
|
throw new Error(
|
|
759
1011
|
"Terraform modules not found. The CLI package may be incomplete.\nTry reinstalling: npm install -g thinkwork-cli@latest"
|
|
760
1012
|
);
|
|
@@ -813,14 +1065,18 @@ function registerInitCommand(program2) {
|
|
|
813
1065
|
}
|
|
814
1066
|
const identity = getAwsIdentity();
|
|
815
1067
|
printHeader("init", opts.stage, identity);
|
|
1068
|
+
const prereqsOk = await ensurePrerequisites();
|
|
1069
|
+
if (!prereqsOk) {
|
|
1070
|
+
process.exit(1);
|
|
1071
|
+
}
|
|
816
1072
|
if (!identity) {
|
|
817
1073
|
printError("AWS credentials not configured. Run `thinkwork login` first.");
|
|
818
1074
|
process.exit(1);
|
|
819
1075
|
}
|
|
820
1076
|
const targetDir = resolve2(opts.dir);
|
|
821
|
-
const tfDir =
|
|
822
|
-
const tfvarsPath =
|
|
823
|
-
if (
|
|
1077
|
+
const tfDir = join3(targetDir, "terraform");
|
|
1078
|
+
const tfvarsPath = join3(tfDir, "terraform.tfvars");
|
|
1079
|
+
if (existsSync5(tfvarsPath)) {
|
|
824
1080
|
printWarning(`terraform.tfvars already exists at ${tfvarsPath}`);
|
|
825
1081
|
const overwrite = await ask2("Overwrite?", "N");
|
|
826
1082
|
if (overwrite.toLowerCase() !== "y") {
|
|
@@ -844,19 +1100,19 @@ function registerInitCommand(program2) {
|
|
|
844
1100
|
config.admin_url = "http://localhost:5174";
|
|
845
1101
|
config.mobile_scheme = "thinkwork";
|
|
846
1102
|
} else {
|
|
847
|
-
console.log(
|
|
1103
|
+
console.log(chalk5.bold(" Configure your Thinkwork environment\n"));
|
|
848
1104
|
const defaultRegion = identity.region !== "unknown" ? identity.region : "us-east-1";
|
|
849
1105
|
config.region = await ask2("AWS Region", defaultRegion);
|
|
850
1106
|
console.log("");
|
|
851
|
-
console.log(
|
|
1107
|
+
console.log(chalk5.dim(" \u2500\u2500 Database \u2500\u2500"));
|
|
852
1108
|
config.database_engine = await choose("Database engine", ["aurora-serverless", "rds-postgres"], "aurora-serverless");
|
|
853
1109
|
console.log("");
|
|
854
|
-
console.log(
|
|
855
|
-
console.log(
|
|
856
|
-
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"));
|
|
857
1113
|
config.memory_engine = await choose("Memory engine", ["managed", "hindsight"], "managed");
|
|
858
1114
|
console.log("");
|
|
859
|
-
console.log(
|
|
1115
|
+
console.log(chalk5.dim(" \u2500\u2500 Auth \u2500\u2500"));
|
|
860
1116
|
const useGoogle = await ask2("Enable Google OAuth login? (y/N)", "N");
|
|
861
1117
|
if (useGoogle.toLowerCase() === "y") {
|
|
862
1118
|
config.google_oauth_client_id = await ask2("Google OAuth Client ID");
|
|
@@ -866,13 +1122,13 @@ function registerInitCommand(program2) {
|
|
|
866
1122
|
config.google_oauth_client_secret = "";
|
|
867
1123
|
}
|
|
868
1124
|
console.log("");
|
|
869
|
-
console.log(
|
|
1125
|
+
console.log(chalk5.dim(" \u2500\u2500 Frontend URLs \u2500\u2500"));
|
|
870
1126
|
config.admin_url = await ask2("Admin UI URL", "http://localhost:5174");
|
|
871
1127
|
config.mobile_scheme = await ask2("Mobile app URL scheme", "thinkwork");
|
|
872
1128
|
console.log("");
|
|
873
|
-
console.log(
|
|
874
|
-
console.log(
|
|
875
|
-
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)}...`));
|
|
876
1132
|
}
|
|
877
1133
|
console.log("");
|
|
878
1134
|
console.log(" Scaffolding Terraform modules...");
|
|
@@ -883,24 +1139,24 @@ function registerInitCommand(program2) {
|
|
|
883
1139
|
printError(String(err));
|
|
884
1140
|
process.exit(1);
|
|
885
1141
|
}
|
|
886
|
-
|
|
1142
|
+
mkdirSync3(tfDir, { recursive: true });
|
|
887
1143
|
const copyDirs = ["modules", "examples"];
|
|
888
1144
|
for (const dir of copyDirs) {
|
|
889
|
-
const src =
|
|
890
|
-
const dst =
|
|
891
|
-
if (
|
|
1145
|
+
const src = join3(bundledTf, dir);
|
|
1146
|
+
const dst = join3(tfDir, dir);
|
|
1147
|
+
if (existsSync5(src) && !existsSync5(dst)) {
|
|
892
1148
|
cpSync(src, dst, { recursive: true });
|
|
893
1149
|
}
|
|
894
1150
|
}
|
|
895
|
-
const schemaPath =
|
|
896
|
-
if (
|
|
897
|
-
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"));
|
|
898
1154
|
}
|
|
899
1155
|
const tfvars = buildTfvars(config);
|
|
900
|
-
|
|
901
|
-
const mainTfPath =
|
|
902
|
-
if (!
|
|
903
|
-
|
|
1156
|
+
writeFileSync3(tfvarsPath, tfvars);
|
|
1157
|
+
const mainTfPath = join3(tfDir, "main.tf");
|
|
1158
|
+
if (!existsSync5(mainTfPath)) {
|
|
1159
|
+
writeFileSync3(mainTfPath, `################################################################################
|
|
904
1160
|
# Thinkwork \u2014 ${config.stage}
|
|
905
1161
|
# Generated by: thinkwork init -s ${config.stage}
|
|
906
1162
|
################################################################################
|
|
@@ -977,31 +1233,42 @@ output "memory_engine" { value = module.thinkwork.memory_engine }
|
|
|
977
1233
|
output "hindsight_endpoint" { value = module.thinkwork.hindsight_endpoint }
|
|
978
1234
|
`);
|
|
979
1235
|
}
|
|
980
|
-
console.log(` Wrote ${
|
|
1236
|
+
console.log(` Wrote ${chalk5.cyan(tfDir + "/")}`);
|
|
981
1237
|
console.log("");
|
|
982
|
-
console.log(
|
|
983
|
-
console.log(` ${
|
|
984
|
-
console.log(` ${
|
|
985
|
-
console.log(` ${
|
|
986
|
-
console.log(` ${
|
|
987
|
-
console.log(` ${
|
|
988
|
-
console.log(` ${
|
|
989
|
-
console.log(` ${
|
|
990
|
-
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"));
|
|
991
1247
|
console.log("\n Initializing Terraform...\n");
|
|
992
1248
|
try {
|
|
993
|
-
|
|
1249
|
+
execSync5("terraform init", { cwd: tfDir, stdio: "inherit" });
|
|
994
1250
|
} catch {
|
|
995
1251
|
printWarning("Terraform init failed. Run `thinkwork doctor -s " + opts.stage + "` to check prerequisites.");
|
|
996
1252
|
return;
|
|
997
1253
|
}
|
|
1254
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1255
|
+
saveEnvironment({
|
|
1256
|
+
stage: config.stage,
|
|
1257
|
+
region: config.region,
|
|
1258
|
+
accountId: config.account_id,
|
|
1259
|
+
terraformDir: tfDir,
|
|
1260
|
+
databaseEngine: config.database_engine,
|
|
1261
|
+
memoryEngine: config.memory_engine,
|
|
1262
|
+
createdAt: now,
|
|
1263
|
+
updatedAt: now
|
|
1264
|
+
});
|
|
998
1265
|
printSuccess(`Environment "${opts.stage}" initialized`);
|
|
999
1266
|
console.log("");
|
|
1000
1267
|
console.log(" Next steps:");
|
|
1001
|
-
console.log(` ${
|
|
1002
|
-
console.log(` ${
|
|
1003
|
-
console.log(` ${
|
|
1004
|
-
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.")}`);
|
|
1005
1272
|
console.log("");
|
|
1006
1273
|
});
|
|
1007
1274
|
}
|