thinkwork-cli 0.1.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 +126 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +828 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @thinkwork-ai/cli
|
|
2
|
+
|
|
3
|
+
Deploy and manage Thinkwork AI agent stacks on AWS.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @thinkwork-ai/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @thinkwork-ai/cli --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. Authenticate with AWS
|
|
21
|
+
thinkwork login
|
|
22
|
+
|
|
23
|
+
# 2. Check prerequisites
|
|
24
|
+
thinkwork doctor -s dev
|
|
25
|
+
|
|
26
|
+
# 3. Initialize a new environment
|
|
27
|
+
thinkwork init -s dev
|
|
28
|
+
|
|
29
|
+
# 4. Review the plan
|
|
30
|
+
thinkwork plan -s dev
|
|
31
|
+
|
|
32
|
+
# 5. Deploy
|
|
33
|
+
thinkwork deploy -s dev
|
|
34
|
+
|
|
35
|
+
# 6. Seed workspace files + skill catalog
|
|
36
|
+
thinkwork bootstrap -s dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
### Setup
|
|
42
|
+
|
|
43
|
+
| Command | Description |
|
|
44
|
+
|---------|-------------|
|
|
45
|
+
| `thinkwork login` | Configure AWS credentials (access keys or SSO) |
|
|
46
|
+
| `thinkwork init -s <stage>` | Initialize a new environment (generates tfvars, runs terraform init) |
|
|
47
|
+
| `thinkwork doctor -s <stage>` | Check prerequisites (AWS CLI, Terraform, credentials, Bedrock access) |
|
|
48
|
+
|
|
49
|
+
### Deploy
|
|
50
|
+
|
|
51
|
+
| Command | Description |
|
|
52
|
+
|---------|-------------|
|
|
53
|
+
| `thinkwork plan -s <stage>` | Preview infrastructure changes |
|
|
54
|
+
| `thinkwork deploy -s <stage>` | Deploy infrastructure (terraform apply) |
|
|
55
|
+
| `thinkwork bootstrap -s <stage>` | Seed workspace defaults, skill catalog, and per-tenant files |
|
|
56
|
+
| `thinkwork destroy -s <stage>` | Tear down infrastructure |
|
|
57
|
+
|
|
58
|
+
### Manage
|
|
59
|
+
|
|
60
|
+
| Command | Description |
|
|
61
|
+
|---------|-------------|
|
|
62
|
+
| `thinkwork outputs -s <stage>` | Show deployment outputs (API URL, Cognito IDs, etc.) |
|
|
63
|
+
| `thinkwork config get <key> -s <stage>` | Read a configuration value |
|
|
64
|
+
| `thinkwork config set <key> <value> -s <stage>` | Update a configuration value |
|
|
65
|
+
|
|
66
|
+
## Options
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
-s, --stage <name> Deployment stage (required for most commands)
|
|
70
|
+
-p, --profile <name> AWS profile to use
|
|
71
|
+
-c, --component <tier> Component tier: foundation, data, app, or all (default: all)
|
|
72
|
+
-y, --yes Skip confirmation prompts (for CI)
|
|
73
|
+
-v, --version Print CLI version
|
|
74
|
+
-h, --help Show help
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Examples
|
|
78
|
+
|
|
79
|
+
### Switch memory engine
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Switch from managed to Hindsight memory
|
|
83
|
+
thinkwork config set memory-engine hindsight -s dev --apply
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Deploy a specific tier
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Only deploy the app tier (Lambda functions, API Gateway)
|
|
90
|
+
thinkwork deploy -s dev -c app
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Use with AWS SSO
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
thinkwork login --sso --profile my-org
|
|
97
|
+
thinkwork deploy -s dev --profile my-org
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### CI/CD (non-interactive)
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
thinkwork deploy -s prod -y
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Prerequisites
|
|
107
|
+
|
|
108
|
+
- Node.js >= 20
|
|
109
|
+
- [AWS CLI](https://aws.amazon.com/cli/) v2
|
|
110
|
+
- [Terraform](https://developer.hashicorp.com/terraform/install) >= 1.5
|
|
111
|
+
- AWS account with Bedrock model access enabled
|
|
112
|
+
|
|
113
|
+
## What Gets Deployed
|
|
114
|
+
|
|
115
|
+
Thinkwork provisions a complete AI agent stack:
|
|
116
|
+
|
|
117
|
+
- **Compute**: Lambda functions (39 handlers), AgentCore container (Lambda + ECR)
|
|
118
|
+
- **Database**: Aurora Serverless PostgreSQL with pgvector
|
|
119
|
+
- **Auth**: Cognito user pool (admin + mobile clients)
|
|
120
|
+
- **API**: API Gateway (REST + GraphQL), AppSync (WebSocket subscriptions)
|
|
121
|
+
- **Storage**: S3 (workspace files, skills, knowledge bases)
|
|
122
|
+
- **Memory**: Managed (built-in) or Hindsight (ECS Fargate, opt-in)
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/version.ts
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
var require2 = createRequire(import.meta.url);
|
|
9
|
+
var pkg = require2("../package.json");
|
|
10
|
+
var VERSION = pkg.version;
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
var VALID_COMPONENTS = ["foundation", "data", "app", "all"];
|
|
14
|
+
var PROD_LIKE_STAGES = ["main", "prod", "production", "staging"];
|
|
15
|
+
function validateStage(stage) {
|
|
16
|
+
if (!stage) {
|
|
17
|
+
return { valid: false, error: "Stage name is required." };
|
|
18
|
+
}
|
|
19
|
+
if (!/^[a-z][a-z0-9-]{1,29}$/.test(stage)) {
|
|
20
|
+
return {
|
|
21
|
+
valid: false,
|
|
22
|
+
error: `Invalid stage name "${stage}". Must be lowercase alphanumeric + hyphens, 2-30 characters, starting with a letter.`
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { valid: true };
|
|
26
|
+
}
|
|
27
|
+
function validateComponent(component) {
|
|
28
|
+
if (!VALID_COMPONENTS.includes(component)) {
|
|
29
|
+
return {
|
|
30
|
+
valid: false,
|
|
31
|
+
error: `Invalid component "${component}". Must be one of: ${VALID_COMPONENTS.join(", ")}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { valid: true };
|
|
35
|
+
}
|
|
36
|
+
function isProdLike(stage) {
|
|
37
|
+
return PROD_LIKE_STAGES.includes(stage);
|
|
38
|
+
}
|
|
39
|
+
function expandComponent(component) {
|
|
40
|
+
if (component === "all") {
|
|
41
|
+
return ["foundation", "data", "app"];
|
|
42
|
+
}
|
|
43
|
+
return [component];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/aws.ts
|
|
47
|
+
import { execSync } from "child_process";
|
|
48
|
+
function getAwsIdentity() {
|
|
49
|
+
try {
|
|
50
|
+
const raw = execSync("aws sts get-caller-identity --output json", {
|
|
51
|
+
encoding: "utf-8",
|
|
52
|
+
timeout: 1e4,
|
|
53
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
54
|
+
});
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
let region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "unknown";
|
|
57
|
+
if (region === "unknown") {
|
|
58
|
+
try {
|
|
59
|
+
region = execSync("aws configure get region", {
|
|
60
|
+
encoding: "utf-8",
|
|
61
|
+
timeout: 5e3,
|
|
62
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
63
|
+
}).trim() || "unknown";
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
account: parsed.Account,
|
|
69
|
+
region,
|
|
70
|
+
arn: parsed.Arn
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/terraform.ts
|
|
78
|
+
import { spawn } from "child_process";
|
|
79
|
+
import { existsSync } from "fs";
|
|
80
|
+
import path from "path";
|
|
81
|
+
function resolveTierDir(terraformDir, stage, tier) {
|
|
82
|
+
const envDir = path.join(terraformDir, "environments", stage, tier);
|
|
83
|
+
if (existsSync(envDir)) {
|
|
84
|
+
return envDir;
|
|
85
|
+
}
|
|
86
|
+
return path.join(terraformDir, "examples", "greenfield");
|
|
87
|
+
}
|
|
88
|
+
async function ensureWorkspace(cwd, stage) {
|
|
89
|
+
const list = await runTerraformRaw(cwd, ["workspace", "list"]);
|
|
90
|
+
const workspaces = list.split("\n").map((l) => l.replace("*", "").trim()).filter(Boolean);
|
|
91
|
+
if (!workspaces.includes(stage)) {
|
|
92
|
+
await runTerraformRaw(cwd, ["workspace", "new", stage]);
|
|
93
|
+
} else {
|
|
94
|
+
await runTerraformRaw(cwd, ["workspace", "select", stage]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function runTerraformRaw(cwd, args) {
|
|
98
|
+
return new Promise((resolve3, reject) => {
|
|
99
|
+
const proc = spawn("terraform", args, {
|
|
100
|
+
cwd,
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
102
|
+
});
|
|
103
|
+
let stdout = "";
|
|
104
|
+
let stderr = "";
|
|
105
|
+
proc.stdout.on("data", (d) => stdout += d);
|
|
106
|
+
proc.stderr.on("data", (d) => stderr += d);
|
|
107
|
+
proc.on("close", (code) => {
|
|
108
|
+
if (code === 0) resolve3(stdout);
|
|
109
|
+
else reject(new Error(`terraform ${args.join(" ")} failed (exit ${code}): ${stderr}`));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function runTerraform(cwd, args) {
|
|
114
|
+
return new Promise((resolve3) => {
|
|
115
|
+
console.log(`
|
|
116
|
+
\u2192 terraform ${args.join(" ")}
|
|
117
|
+
`);
|
|
118
|
+
const proc = spawn("terraform", args, {
|
|
119
|
+
cwd,
|
|
120
|
+
stdio: "inherit"
|
|
121
|
+
});
|
|
122
|
+
proc.on("close", (code) => resolve3(code ?? 1));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function ensureInit(cwd) {
|
|
126
|
+
const dotTerraform = path.join(cwd, ".terraform");
|
|
127
|
+
if (!existsSync(dotTerraform)) {
|
|
128
|
+
const code = await runTerraform(cwd, ["init"]);
|
|
129
|
+
if (code !== 0) {
|
|
130
|
+
throw new Error("terraform init failed");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/ui.ts
|
|
136
|
+
import chalk from "chalk";
|
|
137
|
+
import ora from "ora";
|
|
138
|
+
var TIER_LABELS = {
|
|
139
|
+
foundation: "Foundation",
|
|
140
|
+
data: "Data",
|
|
141
|
+
app: "App"
|
|
142
|
+
};
|
|
143
|
+
function printHeader(command, stage, identity) {
|
|
144
|
+
console.log("");
|
|
145
|
+
console.log(chalk.bold.cyan(" \u2B21 Thinkwork") + chalk.dim(` \u2014 ${command}`));
|
|
146
|
+
console.log(chalk.dim(` Stage: ${chalk.white(stage)}`));
|
|
147
|
+
if (identity) {
|
|
148
|
+
console.log(chalk.dim(` AWS: ${chalk.white(identity.account)} / ${chalk.white(identity.region)}`));
|
|
149
|
+
}
|
|
150
|
+
console.log("");
|
|
151
|
+
}
|
|
152
|
+
function printTierHeader(tier, index, total) {
|
|
153
|
+
const label = TIER_LABELS[tier] ?? tier;
|
|
154
|
+
const progress = chalk.dim(`[${index + 1}/${total}]`);
|
|
155
|
+
console.log(` ${progress} ${chalk.bold(label)}`);
|
|
156
|
+
}
|
|
157
|
+
function printSuccess(message) {
|
|
158
|
+
console.log(`
|
|
159
|
+
${chalk.green("\u2713")} ${chalk.bold(message)}`);
|
|
160
|
+
}
|
|
161
|
+
function printError(message) {
|
|
162
|
+
console.log(`
|
|
163
|
+
${chalk.red("\u2717")} ${chalk.bold.red(message)}`);
|
|
164
|
+
}
|
|
165
|
+
function printWarning(message) {
|
|
166
|
+
console.log(` ${chalk.yellow("\u26A0")} ${message}`);
|
|
167
|
+
}
|
|
168
|
+
function printSummary(command, stage, tiers, startTime) {
|
|
169
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
170
|
+
console.log("");
|
|
171
|
+
console.log(chalk.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"));
|
|
172
|
+
console.log(` ${chalk.bold("Command:")} ${command}`);
|
|
173
|
+
console.log(` ${chalk.bold("Stage:")} ${stage}`);
|
|
174
|
+
console.log(` ${chalk.bold("Tiers:")} ${tiers.map((t) => TIER_LABELS[t] ?? t).join(" \u2192 ")}`);
|
|
175
|
+
console.log(` ${chalk.bold("Time:")} ${elapsed}s`);
|
|
176
|
+
console.log(chalk.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"));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/commands/plan.ts
|
|
180
|
+
function registerPlanCommand(program2) {
|
|
181
|
+
program2.command("plan").description("Run terraform plan for a stage").option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").action(async (opts) => {
|
|
182
|
+
const startTime = Date.now();
|
|
183
|
+
const stageCheck = validateStage(opts.stage);
|
|
184
|
+
if (!stageCheck.valid) {
|
|
185
|
+
printError(stageCheck.error);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
const compCheck = validateComponent(opts.component);
|
|
189
|
+
if (!compCheck.valid) {
|
|
190
|
+
printError(compCheck.error);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
const identity = getAwsIdentity();
|
|
194
|
+
printHeader("plan", opts.stage, identity);
|
|
195
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
196
|
+
const tiers = expandComponent(opts.component);
|
|
197
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
198
|
+
const tier = tiers[i];
|
|
199
|
+
printTierHeader(tier, i, tiers.length);
|
|
200
|
+
const cwd = resolveTierDir(terraformDir, opts.stage, tier);
|
|
201
|
+
await ensureInit(cwd);
|
|
202
|
+
await ensureWorkspace(cwd, opts.stage);
|
|
203
|
+
const code = await runTerraform(cwd, [
|
|
204
|
+
"plan",
|
|
205
|
+
`-var=stage=${opts.stage}`
|
|
206
|
+
]);
|
|
207
|
+
if (code !== 0) {
|
|
208
|
+
printError(`Plan failed for ${tier} (exit ${code})`);
|
|
209
|
+
process.exit(code);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
printSuccess("Plan complete");
|
|
213
|
+
printSummary("plan", opts.stage, tiers, startTime);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/prompt.ts
|
|
218
|
+
import { createInterface } from "readline";
|
|
219
|
+
async function confirm(message) {
|
|
220
|
+
const rl = createInterface({
|
|
221
|
+
input: process.stdin,
|
|
222
|
+
output: process.stdout
|
|
223
|
+
});
|
|
224
|
+
return new Promise((resolve3) => {
|
|
225
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
226
|
+
rl.close();
|
|
227
|
+
resolve3(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/commands/deploy.ts
|
|
233
|
+
function registerDeployCommand(program2) {
|
|
234
|
+
program2.command("deploy").description("Run terraform apply for a stage").option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").option("-y, --yes", "Skip interactive confirmation (for CI)").action(async (opts) => {
|
|
235
|
+
const startTime = Date.now();
|
|
236
|
+
const stageCheck = validateStage(opts.stage);
|
|
237
|
+
if (!stageCheck.valid) {
|
|
238
|
+
printError(stageCheck.error);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
const compCheck = validateComponent(opts.component);
|
|
242
|
+
if (!compCheck.valid) {
|
|
243
|
+
printError(compCheck.error);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
const identity = getAwsIdentity();
|
|
247
|
+
printHeader("deploy", opts.stage, identity);
|
|
248
|
+
if (!identity) {
|
|
249
|
+
printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
|
|
250
|
+
}
|
|
251
|
+
if (isProdLike(opts.stage) && !opts.yes) {
|
|
252
|
+
const ok = await confirm(
|
|
253
|
+
` Stage "${opts.stage}" is production-like. Deploy?`
|
|
254
|
+
);
|
|
255
|
+
if (!ok) {
|
|
256
|
+
console.log(" Aborted.");
|
|
257
|
+
process.exit(0);
|
|
258
|
+
}
|
|
259
|
+
} else if (!opts.yes) {
|
|
260
|
+
const ok = await confirm(` Deploy to stage "${opts.stage}"?`);
|
|
261
|
+
if (!ok) {
|
|
262
|
+
console.log(" Aborted.");
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
267
|
+
const tiers = expandComponent(opts.component);
|
|
268
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
269
|
+
const tier = tiers[i];
|
|
270
|
+
printTierHeader(tier, i, tiers.length);
|
|
271
|
+
const cwd = resolveTierDir(terraformDir, opts.stage, tier);
|
|
272
|
+
await ensureInit(cwd);
|
|
273
|
+
await ensureWorkspace(cwd, opts.stage);
|
|
274
|
+
const code = await runTerraform(cwd, [
|
|
275
|
+
"apply",
|
|
276
|
+
"-auto-approve",
|
|
277
|
+
`-var=stage=${opts.stage}`
|
|
278
|
+
]);
|
|
279
|
+
if (code !== 0) {
|
|
280
|
+
printError(`Deploy failed for ${tier} (exit ${code})`);
|
|
281
|
+
process.exit(code);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
printSuccess("Deploy complete");
|
|
285
|
+
printSummary("deploy", opts.stage, tiers, startTime);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/commands/destroy.ts
|
|
290
|
+
function registerDestroyCommand(program2) {
|
|
291
|
+
program2.command("destroy").description("Run terraform destroy for a stage").option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").option("-y, --yes", "Skip interactive confirmation (for CI)").action(async (opts) => {
|
|
292
|
+
const startTime = Date.now();
|
|
293
|
+
const stageCheck = validateStage(opts.stage);
|
|
294
|
+
if (!stageCheck.valid) {
|
|
295
|
+
printError(stageCheck.error);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
const compCheck = validateComponent(opts.component);
|
|
299
|
+
if (!compCheck.valid) {
|
|
300
|
+
printError(compCheck.error);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
const identity = getAwsIdentity();
|
|
304
|
+
printHeader("destroy", opts.stage, identity);
|
|
305
|
+
if (!identity) {
|
|
306
|
+
printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
|
|
307
|
+
}
|
|
308
|
+
if (isProdLike(opts.stage)) {
|
|
309
|
+
printWarning(`Stage "${opts.stage}" is production-like.`);
|
|
310
|
+
if (!opts.yes) {
|
|
311
|
+
const ok = await confirm(
|
|
312
|
+
` Type 'y' to confirm destruction of stage "${opts.stage}":`
|
|
313
|
+
);
|
|
314
|
+
if (!ok) {
|
|
315
|
+
console.log(" Aborted.");
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
console.log(` Proceeding with destroy of "${opts.stage}" (--yes provided).`);
|
|
320
|
+
} else if (!opts.yes) {
|
|
321
|
+
const ok = await confirm(` Destroy stage "${opts.stage}"?`);
|
|
322
|
+
if (!ok) {
|
|
323
|
+
console.log(" Aborted.");
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
328
|
+
const tiers = expandComponent(opts.component).reverse();
|
|
329
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
330
|
+
const tier = tiers[i];
|
|
331
|
+
printTierHeader(tier, i, tiers.length);
|
|
332
|
+
const cwd = resolveTierDir(terraformDir, opts.stage, tier);
|
|
333
|
+
await ensureInit(cwd);
|
|
334
|
+
await ensureWorkspace(cwd, opts.stage);
|
|
335
|
+
const code = await runTerraform(cwd, [
|
|
336
|
+
"destroy",
|
|
337
|
+
"-auto-approve",
|
|
338
|
+
`-var=stage=${opts.stage}`
|
|
339
|
+
]);
|
|
340
|
+
if (code !== 0) {
|
|
341
|
+
printError(`Destroy failed for ${tier} (exit ${code})`);
|
|
342
|
+
process.exit(code);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
printSuccess("Destroy complete");
|
|
346
|
+
printSummary("destroy", opts.stage, tiers, startTime);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/commands/doctor.ts
|
|
351
|
+
import chalk2 from "chalk";
|
|
352
|
+
import { execSync as execSync2 } from "child_process";
|
|
353
|
+
function checkAwsCli() {
|
|
354
|
+
return {
|
|
355
|
+
name: "AWS CLI installed",
|
|
356
|
+
run: () => {
|
|
357
|
+
try {
|
|
358
|
+
const v = execSync2("aws --version", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
359
|
+
return { pass: true, detail: v.split(" ")[0] ?? v };
|
|
360
|
+
} catch {
|
|
361
|
+
return { pass: false, detail: "aws CLI not found. Install: https://aws.amazon.com/cli/" };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function checkTerraformCli() {
|
|
367
|
+
return {
|
|
368
|
+
name: "Terraform CLI installed",
|
|
369
|
+
run: () => {
|
|
370
|
+
try {
|
|
371
|
+
const v = execSync2("terraform version -json", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] });
|
|
372
|
+
const parsed = JSON.parse(v);
|
|
373
|
+
return { pass: true, detail: `v${parsed.terraform_version}` };
|
|
374
|
+
} catch {
|
|
375
|
+
return { pass: false, detail: "terraform CLI not found. Install: https://developer.hashicorp.com/terraform/install" };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function checkAwsIdentity() {
|
|
381
|
+
return {
|
|
382
|
+
name: "AWS credentials configured",
|
|
383
|
+
run: () => {
|
|
384
|
+
const identity = getAwsIdentity();
|
|
385
|
+
if (identity) {
|
|
386
|
+
return { pass: true, detail: `account=${identity.account} region=${identity.region}` };
|
|
387
|
+
}
|
|
388
|
+
return { pass: false, detail: "Could not resolve AWS identity. Run `aws configure` or set AWS_PROFILE." };
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function checkBedrockAccess() {
|
|
393
|
+
return {
|
|
394
|
+
name: "Bedrock model access",
|
|
395
|
+
run: () => {
|
|
396
|
+
try {
|
|
397
|
+
execSync2(
|
|
398
|
+
"aws bedrock get-foundation-model --model-identifier anthropic.claude-3-haiku-20240307-v1:0 --output json --region us-east-1",
|
|
399
|
+
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
400
|
+
);
|
|
401
|
+
return { pass: true, detail: "anthropic.claude-3-haiku accessible" };
|
|
402
|
+
} catch {
|
|
403
|
+
return {
|
|
404
|
+
pass: false,
|
|
405
|
+
detail: "Bedrock model access not confirmed. You may need to request model access in the AWS console."
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function registerDoctorCommand(program2) {
|
|
412
|
+
program2.command("doctor").description("Check AWS account prerequisites for a Thinkwork deployment").option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").action((opts) => {
|
|
413
|
+
const stageCheck = validateStage(opts.stage);
|
|
414
|
+
if (!stageCheck.valid) {
|
|
415
|
+
printError(stageCheck.error);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
printHeader("doctor", opts.stage);
|
|
419
|
+
const checks = [
|
|
420
|
+
checkAwsCli(),
|
|
421
|
+
checkTerraformCli(),
|
|
422
|
+
checkAwsIdentity(),
|
|
423
|
+
checkBedrockAccess()
|
|
424
|
+
];
|
|
425
|
+
let allPass = true;
|
|
426
|
+
for (const check of checks) {
|
|
427
|
+
const result = check.run();
|
|
428
|
+
const icon = result.pass ? chalk2.green("\u2713") : chalk2.red("\u2717");
|
|
429
|
+
const detail = result.pass ? chalk2.dim(result.detail) : chalk2.yellow(result.detail);
|
|
430
|
+
console.log(` ${icon} ${check.name} ${detail}`);
|
|
431
|
+
if (!result.pass) allPass = false;
|
|
432
|
+
}
|
|
433
|
+
if (allPass) {
|
|
434
|
+
console.log(`
|
|
435
|
+
${chalk2.green.bold("All checks passed.")}`);
|
|
436
|
+
} else {
|
|
437
|
+
console.log(`
|
|
438
|
+
${chalk2.yellow.bold("Some checks failed.")} Fix the issues above before deploying.`);
|
|
439
|
+
}
|
|
440
|
+
process.exit(allPass ? 0 : 1);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/commands/outputs.ts
|
|
445
|
+
function registerOutputsCommand(program2) {
|
|
446
|
+
program2.command("outputs").description("Show terraform outputs for a stage").option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").action(async (opts) => {
|
|
447
|
+
const stageCheck = validateStage(opts.stage);
|
|
448
|
+
if (!stageCheck.valid) {
|
|
449
|
+
printError(stageCheck.error);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
const compCheck = validateComponent(opts.component);
|
|
453
|
+
if (!compCheck.valid) {
|
|
454
|
+
printError(compCheck.error);
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
printHeader("outputs", opts.stage);
|
|
458
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
459
|
+
const tiers = expandComponent(opts.component);
|
|
460
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
461
|
+
const tier = tiers[i];
|
|
462
|
+
printTierHeader(tier, i, tiers.length);
|
|
463
|
+
const cwd = resolveTierDir(terraformDir, opts.stage, tier);
|
|
464
|
+
await ensureInit(cwd);
|
|
465
|
+
await ensureWorkspace(cwd, opts.stage);
|
|
466
|
+
const code = await runTerraform(cwd, ["output"]);
|
|
467
|
+
if (code !== 0) {
|
|
468
|
+
printError(`Outputs failed for ${tier} (exit ${code})`);
|
|
469
|
+
process.exit(code);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/commands/config.ts
|
|
476
|
+
import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
477
|
+
var VALID_MEMORY_ENGINES = ["managed", "hindsight"];
|
|
478
|
+
function readTfVar(tfvarsPath, key) {
|
|
479
|
+
if (!existsSync2(tfvarsPath)) return null;
|
|
480
|
+
const content = readFileSync(tfvarsPath, "utf-8");
|
|
481
|
+
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
|
|
482
|
+
return match ? match[1] : null;
|
|
483
|
+
}
|
|
484
|
+
function setTfVar(tfvarsPath, key, value) {
|
|
485
|
+
if (!existsSync2(tfvarsPath)) {
|
|
486
|
+
throw new Error(`terraform.tfvars not found at ${tfvarsPath}`);
|
|
487
|
+
}
|
|
488
|
+
let content = readFileSync(tfvarsPath, "utf-8");
|
|
489
|
+
const regex = new RegExp(`^(${key}\\s*=\\s*)"[^"]*"`, "m");
|
|
490
|
+
if (regex.test(content)) {
|
|
491
|
+
content = content.replace(regex, `$1"${value}"`);
|
|
492
|
+
} else {
|
|
493
|
+
content += `
|
|
494
|
+
${key} = "${value}"
|
|
495
|
+
`;
|
|
496
|
+
}
|
|
497
|
+
writeFileSync(tfvarsPath, content);
|
|
498
|
+
}
|
|
499
|
+
function registerConfigCommand(program2) {
|
|
500
|
+
const config = program2.command("config").description("View or change stack configuration");
|
|
501
|
+
config.command("get <key>").description("Get a configuration value (e.g. memory-engine)").requiredOption("-s, --stage <name>", "Deployment stage").action((key, opts) => {
|
|
502
|
+
const stageCheck = validateStage(opts.stage);
|
|
503
|
+
if (!stageCheck.valid) {
|
|
504
|
+
printError(stageCheck.error);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
508
|
+
const cwd = resolveTierDir(terraformDir, opts.stage, "app");
|
|
509
|
+
const tfvarsPath = `${cwd}/terraform.tfvars`;
|
|
510
|
+
const tfKey = key.replace(/-/g, "_");
|
|
511
|
+
const value = readTfVar(tfvarsPath, tfKey);
|
|
512
|
+
if (value === null) {
|
|
513
|
+
printWarning(`${key} is not set in ${tfvarsPath}`);
|
|
514
|
+
} else {
|
|
515
|
+
console.log(` ${key} = ${value}`);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
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) => {
|
|
519
|
+
const stageCheck = validateStage(opts.stage);
|
|
520
|
+
if (!stageCheck.valid) {
|
|
521
|
+
printError(stageCheck.error);
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
const tfKey = key.replace(/-/g, "_");
|
|
525
|
+
if (tfKey === "memory_engine" && !VALID_MEMORY_ENGINES.includes(value)) {
|
|
526
|
+
printError(`Invalid memory engine "${value}". Must be: ${VALID_MEMORY_ENGINES.join(", ")}`);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
const identity = getAwsIdentity();
|
|
530
|
+
printHeader("config set", opts.stage, identity);
|
|
531
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
532
|
+
const cwd = resolveTierDir(terraformDir, opts.stage, "app");
|
|
533
|
+
const tfvarsPath = `${cwd}/terraform.tfvars`;
|
|
534
|
+
const oldValue = readTfVar(tfvarsPath, tfKey);
|
|
535
|
+
setTfVar(tfvarsPath, tfKey, value);
|
|
536
|
+
console.log(` ${key}: ${oldValue ?? "(unset)"} \u2192 ${value}`);
|
|
537
|
+
if (opts.apply) {
|
|
538
|
+
console.log("");
|
|
539
|
+
console.log(" Applying configuration change...");
|
|
540
|
+
await ensureInit(cwd);
|
|
541
|
+
await ensureWorkspace(cwd, opts.stage);
|
|
542
|
+
const code = await runTerraform(cwd, [
|
|
543
|
+
"apply",
|
|
544
|
+
"-auto-approve",
|
|
545
|
+
`-var=stage=${opts.stage}`
|
|
546
|
+
]);
|
|
547
|
+
if (code !== 0) {
|
|
548
|
+
printError(`Deploy failed (exit ${code})`);
|
|
549
|
+
process.exit(code);
|
|
550
|
+
}
|
|
551
|
+
printSuccess(`Configuration applied: ${key} = ${value}`);
|
|
552
|
+
} else {
|
|
553
|
+
printSuccess(`Configuration updated: ${key} = ${value}`);
|
|
554
|
+
printWarning("Run with --apply to deploy the change, or run 'thinkwork deploy' separately.");
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/commands/bootstrap.ts
|
|
560
|
+
import { spawn as spawn2 } from "child_process";
|
|
561
|
+
import { resolve } from "path";
|
|
562
|
+
function getTerraformOutput(cwd, key) {
|
|
563
|
+
return new Promise((resolve3, reject) => {
|
|
564
|
+
const proc = spawn2("terraform", ["output", "-raw", key], {
|
|
565
|
+
cwd,
|
|
566
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
567
|
+
});
|
|
568
|
+
let stdout = "";
|
|
569
|
+
proc.stdout.on("data", (d) => stdout += d);
|
|
570
|
+
proc.on("close", (code) => {
|
|
571
|
+
if (code === 0) resolve3(stdout.trim());
|
|
572
|
+
else reject(new Error(`terraform output ${key} failed (exit ${code})`));
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function runScript(scriptPath, args) {
|
|
577
|
+
return new Promise((resolve3) => {
|
|
578
|
+
const proc = spawn2("bash", [scriptPath, ...args], {
|
|
579
|
+
stdio: "inherit"
|
|
580
|
+
});
|
|
581
|
+
proc.on("close", (code) => resolve3(code ?? 1));
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
function registerBootstrapCommand(program2) {
|
|
585
|
+
program2.command("bootstrap").description("Seed workspace defaults, skill catalog, and per-tenant files for a stage").requiredOption("-s, --stage <name>", "Deployment stage").action(async (opts) => {
|
|
586
|
+
const stageCheck = validateStage(opts.stage);
|
|
587
|
+
if (!stageCheck.valid) {
|
|
588
|
+
printError(stageCheck.error);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
const identity = getAwsIdentity();
|
|
592
|
+
printHeader("bootstrap", opts.stage, identity);
|
|
593
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
594
|
+
const cwd = resolveTierDir(terraformDir, opts.stage, "app");
|
|
595
|
+
await ensureInit(cwd);
|
|
596
|
+
await ensureWorkspace(cwd, opts.stage);
|
|
597
|
+
let bucket;
|
|
598
|
+
let dbEndpoint;
|
|
599
|
+
let dbPassword;
|
|
600
|
+
try {
|
|
601
|
+
bucket = await getTerraformOutput(cwd, "bucket_name");
|
|
602
|
+
dbEndpoint = await getTerraformOutput(cwd, "db_cluster_endpoint");
|
|
603
|
+
const secretArn = await getTerraformOutput(cwd, "db_secret_arn");
|
|
604
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
605
|
+
const secretJson = execSync5(
|
|
606
|
+
`aws secretsmanager get-secret-value --secret-id "${secretArn}" --query SecretString --output text`,
|
|
607
|
+
{ encoding: "utf-8" }
|
|
608
|
+
).trim();
|
|
609
|
+
const secret = JSON.parse(secretJson);
|
|
610
|
+
dbPassword = secret.password;
|
|
611
|
+
} catch (err) {
|
|
612
|
+
printError(`Failed to read terraform outputs: ${err}`);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
const databaseUrl = `postgresql://thinkwork_admin:${encodeURIComponent(dbPassword)}@${dbEndpoint}:5432/thinkwork?sslmode=no-verify`;
|
|
616
|
+
const repoRoot = resolve(terraformDir);
|
|
617
|
+
const scriptPath = resolve(repoRoot, "scripts/bootstrap-workspace.sh");
|
|
618
|
+
const code = await runScript(scriptPath, [opts.stage, bucket, databaseUrl]);
|
|
619
|
+
if (code !== 0) {
|
|
620
|
+
printError(`Bootstrap failed (exit ${code})`);
|
|
621
|
+
process.exit(code);
|
|
622
|
+
}
|
|
623
|
+
printSuccess("Bootstrap complete");
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/commands/login.ts
|
|
628
|
+
import { execSync as execSync3 } from "child_process";
|
|
629
|
+
import { createInterface as createInterface2 } from "readline";
|
|
630
|
+
function ask(prompt) {
|
|
631
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
632
|
+
return new Promise((resolve3) => {
|
|
633
|
+
rl.question(prompt, (answer) => {
|
|
634
|
+
rl.close();
|
|
635
|
+
resolve3(answer.trim());
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
function registerLoginCommand(program2) {
|
|
640
|
+
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) => {
|
|
641
|
+
printHeader("login", opts.profile);
|
|
642
|
+
const existing = getAwsIdentity();
|
|
643
|
+
if (existing) {
|
|
644
|
+
console.log(` Already authenticated:`);
|
|
645
|
+
console.log(` Account: ${existing.account}`);
|
|
646
|
+
console.log(` Region: ${existing.region}`);
|
|
647
|
+
console.log(` ARN: ${existing.arn}`);
|
|
648
|
+
console.log("");
|
|
649
|
+
const reauth = await ask(" Re-authenticate? [y/N] ");
|
|
650
|
+
if (reauth.toLowerCase() !== "y") {
|
|
651
|
+
printSuccess("Using existing credentials");
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (opts.sso) {
|
|
656
|
+
console.log(" Launching AWS SSO login...");
|
|
657
|
+
console.log("");
|
|
658
|
+
try {
|
|
659
|
+
execSync3(`aws sso login --profile ${opts.profile}`, {
|
|
660
|
+
stdio: "inherit"
|
|
661
|
+
});
|
|
662
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
663
|
+
const identity2 = getAwsIdentity();
|
|
664
|
+
if (identity2) {
|
|
665
|
+
printSuccess(`Logged in via SSO (account: ${identity2.account}, region: ${identity2.region})`);
|
|
666
|
+
} else {
|
|
667
|
+
printError("SSO login succeeded but could not verify identity");
|
|
668
|
+
}
|
|
669
|
+
} catch {
|
|
670
|
+
printError("SSO login failed. Run `aws configure sso` first to set up your SSO profile.");
|
|
671
|
+
}
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
console.log(" Enter your AWS credentials. These will be saved to the");
|
|
675
|
+
console.log(` AWS CLI profile "${opts.profile}".`);
|
|
676
|
+
console.log("");
|
|
677
|
+
const accessKeyId = await ask(" AWS Access Key ID: ");
|
|
678
|
+
if (!accessKeyId) {
|
|
679
|
+
printError("Access Key ID is required");
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
const secretAccessKey = await ask(" AWS Secret Access Key: ");
|
|
683
|
+
if (!secretAccessKey) {
|
|
684
|
+
printError("Secret Access Key is required");
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
const region = await ask(" Default region [us-east-1]: ");
|
|
688
|
+
const finalRegion = region || "us-east-1";
|
|
689
|
+
try {
|
|
690
|
+
execSync3(`aws configure set aws_access_key_id "${accessKeyId}" --profile ${opts.profile}`, { stdio: "pipe" });
|
|
691
|
+
execSync3(`aws configure set aws_secret_access_key "${secretAccessKey}" --profile ${opts.profile}`, { stdio: "pipe" });
|
|
692
|
+
execSync3(`aws configure set region "${finalRegion}" --profile ${opts.profile}`, { stdio: "pipe" });
|
|
693
|
+
} catch (err) {
|
|
694
|
+
printError(`Failed to save credentials: ${err}`);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
698
|
+
const identity = getAwsIdentity();
|
|
699
|
+
if (identity) {
|
|
700
|
+
printSuccess(`Logged in (account: ${identity.account}, region: ${identity.region})`);
|
|
701
|
+
console.log("");
|
|
702
|
+
console.log(` Profile saved as "${opts.profile}". Use it with:`);
|
|
703
|
+
console.log(` thinkwork deploy -s dev --profile ${opts.profile}`);
|
|
704
|
+
console.log(` export AWS_PROFILE=${opts.profile}`);
|
|
705
|
+
} else {
|
|
706
|
+
printError("Credentials saved but could not verify. Check your Access Key ID and Secret.");
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/commands/init.ts
|
|
712
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
713
|
+
import { resolve as resolve2, join } from "path";
|
|
714
|
+
import { execSync as execSync4 } from "child_process";
|
|
715
|
+
import { createInterface as createInterface3 } from "readline";
|
|
716
|
+
function ask2(prompt, defaultVal = "") {
|
|
717
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
718
|
+
const suffix = defaultVal ? ` [${defaultVal}]` : "";
|
|
719
|
+
return new Promise((resolve3) => {
|
|
720
|
+
rl.question(`${prompt}${suffix}: `, (answer) => {
|
|
721
|
+
rl.close();
|
|
722
|
+
resolve3(answer.trim() || defaultVal);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function generatePassword() {
|
|
727
|
+
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%";
|
|
728
|
+
let pw = "";
|
|
729
|
+
const bytes = new Uint8Array(32);
|
|
730
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
731
|
+
for (const b of bytes) pw += chars[b % chars.length];
|
|
732
|
+
return pw;
|
|
733
|
+
}
|
|
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
|
+
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) => {
|
|
753
|
+
const stageCheck = validateStage(opts.stage);
|
|
754
|
+
if (!stageCheck.valid) {
|
|
755
|
+
printError(stageCheck.error);
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
const identity = getAwsIdentity();
|
|
759
|
+
printHeader("init", opts.stage, identity);
|
|
760
|
+
if (!identity) {
|
|
761
|
+
printError("AWS credentials not configured. Run `thinkwork login` first.");
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
const rootDir = resolve2(opts.dir);
|
|
765
|
+
const tfDir = join(rootDir, "terraform", "examples", "greenfield");
|
|
766
|
+
const tfvarsPath = join(tfDir, "terraform.tfvars");
|
|
767
|
+
if (existsSync3(tfvarsPath)) {
|
|
768
|
+
printWarning(`terraform.tfvars already exists at ${tfvarsPath}`);
|
|
769
|
+
const overwrite = await ask2(" Overwrite? [y/N]");
|
|
770
|
+
if (overwrite.toLowerCase() !== "y") {
|
|
771
|
+
console.log(" Aborted.");
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
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 });
|
|
782
|
+
}
|
|
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);
|
|
784
|
+
writeFileSync2(tfvarsPath, tfvars);
|
|
785
|
+
console.log(`
|
|
786
|
+
Wrote ${tfvarsPath}`);
|
|
787
|
+
console.log("\n Initializing Terraform...\n");
|
|
788
|
+
try {
|
|
789
|
+
execSync4("terraform init", { cwd: tfDir, stdio: "inherit" });
|
|
790
|
+
} catch {
|
|
791
|
+
printWarning("Terraform init failed \u2014 you may need to install Terraform first.");
|
|
792
|
+
printWarning("Run: thinkwork doctor -s " + opts.stage);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
printSuccess(`Environment "${opts.stage}" initialized`);
|
|
796
|
+
console.log("");
|
|
797
|
+
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`);
|
|
801
|
+
console.log("");
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/cli.ts
|
|
806
|
+
var program = new Command();
|
|
807
|
+
program.name("thinkwork").description(
|
|
808
|
+
"Thinkwork CLI \u2014 deploy, manage, and interact with your Thinkwork stack"
|
|
809
|
+
).version(VERSION, "-v, --version", "Print the CLI version").option(
|
|
810
|
+
"-p, --profile <name>",
|
|
811
|
+
"AWS profile to use (sets AWS_PROFILE for Terraform and AWS CLI)"
|
|
812
|
+
);
|
|
813
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
814
|
+
const profile = actionCommand.opts().profile ?? program.opts().profile;
|
|
815
|
+
if (profile) {
|
|
816
|
+
process.env.AWS_PROFILE = profile;
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
registerLoginCommand(program);
|
|
820
|
+
registerInitCommand(program);
|
|
821
|
+
registerDoctorCommand(program);
|
|
822
|
+
registerPlanCommand(program);
|
|
823
|
+
registerDeployCommand(program);
|
|
824
|
+
registerBootstrapCommand(program);
|
|
825
|
+
registerDestroyCommand(program);
|
|
826
|
+
registerOutputsCommand(program);
|
|
827
|
+
registerConfigCommand(program);
|
|
828
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "thinkwork-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thinkwork CLI — deploy, manage, and interact with your Thinkwork stack",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"thinkwork": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup src/cli.ts --format esm --dts --clean",
|
|
15
|
+
"dev": "tsx src/cli.ts",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"lint": "echo 'lint: skipped (eslint not configured)'",
|
|
19
|
+
"prepublishOnly": "npm run build && npm run typecheck"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^5.6.2",
|
|
23
|
+
"commander": "^12.0.0",
|
|
24
|
+
"ora": "^9.3.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^25.6.0",
|
|
28
|
+
"tsup": "^8.0.0",
|
|
29
|
+
"tsx": "^4.0.0",
|
|
30
|
+
"typescript": "^5.5.0",
|
|
31
|
+
"vitest": "^2.0.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/thinkwork-ai/thinkwork.git",
|
|
39
|
+
"directory": "apps/cli"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://thinkwork.ai",
|
|
42
|
+
"keywords": [
|
|
43
|
+
"thinkwork",
|
|
44
|
+
"aws",
|
|
45
|
+
"agents",
|
|
46
|
+
"terraform",
|
|
47
|
+
"cli"
|
|
48
|
+
]
|
|
49
|
+
}
|