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.
Files changed (3) hide show
  1. package/README.md +42 -8
  2. package/dist/cli.js +337 -70
  3. 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 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
@@ -485,19 +485,67 @@ function registerOutputsCommand(program2) {
485
485
  }
486
486
 
487
487
  // src/commands/config.ts
488
- import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
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 (!existsSync2(tfvarsPath)) return null;
492
- const content = readFileSync(tfvarsPath, "utf-8");
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 (!existsSync2(tfvarsPath)) {
545
+ if (!existsSync3(tfvarsPath)) {
498
546
  throw new Error(`terraform.tfvars not found at ${tfvarsPath}`);
499
547
  }
500
- let content = readFileSync(tfvarsPath, "utf-8");
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
- writeFileSync(tfvarsPath, content);
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 terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
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 (e.g. config set memory-engine hindsight)").requiredOption("-s, --stage <name>", "Deployment stage").option("--apply", "Run terraform apply after changing the value").action(async (key, value, opts) => {
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 terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
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(cwd);
553
- await ensureWorkspace(cwd, opts.stage);
554
- const code = await runTerraform(cwd, [
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: execSync5 } = await import("child_process");
617
- const secretJson = execSync5(
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 execSync3 } from "child_process";
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
- execSync3(`aws sso login --profile ${opts.profile}`, {
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
- execSync3(`aws configure set aws_access_key_id "${accessKeyId}" --profile ${opts.profile}`, { stdio: "pipe" });
703
- execSync3(`aws configure set aws_secret_access_key "${secretAccessKey}" --profile ${opts.profile}`, { stdio: "pipe" });
704
- 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" });
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 existsSync3, mkdirSync, writeFileSync as writeFileSync2, cpSync } from "fs";
725
- import { resolve as resolve2, join, dirname } from "path";
726
- 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";
727
979
  import { fileURLToPath } from "url";
728
980
  import { createInterface as createInterface3 } from "readline";
729
- import chalk3 from "chalk";
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 ? chalk3.dim(` [${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 ? chalk3.bold(o) : chalk3.dim(o)).join(" / ");
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 (existsSync3(join(bundled, "modules"))) return bundled;
1007
+ if (existsSync5(join3(bundled, "modules"))) return bundled;
756
1008
  const repoTf = resolve2(__dirname, "..", "..", "..", "..", "terraform");
757
- if (existsSync3(join(repoTf, "modules"))) return repoTf;
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 = join(targetDir, "terraform");
822
- const tfvarsPath = join(tfDir, "terraform.tfvars");
823
- if (existsSync3(tfvarsPath)) {
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(chalk3.bold(" Configure your Thinkwork environment\n"));
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(chalk3.dim(" \u2500\u2500 Database \u2500\u2500"));
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(chalk3.dim(" \u2500\u2500 Memory \u2500\u2500"));
855
- console.log(chalk3.dim(" managed = Built-in AgentCore memory (remember/recall/forget)"));
856
- console.log(chalk3.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"));
857
1113
  config.memory_engine = await choose("Memory engine", ["managed", "hindsight"], "managed");
858
1114
  console.log("");
859
- console.log(chalk3.dim(" \u2500\u2500 Auth \u2500\u2500"));
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(chalk3.dim(" \u2500\u2500 Frontend URLs \u2500\u2500"));
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(chalk3.dim(" \u2500\u2500 Secrets (auto-generated) \u2500\u2500"));
874
- console.log(chalk3.dim(` DB password: ${config.db_password.slice(0, 8)}...`));
875
- console.log(chalk3.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)}...`));
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
- mkdirSync(tfDir, { recursive: true });
1142
+ mkdirSync3(tfDir, { recursive: true });
887
1143
  const copyDirs = ["modules", "examples"];
888
1144
  for (const dir of copyDirs) {
889
- const src = join(bundledTf, dir);
890
- const dst = join(tfDir, dir);
891
- if (existsSync3(src) && !existsSync3(dst)) {
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 = join(bundledTf, "schema.graphql");
896
- if (existsSync3(schemaPath) && !existsSync3(join(tfDir, "schema.graphql"))) {
897
- cpSync(schemaPath, join(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"));
898
1154
  }
899
1155
  const tfvars = buildTfvars(config);
900
- writeFileSync2(tfvarsPath, tfvars);
901
- const mainTfPath = join(tfDir, "main.tf");
902
- if (!existsSync3(mainTfPath)) {
903
- writeFileSync2(mainTfPath, `################################################################################
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 ${chalk3.cyan(tfDir + "/")}`);
1236
+ console.log(` Wrote ${chalk5.cyan(tfDir + "/")}`);
981
1237
  console.log("");
982
- 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"));
983
- console.log(` ${chalk3.bold("Stage:")} ${config.stage}`);
984
- console.log(` ${chalk3.bold("Region:")} ${config.region}`);
985
- console.log(` ${chalk3.bold("Account:")} ${config.account_id}`);
986
- console.log(` ${chalk3.bold("Database:")} ${config.database_engine}`);
987
- console.log(` ${chalk3.bold("Memory:")} ${config.memory_engine}`);
988
- console.log(` ${chalk3.bold("Google OAuth:")} ${config.google_oauth_client_id ? "enabled" : "disabled"}`);
989
- console.log(` ${chalk3.bold("Directory:")} ${tfDir}`);
990
- 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"));
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
- execSync4("terraform init", { cwd: tfDir, stdio: "inherit" });
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(` ${chalk3.cyan("1.")} thinkwork plan -s ${opts.stage} ${chalk3.dim("# Review infrastructure plan")}`);
1002
- console.log(` ${chalk3.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk3.dim("# Deploy to AWS (~5 min)")}`);
1003
- console.log(` ${chalk3.cyan("3.")} thinkwork bootstrap -s ${opts.stage} ${chalk3.dim("# Seed workspace files + skills")}`);
1004
- console.log(` ${chalk3.cyan("4.")} thinkwork outputs -s ${opts.stage} ${chalk3.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.")}`);
1005
1272
  console.log("");
1006
1273
  });
1007
1274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkwork-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Thinkwork CLI — deploy, manage, and interact with your Thinkwork stack",
5
5
  "license": "MIT",
6
6
  "type": "module",