thinkwork-cli 0.1.0 → 0.2.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 (39) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +251 -47
  3. package/dist/terraform/examples/greenfield/main.tf +190 -0
  4. package/dist/terraform/examples/greenfield/terraform.tfvars.example +28 -0
  5. package/dist/terraform/modules/_internal/workspace-guard/main.tf +29 -0
  6. package/dist/terraform/modules/app/agentcore-runtime/main.tf +217 -0
  7. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +122 -0
  8. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +20 -0
  9. package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +31 -0
  10. package/dist/terraform/modules/app/crons/main.tf +55 -0
  11. package/dist/terraform/modules/app/hindsight-memory/README.md +66 -0
  12. package/dist/terraform/modules/app/hindsight-memory/main.tf +331 -0
  13. package/dist/terraform/modules/app/job-triggers/main.tf +70 -0
  14. package/dist/terraform/modules/app/lambda-api/.build/placeholder.zip +0 -0
  15. package/dist/terraform/modules/app/lambda-api/handlers.tf +311 -0
  16. package/dist/terraform/modules/app/lambda-api/main.tf +245 -0
  17. package/dist/terraform/modules/app/lambda-api/outputs.tf +24 -0
  18. package/dist/terraform/modules/app/lambda-api/variables.tf +153 -0
  19. package/dist/terraform/modules/app/ses-email/main.tf +51 -0
  20. package/dist/terraform/modules/app/static-site/main.tf +176 -0
  21. package/dist/terraform/modules/data/aurora-postgres/README.md +92 -0
  22. package/dist/terraform/modules/data/aurora-postgres/main.tf +185 -0
  23. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +30 -0
  24. package/dist/terraform/modules/data/aurora-postgres/variables.tf +114 -0
  25. package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +102 -0
  26. package/dist/terraform/modules/data/s3-buckets/main.tf +91 -0
  27. package/dist/terraform/modules/foundation/cognito/main.tf +377 -0
  28. package/dist/terraform/modules/foundation/cognito/outputs.tf +29 -0
  29. package/dist/terraform/modules/foundation/cognito/variables.tf +124 -0
  30. package/dist/terraform/modules/foundation/dns/main.tf +49 -0
  31. package/dist/terraform/modules/foundation/kms/main.tf +49 -0
  32. package/dist/terraform/modules/foundation/vpc/main.tf +137 -0
  33. package/dist/terraform/modules/foundation/vpc/outputs.tf +14 -0
  34. package/dist/terraform/modules/foundation/vpc/variables.tf +40 -0
  35. package/dist/terraform/modules/thinkwork/main.tf +212 -0
  36. package/dist/terraform/modules/thinkwork/outputs.tf +87 -0
  37. package/dist/terraform/modules/thinkwork/variables.tf +241 -0
  38. package/dist/terraform/schema.graphql +199 -0
  39. package/package.json +2 -2
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
- # @thinkwork-ai/cli
1
+ # thinkwork-cli
2
2
 
3
3
  Deploy and manage Thinkwork AI agent stacks on AWS.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install -g @thinkwork-ai/cli
8
+ npm install -g thinkwork-cli
9
9
  ```
10
10
 
11
11
  Or run without installing:
12
12
 
13
13
  ```bash
14
- npx @thinkwork-ai/cli --help
14
+ npx thinkwork-cli --help
15
15
  ```
16
16
 
17
17
  ## Quick Start
package/dist/cli.js CHANGED
@@ -83,7 +83,19 @@ function resolveTierDir(terraformDir, stage, tier) {
83
83
  if (existsSync(envDir)) {
84
84
  return envDir;
85
85
  }
86
- return path.join(terraformDir, "examples", "greenfield");
86
+ const greenfield = path.join(terraformDir, "examples", "greenfield");
87
+ if (existsSync(greenfield)) {
88
+ return greenfield;
89
+ }
90
+ const flat = path.join(terraformDir);
91
+ if (existsSync(path.join(flat, "main.tf"))) {
92
+ return flat;
93
+ }
94
+ const cwdTf = path.join(process.cwd(), "terraform");
95
+ if (existsSync(path.join(cwdTf, "main.tf"))) {
96
+ return cwdTf;
97
+ }
98
+ return terraformDir;
87
99
  }
88
100
  async function ensureWorkspace(cwd, stage) {
89
101
  const list = await runTerraformRaw(cwd, ["workspace", "list"]);
@@ -709,47 +721,91 @@ function registerLoginCommand(program2) {
709
721
  }
710
722
 
711
723
  // src/commands/init.ts
712
- import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
713
- import { resolve as resolve2, join } from "path";
724
+ import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync2, cpSync } from "fs";
725
+ import { resolve as resolve2, join, dirname } from "path";
714
726
  import { execSync as execSync4 } from "child_process";
727
+ import { fileURLToPath } from "url";
715
728
  import { createInterface as createInterface3 } from "readline";
729
+ import chalk3 from "chalk";
730
+ var __dirname = dirname(fileURLToPath(import.meta.url));
716
731
  function ask2(prompt, defaultVal = "") {
717
732
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
718
- const suffix = defaultVal ? ` [${defaultVal}]` : "";
733
+ const suffix = defaultVal ? chalk3.dim(` [${defaultVal}]`) : "";
719
734
  return new Promise((resolve3) => {
720
- rl.question(`${prompt}${suffix}: `, (answer) => {
735
+ rl.question(` ${prompt}${suffix}: `, (answer) => {
721
736
  rl.close();
722
737
  resolve3(answer.trim() || defaultVal);
723
738
  });
724
739
  });
725
740
  }
726
- function generatePassword() {
727
- const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%";
728
- let pw = "";
729
- const bytes = new Uint8Array(32);
741
+ function choose(prompt, options, defaultVal) {
742
+ const optStr = options.map((o) => o === defaultVal ? chalk3.bold(o) : chalk3.dim(o)).join(" / ");
743
+ return ask2(`${prompt} (${optStr})`, defaultVal);
744
+ }
745
+ function generateSecret(length = 32) {
746
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
747
+ let result = "";
748
+ const bytes = new Uint8Array(length);
730
749
  globalThis.crypto.getRandomValues(bytes);
731
- for (const b of bytes) pw += chars[b % chars.length];
732
- return pw;
750
+ for (const b of bytes) result += chars[b % chars.length];
751
+ return result;
752
+ }
753
+ function findBundledTerraform() {
754
+ const bundled = resolve2(__dirname, "..", "terraform");
755
+ if (existsSync3(join(bundled, "modules"))) return bundled;
756
+ const repoTf = resolve2(__dirname, "..", "..", "..", "..", "terraform");
757
+ if (existsSync3(join(repoTf, "modules"))) return repoTf;
758
+ throw new Error(
759
+ "Terraform modules not found. The CLI package may be incomplete.\nTry reinstalling: npm install -g thinkwork-cli@latest"
760
+ );
761
+ }
762
+ function buildTfvars(config) {
763
+ const lines = [
764
+ `# Thinkwork \u2014 ${config.stage} stage`,
765
+ `# Generated by: thinkwork init -s ${config.stage}`,
766
+ `# ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
767
+ ``,
768
+ `# \u2500\u2500 Core \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
769
+ `stage = "${config.stage}"`,
770
+ `region = "${config.region}"`,
771
+ `account_id = "${config.account_id}"`,
772
+ ``,
773
+ `# \u2500\u2500 Database \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
774
+ `database_engine = "${config.database_engine}"`,
775
+ `db_password = "${config.db_password}"`,
776
+ ``,
777
+ `# \u2500\u2500 Memory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
778
+ `memory_engine = "${config.memory_engine}"`,
779
+ ``,
780
+ `# \u2500\u2500 Auth \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
781
+ `api_auth_secret = "${config.api_auth_secret}"`
782
+ ];
783
+ if (config.google_oauth_client_id) {
784
+ lines.push(``);
785
+ lines.push(`# \u2500\u2500 Google OAuth \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
786
+ lines.push(`google_oauth_client_id = "${config.google_oauth_client_id}"`);
787
+ lines.push(`google_oauth_client_secret = "${config.google_oauth_client_secret}"`);
788
+ } else {
789
+ lines.push(``);
790
+ lines.push(`# \u2500\u2500 Google OAuth (uncomment to enable Google social login) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
791
+ lines.push(`# google_oauth_client_id = ""`);
792
+ lines.push(`# google_oauth_client_secret = ""`);
793
+ }
794
+ if (config.admin_url && config.admin_url !== "http://localhost:5174") {
795
+ lines.push(``);
796
+ lines.push(`# \u2500\u2500 Callback URLs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
797
+ lines.push(`admin_callback_urls = ["${config.admin_url}", "${config.admin_url}/auth/callback"]`);
798
+ lines.push(`admin_logout_urls = ["${config.admin_url}"]`);
799
+ }
800
+ if (config.mobile_scheme && config.mobile_scheme !== "thinkwork") {
801
+ lines.push(`mobile_callback_urls = ["${config.mobile_scheme}://", "${config.mobile_scheme}://auth/callback"]`);
802
+ lines.push(`mobile_logout_urls = ["${config.mobile_scheme}://"]`);
803
+ }
804
+ lines.push(``);
805
+ return lines.join("\n");
733
806
  }
734
- var TFVARS_TEMPLATE = `# Thinkwork \u2014 {STAGE} stage
735
- # Generated by: thinkwork init -s {STAGE}
736
-
737
- stage = "{STAGE}"
738
- region = "{REGION}"
739
- account_id = "{ACCOUNT_ID}"
740
-
741
- # Database
742
- database_engine = "aurora-serverless"
743
- db_password = "{DB_PASSWORD}"
744
-
745
- # Memory engine: "managed" (built-in) or "hindsight" (ECS Fargate service)
746
- memory_engine = "{MEMORY_ENGINE}"
747
-
748
- # API authentication secret (shared between services)
749
- api_auth_secret = "{API_SECRET}"
750
- `;
751
807
  function registerInitCommand(program2) {
752
- program2.command("init").description("Initialize a new Thinkwork environment").requiredOption("-s, --stage <name>", "Stage name (e.g. dev, staging, prod)").option("-d, --dir <path>", "Directory to initialize in", ".").action(async (opts) => {
808
+ program2.command("init").description("Initialize a new Thinkwork environment").requiredOption("-s, --stage <name>", "Stage name (e.g. dev, staging, prod)").option("-d, --dir <path>", "Target directory", ".").option("--defaults", "Skip interactive prompts, use all defaults").action(async (opts) => {
753
809
  const stageCheck = validateStage(opts.stage);
754
810
  if (!stageCheck.valid) {
755
811
  printError(stageCheck.error);
@@ -761,43 +817,191 @@ function registerInitCommand(program2) {
761
817
  printError("AWS credentials not configured. Run `thinkwork login` first.");
762
818
  process.exit(1);
763
819
  }
764
- const rootDir = resolve2(opts.dir);
765
- const tfDir = join(rootDir, "terraform", "examples", "greenfield");
820
+ const targetDir = resolve2(opts.dir);
821
+ const tfDir = join(targetDir, "terraform");
766
822
  const tfvarsPath = join(tfDir, "terraform.tfvars");
767
823
  if (existsSync3(tfvarsPath)) {
768
824
  printWarning(`terraform.tfvars already exists at ${tfvarsPath}`);
769
- const overwrite = await ask2(" Overwrite? [y/N]");
825
+ const overwrite = await ask2("Overwrite?", "N");
770
826
  if (overwrite.toLowerCase() !== "y") {
771
827
  console.log(" Aborted.");
772
828
  return;
773
829
  }
830
+ console.log("");
774
831
  }
775
- console.log(" Configure your Thinkwork environment:\n");
776
- const region = await ask2(" AWS Region", identity.region !== "unknown" ? identity.region : "us-east-1");
777
- const memoryEngine = await ask2(" Memory engine (managed/hindsight)", "managed");
778
- const dbPassword = generatePassword();
779
- const apiSecret = `tw-${opts.stage}-${generatePassword().slice(0, 12)}`;
780
- if (!existsSync3(tfDir)) {
781
- mkdirSync(tfDir, { recursive: true });
832
+ const config = {
833
+ stage: opts.stage,
834
+ account_id: identity.account,
835
+ db_password: generateSecret(24),
836
+ api_auth_secret: `tw-${opts.stage}-${generateSecret(16)}`
837
+ };
838
+ if (opts.defaults) {
839
+ config.region = identity.region !== "unknown" ? identity.region : "us-east-1";
840
+ config.database_engine = "aurora-serverless";
841
+ config.memory_engine = "managed";
842
+ config.google_oauth_client_id = "";
843
+ config.google_oauth_client_secret = "";
844
+ config.admin_url = "http://localhost:5174";
845
+ config.mobile_scheme = "thinkwork";
846
+ } else {
847
+ console.log(chalk3.bold(" Configure your Thinkwork environment\n"));
848
+ const defaultRegion = identity.region !== "unknown" ? identity.region : "us-east-1";
849
+ config.region = await ask2("AWS Region", defaultRegion);
850
+ console.log("");
851
+ console.log(chalk3.dim(" \u2500\u2500 Database \u2500\u2500"));
852
+ config.database_engine = await choose("Database engine", ["aurora-serverless", "rds-postgres"], "aurora-serverless");
853
+ 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"));
857
+ config.memory_engine = await choose("Memory engine", ["managed", "hindsight"], "managed");
858
+ console.log("");
859
+ console.log(chalk3.dim(" \u2500\u2500 Auth \u2500\u2500"));
860
+ const useGoogle = await ask2("Enable Google OAuth login? (y/N)", "N");
861
+ if (useGoogle.toLowerCase() === "y") {
862
+ config.google_oauth_client_id = await ask2("Google OAuth Client ID");
863
+ config.google_oauth_client_secret = await ask2("Google OAuth Client Secret");
864
+ } else {
865
+ config.google_oauth_client_id = "";
866
+ config.google_oauth_client_secret = "";
867
+ }
868
+ console.log("");
869
+ console.log(chalk3.dim(" \u2500\u2500 Frontend URLs \u2500\u2500"));
870
+ config.admin_url = await ask2("Admin UI URL", "http://localhost:5174");
871
+ config.mobile_scheme = await ask2("Mobile app URL scheme", "thinkwork");
872
+ 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)}...`));
782
876
  }
783
- const tfvars = TFVARS_TEMPLATE.replace(/{STAGE}/g, opts.stage).replace(/{REGION}/g, region).replace(/{ACCOUNT_ID}/g, identity.account).replace(/{DB_PASSWORD}/g, dbPassword).replace(/{MEMORY_ENGINE}/g, memoryEngine).replace(/{API_SECRET}/g, apiSecret);
877
+ console.log("");
878
+ console.log(" Scaffolding Terraform modules...");
879
+ let bundledTf;
880
+ try {
881
+ bundledTf = findBundledTerraform();
882
+ } catch (err) {
883
+ printError(String(err));
884
+ process.exit(1);
885
+ }
886
+ mkdirSync(tfDir, { recursive: true });
887
+ const copyDirs = ["modules", "examples"];
888
+ for (const dir of copyDirs) {
889
+ const src = join(bundledTf, dir);
890
+ const dst = join(tfDir, dir);
891
+ if (existsSync3(src) && !existsSync3(dst)) {
892
+ cpSync(src, dst, { recursive: true });
893
+ }
894
+ }
895
+ const schemaPath = join(bundledTf, "schema.graphql");
896
+ if (existsSync3(schemaPath) && !existsSync3(join(tfDir, "schema.graphql"))) {
897
+ cpSync(schemaPath, join(tfDir, "schema.graphql"));
898
+ }
899
+ const tfvars = buildTfvars(config);
784
900
  writeFileSync2(tfvarsPath, tfvars);
785
- console.log(`
786
- Wrote ${tfvarsPath}`);
901
+ const mainTfPath = join(tfDir, "main.tf");
902
+ if (!existsSync3(mainTfPath)) {
903
+ writeFileSync2(mainTfPath, `################################################################################
904
+ # Thinkwork \u2014 ${config.stage}
905
+ # Generated by: thinkwork init -s ${config.stage}
906
+ ################################################################################
907
+
908
+ terraform {
909
+ required_version = ">= 1.5"
910
+
911
+ required_providers {
912
+ aws = {
913
+ source = "hashicorp/aws"
914
+ version = "~> 5.0"
915
+ }
916
+ archive = {
917
+ source = "hashicorp/archive"
918
+ version = "~> 2.0"
919
+ }
920
+ null = {
921
+ source = "hashicorp/null"
922
+ version = "~> 3.0"
923
+ }
924
+ }
925
+ }
926
+
927
+ provider "aws" {
928
+ region = var.region
929
+ }
930
+
931
+ variable "stage" { type = string }
932
+ variable "region" { type = string; default = "us-east-1" }
933
+ variable "account_id" { type = string }
934
+ variable "db_password" { type = string; sensitive = true }
935
+ variable "database_engine" { type = string; default = "aurora-serverless" }
936
+ variable "memory_engine" { type = string; default = "managed" }
937
+ variable "google_oauth_client_id" { type = string; default = "" }
938
+ variable "google_oauth_client_secret" { type = string; sensitive = true; default = "" }
939
+ variable "pre_signup_lambda_zip" { type = string; default = "" }
940
+ variable "lambda_zips_dir" { type = string; default = "" }
941
+ variable "api_auth_secret" { type = string; sensitive = true; default = "" }
942
+ variable "admin_callback_urls" { type = list(string); default = ["http://localhost:5174", "http://localhost:5174/auth/callback"] }
943
+ variable "admin_logout_urls" { type = list(string); default = ["http://localhost:5174"] }
944
+ variable "mobile_callback_urls" { type = list(string); default = ["exp://localhost:8081", "thinkwork://", "thinkwork://auth/callback"] }
945
+ variable "mobile_logout_urls" { type = list(string); default = ["exp://localhost:8081", "thinkwork://"] }
946
+
947
+ module "thinkwork" {
948
+ source = "./modules/thinkwork"
949
+
950
+ stage = var.stage
951
+ region = var.region
952
+ account_id = var.account_id
953
+
954
+ db_password = var.db_password
955
+ database_engine = var.database_engine
956
+ memory_engine = var.memory_engine
957
+ google_oauth_client_id = var.google_oauth_client_id
958
+ google_oauth_client_secret = var.google_oauth_client_secret
959
+ pre_signup_lambda_zip = var.pre_signup_lambda_zip
960
+ lambda_zips_dir = var.lambda_zips_dir
961
+ api_auth_secret = var.api_auth_secret
962
+ admin_callback_urls = var.admin_callback_urls
963
+ admin_logout_urls = var.admin_logout_urls
964
+ mobile_callback_urls = var.mobile_callback_urls
965
+ mobile_logout_urls = var.mobile_logout_urls
966
+ }
967
+
968
+ output "api_endpoint" { value = module.thinkwork.api_endpoint }
969
+ output "user_pool_id" { value = module.thinkwork.user_pool_id }
970
+ output "admin_client_id" { value = module.thinkwork.admin_client_id }
971
+ output "mobile_client_id" { value = module.thinkwork.mobile_client_id }
972
+ output "bucket_name" { value = module.thinkwork.bucket_name }
973
+ output "db_cluster_endpoint" { value = module.thinkwork.db_cluster_endpoint }
974
+ output "db_secret_arn" { value = module.thinkwork.db_secret_arn; sensitive = true }
975
+ output "ecr_repository_url" { value = module.thinkwork.ecr_repository_url }
976
+ output "memory_engine" { value = module.thinkwork.memory_engine }
977
+ output "hindsight_endpoint" { value = module.thinkwork.hindsight_endpoint }
978
+ `);
979
+ }
980
+ console.log(` Wrote ${chalk3.cyan(tfDir + "/")}`);
981
+ 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"));
787
991
  console.log("\n Initializing Terraform...\n");
788
992
  try {
789
993
  execSync4("terraform init", { cwd: tfDir, stdio: "inherit" });
790
994
  } catch {
791
- printWarning("Terraform init failed \u2014 you may need to install Terraform first.");
792
- printWarning("Run: thinkwork doctor -s " + opts.stage);
995
+ printWarning("Terraform init failed. Run `thinkwork doctor -s " + opts.stage + "` to check prerequisites.");
793
996
  return;
794
997
  }
795
998
  printSuccess(`Environment "${opts.stage}" initialized`);
796
999
  console.log("");
797
1000
  console.log(" Next steps:");
798
- console.log(` 1. thinkwork plan -s ${opts.stage} # Review the plan`);
799
- console.log(` 2. thinkwork deploy -s ${opts.stage} # Deploy infrastructure`);
800
- console.log(` 3. thinkwork bootstrap -s ${opts.stage} # Seed workspace + skills`);
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.")}`);
801
1005
  console.log("");
802
1006
  });
803
1007
  }
@@ -0,0 +1,190 @@
1
+ ################################################################################
2
+ # Greenfield Example
3
+ #
4
+ # Creates everything from scratch in a fresh AWS account.
5
+ # Copy this directory to start a new Thinkwork deployment.
6
+ #
7
+ # Usage:
8
+ # cd terraform/examples/greenfield
9
+ # cp terraform.tfvars.example terraform.tfvars # edit with your values
10
+ # terraform init
11
+ # terraform workspace new dev # or your stage name
12
+ # terraform plan -var-file=terraform.tfvars
13
+ # terraform apply -var-file=terraform.tfvars
14
+ ################################################################################
15
+
16
+ terraform {
17
+ required_version = ">= 1.5"
18
+
19
+ required_providers {
20
+ aws = {
21
+ source = "hashicorp/aws"
22
+ version = "~> 5.0"
23
+ }
24
+ archive = {
25
+ source = "hashicorp/archive"
26
+ version = "~> 2.0"
27
+ }
28
+ null = {
29
+ source = "hashicorp/null"
30
+ version = "~> 3.0"
31
+ }
32
+ }
33
+
34
+ # For the example, use local state. Production deployments should
35
+ # use S3 + DynamoDB backend — see the docs for configuration.
36
+ # backend "s3" { ... }
37
+ }
38
+
39
+ provider "aws" {
40
+ region = var.region
41
+ }
42
+
43
+ variable "stage" {
44
+ description = "Deployment stage — must match the Terraform workspace name"
45
+ type = string
46
+ }
47
+
48
+ variable "region" {
49
+ description = "AWS region"
50
+ type = string
51
+ default = "us-east-1"
52
+ }
53
+
54
+ variable "account_id" {
55
+ description = "AWS account ID"
56
+ type = string
57
+ }
58
+
59
+ variable "db_password" {
60
+ description = "Master password for the Aurora cluster"
61
+ type = string
62
+ sensitive = true
63
+ }
64
+
65
+ variable "database_engine" {
66
+ description = "Database engine: 'aurora-serverless' (production) or 'rds-postgres' (dev/test, cheaper)"
67
+ type = string
68
+ default = "aurora-serverless"
69
+ }
70
+
71
+ variable "memory_engine" {
72
+ description = "Memory engine: 'managed' (AgentCore built-in, default) or 'hindsight' (ECS+ALB, opt-in)"
73
+ type = string
74
+ default = "managed"
75
+ }
76
+
77
+ variable "google_oauth_client_id" {
78
+ description = "Google OAuth client ID (optional — leave empty to skip Google login)"
79
+ type = string
80
+ default = ""
81
+ }
82
+
83
+ variable "google_oauth_client_secret" {
84
+ description = "Google OAuth client secret"
85
+ type = string
86
+ sensitive = true
87
+ default = ""
88
+ }
89
+
90
+ variable "pre_signup_lambda_zip" {
91
+ description = "Path to the Cognito pre-signup Lambda zip"
92
+ type = string
93
+ default = ""
94
+ }
95
+
96
+ variable "lambda_zips_dir" {
97
+ description = "Local directory containing Lambda zip artifacts (from pnpm build:lambdas)"
98
+ type = string
99
+ default = ""
100
+ }
101
+
102
+ variable "api_auth_secret" {
103
+ description = "Shared secret for inter-service API authentication"
104
+ type = string
105
+ sensitive = true
106
+ default = ""
107
+ }
108
+
109
+ module "thinkwork" {
110
+ source = "../../modules/thinkwork"
111
+
112
+ stage = var.stage
113
+ region = var.region
114
+ account_id = var.account_id
115
+
116
+ db_password = var.db_password
117
+ database_engine = var.database_engine
118
+ memory_engine = var.memory_engine
119
+ google_oauth_client_id = var.google_oauth_client_id
120
+ google_oauth_client_secret = var.google_oauth_client_secret
121
+ pre_signup_lambda_zip = var.pre_signup_lambda_zip
122
+ lambda_zips_dir = var.lambda_zips_dir
123
+ api_auth_secret = var.api_auth_secret
124
+
125
+ # Greenfield: create everything (all defaults are true)
126
+ }
127
+
128
+ ################################################################################
129
+ # Outputs
130
+ ################################################################################
131
+
132
+ output "api_endpoint" {
133
+ description = "API Gateway endpoint URL"
134
+ value = module.thinkwork.api_endpoint
135
+ }
136
+
137
+ output "appsync_realtime_url" {
138
+ description = "AppSync realtime WebSocket URL (for frontend subscription clients)"
139
+ value = module.thinkwork.appsync_realtime_url
140
+ }
141
+
142
+ output "user_pool_id" {
143
+ description = "Cognito user pool ID"
144
+ value = module.thinkwork.user_pool_id
145
+ }
146
+
147
+ output "admin_client_id" {
148
+ description = "Cognito app client ID for web admin"
149
+ value = module.thinkwork.admin_client_id
150
+ }
151
+
152
+ output "mobile_client_id" {
153
+ description = "Cognito app client ID for mobile"
154
+ value = module.thinkwork.mobile_client_id
155
+ }
156
+
157
+ output "ecr_repository_url" {
158
+ description = "ECR repository URL for the AgentCore container"
159
+ value = module.thinkwork.ecr_repository_url
160
+ }
161
+
162
+ output "bucket_name" {
163
+ description = "Primary S3 bucket"
164
+ value = module.thinkwork.bucket_name
165
+ }
166
+
167
+ output "db_cluster_endpoint" {
168
+ description = "Aurora cluster endpoint"
169
+ value = module.thinkwork.db_cluster_endpoint
170
+ }
171
+
172
+ output "db_secret_arn" {
173
+ description = "Secrets Manager ARN for database credentials"
174
+ value = module.thinkwork.db_secret_arn
175
+ }
176
+
177
+ output "database_name" {
178
+ description = "Database name"
179
+ value = module.thinkwork.database_name
180
+ }
181
+
182
+ output "memory_engine" {
183
+ description = "Active memory engine (managed or hindsight)"
184
+ value = module.thinkwork.memory_engine
185
+ }
186
+
187
+ output "hindsight_endpoint" {
188
+ description = "Hindsight API endpoint (null when memory_engine = managed)"
189
+ value = module.thinkwork.hindsight_endpoint
190
+ }
@@ -0,0 +1,28 @@
1
+ # Thinkwork Greenfield Deployment
2
+ #
3
+ # Copy this file to terraform.tfvars and fill in your values.
4
+ # DO NOT commit terraform.tfvars to version control.
5
+
6
+ stage = "dev"
7
+ region = "us-east-1"
8
+ account_id = "123456789012" # your AWS account ID
9
+
10
+ # Database engine:
11
+ # "aurora-serverless" — Aurora Serverless v2 (production, auto-scaling, deletion protection on)
12
+ # "rds-postgres" — Standard RDS PostgreSQL (dev/test, cheaper, deletion protection off)
13
+ database_engine = "rds-postgres"
14
+
15
+ # Memory engine:
16
+ # "managed" — AgentCore built-in long-term memory (default, no extra infra)
17
+ # "hindsight" — Hindsight ECS+ALB service (opt-in, adds retain/recall/reflect tools)
18
+ memory_engine = "managed"
19
+
20
+ # Database master password — use a strong password
21
+ db_password = "CHANGE_ME_strong_password_here"
22
+
23
+ # Google OAuth (optional — leave empty to skip Google social login)
24
+ # google_oauth_client_id = ""
25
+ # google_oauth_client_secret = ""
26
+
27
+ # Pre-signup Lambda (optional — leave empty if not using custom pre-signup logic)
28
+ # pre_signup_lambda_zip = "./lambdas/pre-signup.zip"
@@ -0,0 +1,29 @@
1
+ ################################################################################
2
+ # Workspace Guard
3
+ #
4
+ # Prevents applying the wrong var file to the wrong Terraform workspace.
5
+ # Every tier (foundation, data, app) should consume this module.
6
+ #
7
+ # History: on 2026-04-05, running `terraform apply -var-file=prod.tfvars` in
8
+ # the dev workspace destroyed dev infrastructure. This guard prevents that
9
+ # class of incident by failing the plan before any damage occurs.
10
+ ################################################################################
11
+
12
+ variable "stage" {
13
+ description = "The deployment stage (must match the Terraform workspace name)"
14
+ type = string
15
+ }
16
+
17
+ resource "null_resource" "workspace_guard" {
18
+ triggers = {
19
+ stage = var.stage
20
+ workspace = terraform.workspace
21
+ }
22
+
23
+ lifecycle {
24
+ precondition {
25
+ condition = var.stage == terraform.workspace
26
+ error_message = "SAFETY: stage '${var.stage}' does not match workspace '${terraform.workspace}'. You are applying the wrong var file!"
27
+ }
28
+ }
29
+ }