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.
- package/README.md +3 -3
- package/dist/cli.js +251 -47
- package/dist/terraform/examples/greenfield/main.tf +190 -0
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +28 -0
- package/dist/terraform/modules/_internal/workspace-guard/main.tf +29 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +217 -0
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +122 -0
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +20 -0
- package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +31 -0
- package/dist/terraform/modules/app/crons/main.tf +55 -0
- package/dist/terraform/modules/app/hindsight-memory/README.md +66 -0
- package/dist/terraform/modules/app/hindsight-memory/main.tf +331 -0
- package/dist/terraform/modules/app/job-triggers/main.tf +70 -0
- package/dist/terraform/modules/app/lambda-api/.build/placeholder.zip +0 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +311 -0
- package/dist/terraform/modules/app/lambda-api/main.tf +245 -0
- package/dist/terraform/modules/app/lambda-api/outputs.tf +24 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +153 -0
- package/dist/terraform/modules/app/ses-email/main.tf +51 -0
- package/dist/terraform/modules/app/static-site/main.tf +176 -0
- package/dist/terraform/modules/data/aurora-postgres/README.md +92 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +185 -0
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +30 -0
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +114 -0
- package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +102 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +91 -0
- package/dist/terraform/modules/foundation/cognito/main.tf +377 -0
- package/dist/terraform/modules/foundation/cognito/outputs.tf +29 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +124 -0
- package/dist/terraform/modules/foundation/dns/main.tf +49 -0
- package/dist/terraform/modules/foundation/kms/main.tf +49 -0
- package/dist/terraform/modules/foundation/vpc/main.tf +137 -0
- package/dist/terraform/modules/foundation/vpc/outputs.tf +14 -0
- package/dist/terraform/modules/foundation/vpc/variables.tf +40 -0
- package/dist/terraform/modules/thinkwork/main.tf +212 -0
- package/dist/terraform/modules/thinkwork/outputs.tf +87 -0
- package/dist/terraform/modules/thinkwork/variables.tf +241 -0
- package/dist/terraform/schema.graphql +199 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
#
|
|
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
|
|
8
|
+
npm install -g thinkwork-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Or run without installing:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx
|
|
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
|
-
|
|
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(
|
|
735
|
+
rl.question(` ${prompt}${suffix}: `, (answer) => {
|
|
721
736
|
rl.close();
|
|
722
737
|
resolve3(answer.trim() || defaultVal);
|
|
723
738
|
});
|
|
724
739
|
});
|
|
725
740
|
}
|
|
726
|
-
function
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
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)
|
|
732
|
-
return
|
|
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>", "
|
|
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
|
|
765
|
-
const tfDir = join(
|
|
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("
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
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
|
-
|
|
786
|
-
|
|
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
|
|
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
|
|
799
|
-
console.log(` 2. thinkwork deploy -s ${opts.stage} # Deploy
|
|
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
|
+
}
|