thinkwork-cli 0.3.0 → 0.3.2

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 CHANGED
@@ -64,6 +64,8 @@ No repo clone required — `thinkwork init` scaffolds all Terraform modules from
64
64
 
65
65
  | Command | Description |
66
66
  |---------|-------------|
67
+ | `thinkwork status` | Discover all deployed environments in AWS (clickable URLs) |
68
+ | `thinkwork status -s <stage>` | Detailed view for one environment |
67
69
  | `thinkwork outputs -s <stage>` | Show deployment outputs (API URL, Cognito IDs, etc.) |
68
70
  | `thinkwork config list` | List all initialized environments |
69
71
  | `thinkwork config list -s <stage>` | Show full config for an environment (secrets masked) |
@@ -110,6 +112,25 @@ All initialized environments are saved to `~/.thinkwork/environments/<stage>/con
110
112
 
111
113
  ## Examples
112
114
 
115
+ ### Check all environments
116
+
117
+ ```bash
118
+ thinkwork status
119
+ ```
120
+
121
+ ```
122
+ Environments
123
+ ──────────────────────────────────────────────────────────────────────
124
+ Stage Source Lambdas AgentCore Memory URLs
125
+ ──────────────────────────────────────────────────────────────────────
126
+ ● dev aws+cli 42 active hindsight ✓ API: https://ho7oy...
127
+ WS: dcrs2r...
128
+ Mem: http://tw-dev...
129
+ ──────────────────────────────────────────────────────────────────────
130
+ ```
131
+
132
+ URLs are clickable in supported terminals (iTerm2, Windows Terminal, VS Code, etc.).
133
+
113
134
  ### Switch memory engine
114
135
 
115
136
  ```bash
package/dist/cli.js CHANGED
@@ -738,8 +738,8 @@ function registerBootstrapCommand(program2) {
738
738
  bucket = await getTerraformOutput(cwd, "bucket_name");
739
739
  dbEndpoint = await getTerraformOutput(cwd, "db_cluster_endpoint");
740
740
  const secretArn = await getTerraformOutput(cwd, "db_secret_arn");
741
- const { execSync: execSync6 } = await import("child_process");
742
- const secretJson = execSync6(
741
+ const { execSync: execSync7 } = await import("child_process");
742
+ const secretJson = execSync7(
743
743
  `aws secretsmanager get-secret-value --secret-id "${secretArn}" --query SecretString --output text`,
744
744
  { encoding: "utf-8" }
745
745
  ).trim();
@@ -1273,6 +1273,189 @@ output "hindsight_endpoint" { value = module.thinkwork.hindsight_endpoint }
1273
1273
  });
1274
1274
  }
1275
1275
 
1276
+ // src/commands/status.ts
1277
+ import { execSync as execSync6 } from "child_process";
1278
+ import chalk6 from "chalk";
1279
+ function link(url, label) {
1280
+ const text = label || url;
1281
+ return `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
1282
+ }
1283
+ function runAws(cmd) {
1284
+ try {
1285
+ return execSync6(`aws ${cmd}`, {
1286
+ encoding: "utf-8",
1287
+ timeout: 15e3,
1288
+ stdio: ["pipe", "pipe", "pipe"]
1289
+ }).trim();
1290
+ } catch {
1291
+ return null;
1292
+ }
1293
+ }
1294
+ function discoverAwsStages(region) {
1295
+ const stages = /* @__PURE__ */ new Map();
1296
+ const raw = runAws(
1297
+ `lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
1298
+ );
1299
+ if (!raw) return stages;
1300
+ const functions = JSON.parse(raw);
1301
+ for (const fn of functions) {
1302
+ const match = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
1303
+ if (match) {
1304
+ const stage = match[1];
1305
+ stages.set(stage, { stage, source: "aws", region });
1306
+ }
1307
+ }
1308
+ for (const [stage, info] of stages) {
1309
+ const count = functions.filter((f) => f.startsWith(`thinkwork-${stage}-`)).length;
1310
+ info.lambdaCount = count;
1311
+ const apiRaw = runAws(
1312
+ `apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
1313
+ );
1314
+ if (apiRaw && apiRaw !== "None") info.apiEndpoint = apiRaw;
1315
+ const appsyncRaw = runAws(
1316
+ `appsync list-graphql-apis --region ${region} --query "graphqlApis[?name=='thinkwork-${stage}-subscriptions'].uris.REALTIME|[0]" --output text`
1317
+ );
1318
+ if (appsyncRaw && appsyncRaw !== "None") info.appsyncUrl = appsyncRaw;
1319
+ const acRaw = runAws(
1320
+ `lambda get-function --function-name thinkwork-${stage}-agentcore --region ${region} --query "Configuration.State" --output text 2>/dev/null`
1321
+ );
1322
+ info.agentcoreStatus = acRaw || "not deployed";
1323
+ const bucketRaw = runAws(
1324
+ `s3api head-bucket --bucket thinkwork-${stage}-storage --region ${region} 2>/dev/null && echo "exists"`
1325
+ );
1326
+ info.bucketName = bucketRaw ? `thinkwork-${stage}-storage` : void 0;
1327
+ const ecsRaw = runAws(
1328
+ `ecs describe-services --cluster thinkwork-${stage}-cluster --services thinkwork-${stage}-hindsight --region ${region} --query "services[0].runningCount" --output text 2>/dev/null`
1329
+ );
1330
+ if (ecsRaw && ecsRaw !== "None" && ecsRaw !== "0") {
1331
+ info.memoryEngine = "hindsight";
1332
+ const albRaw = runAws(
1333
+ `elbv2 describe-load-balancers --region ${region} --query "LoadBalancers[?contains(LoadBalancerName, 'tw-${stage}-hindsight')].DNSName|[0]" --output text`
1334
+ );
1335
+ if (albRaw && albRaw !== "None") {
1336
+ info.hindsightEndpoint = `http://${albRaw}`;
1337
+ try {
1338
+ const health = execSync6(`curl -s --max-time 3 http://${albRaw}/health`, { encoding: "utf-8" }).trim();
1339
+ info.hindsightHealth = health.includes("healthy") ? "healthy" : "unhealthy";
1340
+ } catch {
1341
+ info.hindsightHealth = "unreachable";
1342
+ }
1343
+ }
1344
+ } else {
1345
+ info.memoryEngine = "managed";
1346
+ }
1347
+ }
1348
+ return stages;
1349
+ }
1350
+ function registerStatusCommand(program2) {
1351
+ program2.command("status").description("Show all Thinkwork environments (AWS + local)").option("-s, --stage <name>", "Show details for a specific stage").option("--region <region>", "AWS region to scan", "us-east-1").action(async (opts) => {
1352
+ const identity = getAwsIdentity();
1353
+ printHeader("status", opts.stage || "all", identity);
1354
+ if (!identity) {
1355
+ printError("AWS credentials not configured. Run `thinkwork login` first.");
1356
+ process.exit(1);
1357
+ }
1358
+ console.log(chalk6.dim(" Scanning AWS account for Thinkwork deployments...\n"));
1359
+ const awsStages = discoverAwsStages(opts.region);
1360
+ const localEnvs = listEnvironments();
1361
+ const merged = /* @__PURE__ */ new Map();
1362
+ for (const [stage, info] of awsStages) {
1363
+ const local = localEnvs.find((e) => e.stage === stage);
1364
+ merged.set(stage, {
1365
+ stage,
1366
+ source: local ? "both" : "aws",
1367
+ region: opts.region,
1368
+ accountId: identity.account,
1369
+ ...info
1370
+ });
1371
+ }
1372
+ for (const env of localEnvs) {
1373
+ if (!merged.has(env.stage)) {
1374
+ merged.set(env.stage, {
1375
+ stage: env.stage,
1376
+ source: "local",
1377
+ region: env.region,
1378
+ accountId: env.accountId
1379
+ });
1380
+ }
1381
+ }
1382
+ if (opts.stage) {
1383
+ const info = merged.get(opts.stage);
1384
+ if (!info) {
1385
+ printError(`No environment "${opts.stage}" found in AWS or local config.`);
1386
+ process.exit(1);
1387
+ }
1388
+ console.log(chalk6.bold.cyan(` \u2B21 ${info.stage}`));
1389
+ console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1390
+ console.log(` ${chalk6.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
1391
+ console.log(` ${chalk6.bold("Region:")} ${info.region}`);
1392
+ console.log(` ${chalk6.bold("Account:")} ${info.accountId}`);
1393
+ console.log(` ${chalk6.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
1394
+ console.log(` ${chalk6.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
1395
+ console.log(` ${chalk6.bold("Memory:")} ${info.memoryEngine || "unknown"}`);
1396
+ if (info.hindsightHealth) console.log(` ${chalk6.bold("Hindsight:")} ${info.hindsightHealth}`);
1397
+ if (info.bucketName) console.log(` ${chalk6.bold("S3 bucket:")} ${info.bucketName}`);
1398
+ console.log("");
1399
+ console.log(chalk6.bold(" URLs:"));
1400
+ if (info.apiEndpoint) console.log(` API: ${link(info.apiEndpoint)}`);
1401
+ if (info.appsyncUrl) console.log(` WebSocket: ${link(info.appsyncUrl)}`);
1402
+ if (info.hindsightEndpoint) console.log(` Hindsight: ${link(info.hindsightEndpoint)}`);
1403
+ console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1404
+ const local = loadEnvironment(opts.stage);
1405
+ if (local) {
1406
+ console.log(chalk6.dim(` Terraform dir: ${local.terraformDir}`));
1407
+ } else {
1408
+ console.log(chalk6.dim(` No local config. Run: thinkwork init -s ${opts.stage}`));
1409
+ }
1410
+ console.log("");
1411
+ return;
1412
+ }
1413
+ if (merged.size === 0) {
1414
+ console.log(" No Thinkwork environments found.");
1415
+ console.log(` Run ${chalk6.cyan("thinkwork init -s <stage>")} to create one.`);
1416
+ console.log("");
1417
+ return;
1418
+ }
1419
+ const COL1 = 16;
1420
+ const COL2 = 10;
1421
+ const COL3 = 10;
1422
+ const COL4 = 14;
1423
+ const COL5 = 14;
1424
+ const pad = " ".repeat(2 + 2 + COL1 + COL2 + COL3 + COL4 + COL5);
1425
+ console.log(chalk6.bold(" Environments"));
1426
+ console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1427
+ console.log(
1428
+ chalk6.dim(" ") + "Stage".padEnd(COL1) + "Source".padEnd(COL2) + "Lambdas".padEnd(COL3) + "AgentCore".padEnd(COL4) + "Memory".padEnd(COL5) + "URLs"
1429
+ );
1430
+ console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1431
+ for (const [, info] of [...merged].sort((a, b) => a[0].localeCompare(b[0]))) {
1432
+ const sourceBadge = info.source === "both" ? chalk6.green("\u25CF") : info.source === "aws" ? chalk6.yellow("\u25CF") : chalk6.dim("\u25CB");
1433
+ const acStatus = info.agentcoreStatus === "Active" ? chalk6.green("active") : info.agentcoreStatus === "not deployed" ? chalk6.dim("\u2014") : chalk6.yellow(info.agentcoreStatus || "\u2014");
1434
+ const memBadge = info.memoryEngine === "hindsight" ? info.hindsightHealth === "healthy" ? chalk6.magenta("hindsight \u2713") : chalk6.yellow("hindsight ?") : chalk6.dim(info.memoryEngine || "\u2014");
1435
+ const prefix = ` ${sourceBadge} ` + chalk6.bold(info.stage.padEnd(COL1 - 1)) + " " + (info.source === "both" ? "aws+cli" : info.source).padEnd(COL2) + String(info.lambdaCount || "\u2014").padEnd(COL3);
1436
+ const urls = [];
1437
+ if (info.apiEndpoint) urls.push(`API: ${link(info.apiEndpoint, info.apiEndpoint)}`);
1438
+ if (info.appsyncUrl) urls.push(`WS: ${link(info.appsyncUrl, info.appsyncUrl.replace("wss://", "").split(".")[0] + "...")}`);
1439
+ if (info.hindsightEndpoint) urls.push(`Mem: ${link(info.hindsightEndpoint, info.hindsightEndpoint)}`);
1440
+ if (urls.length === 0) {
1441
+ console.log(prefix + acStatus.padEnd(22) + memBadge.padEnd(22) + chalk6.dim("\u2014"));
1442
+ } else {
1443
+ console.log(prefix + acStatus.padEnd(22) + memBadge.padEnd(22) + chalk6.dim(urls[0]));
1444
+ for (let i = 1; i < urls.length; i++) {
1445
+ console.log(pad + chalk6.dim(urls[i]));
1446
+ }
1447
+ }
1448
+ }
1449
+ console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1450
+ console.log(
1451
+ chalk6.dim(` ${merged.size} environment(s) `) + chalk6.green("\u25CF") + chalk6.dim(" aws+cli ") + chalk6.yellow("\u25CF") + chalk6.dim(" aws only ") + chalk6.dim("\u25CB local only")
1452
+ );
1453
+ console.log("");
1454
+ console.log(` Details: ${chalk6.cyan("thinkwork status -s <stage>")}`);
1455
+ console.log("");
1456
+ });
1457
+ }
1458
+
1276
1459
  // src/cli.ts
1277
1460
  var program = new Command();
1278
1461
  program.name("thinkwork").description(
@@ -1294,6 +1477,7 @@ registerPlanCommand(program);
1294
1477
  registerDeployCommand(program);
1295
1478
  registerBootstrapCommand(program);
1296
1479
  registerDestroyCommand(program);
1480
+ registerStatusCommand(program);
1297
1481
  registerOutputsCommand(program);
1298
1482
  registerConfigCommand(program);
1299
1483
  program.parse();
@@ -34,7 +34,7 @@ variable "database_url" {
34
34
  variable "image_tag" {
35
35
  description = "Hindsight Docker image tag (ghcr.io/vectorize-io/hindsight:<tag>)"
36
36
  type = string
37
- default = "0.4.22"
37
+ default = "0.5.0"
38
38
  }
39
39
 
40
40
  data "aws_region" "current" {}
@@ -152,7 +152,7 @@ variable "memory_engine" {
152
152
  variable "hindsight_image_tag" {
153
153
  description = "Hindsight Docker image tag (only used when memory_engine = 'hindsight')"
154
154
  type = string
155
- default = "0.4.22"
155
+ default = "0.5.0"
156
156
  }
157
157
 
158
158
  variable "agentcore_memory_id" {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkwork-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Thinkwork CLI — deploy, manage, and interact with your Thinkwork stack",
5
5
  "license": "MIT",
6
6
  "type": "module",