thinkwork-cli 0.7.0 → 0.8.1

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/dist/cli.js CHANGED
@@ -32,6 +32,45 @@ function saveCliConfig(next, pathOverride) {
32
32
  const merged = { ...loadCliConfig(pathOverride), ...next };
33
33
  writeFileSync(path2, JSON.stringify(merged, null, 2) + "\n");
34
34
  }
35
+ function saveStageSession(stage, session, pathOverride) {
36
+ const current = loadCliConfig(pathOverride);
37
+ const sessions = { ...current.sessions ?? {}, [stage]: session };
38
+ saveCliConfig({ sessions }, pathOverride);
39
+ }
40
+ function loadStageSession(stage, pathOverride) {
41
+ return loadCliConfig(pathOverride).sessions?.[stage] ?? null;
42
+ }
43
+ function clearStageSession(stage, pathOverride) {
44
+ const current = loadCliConfig(pathOverride);
45
+ if (!current.sessions?.[stage]) return;
46
+ const { [stage]: _removed, ...rest } = current.sessions;
47
+ saveCliConfig({ sessions: rest }, pathOverride);
48
+ }
49
+
50
+ // src/lib/output.ts
51
+ import chalk from "chalk";
52
+ var jsonMode = false;
53
+ function setJsonMode(enabled) {
54
+ jsonMode = Boolean(enabled);
55
+ }
56
+ function isJsonMode() {
57
+ return jsonMode;
58
+ }
59
+ function printJson(value) {
60
+ if (!jsonMode) return;
61
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
62
+ }
63
+ function printKeyValue(pairs) {
64
+ if (jsonMode) return;
65
+ const width = Math.max(...pairs.map(([k]) => k.length));
66
+ for (const [k, v] of pairs) {
67
+ const label = chalk.dim(k.padEnd(width) + " ");
68
+ console.log(` ${label}${v ?? chalk.dim("\u2014")}`);
69
+ }
70
+ }
71
+ function logStderr(message) {
72
+ process.stderr.write(message + "\n");
73
+ }
35
74
 
36
75
  // src/config.ts
37
76
  var VALID_COMPONENTS = ["foundation", "data", "app", "all"];
@@ -169,7 +208,7 @@ async function ensureInit(cwd) {
169
208
  }
170
209
 
171
210
  // src/ui.ts
172
- import chalk from "chalk";
211
+ import chalk2 from "chalk";
173
212
  import ora from "ora";
174
213
  var TIER_LABELS = {
175
214
  foundation: "Foundation",
@@ -178,75 +217,175 @@ var TIER_LABELS = {
178
217
  };
179
218
  function printHeader(command, stage, identity) {
180
219
  console.log("");
181
- console.log(chalk.bold.cyan(" \u2B21 Thinkwork") + chalk.dim(` \u2014 ${command}`));
182
- console.log(chalk.dim(` Stage: ${chalk.white(stage)}`));
220
+ console.log(chalk2.bold.cyan(" \u2B21 Thinkwork") + chalk2.dim(` \u2014 ${command}`));
221
+ console.log(chalk2.dim(` Stage: ${chalk2.white(stage)}`));
183
222
  if (identity) {
184
- console.log(chalk.dim(` AWS: ${chalk.white(identity.account)} / ${chalk.white(identity.region)}`));
223
+ console.log(chalk2.dim(` AWS: ${chalk2.white(identity.account)} / ${chalk2.white(identity.region)}`));
185
224
  }
186
225
  console.log("");
187
226
  }
188
227
  function printTierHeader(tier, index, total) {
189
228
  const label = TIER_LABELS[tier] ?? tier;
190
- const progress = chalk.dim(`[${index + 1}/${total}]`);
191
- console.log(` ${progress} ${chalk.bold(label)}`);
229
+ const progress = chalk2.dim(`[${index + 1}/${total}]`);
230
+ console.log(` ${progress} ${chalk2.bold(label)}`);
192
231
  }
193
232
  function printSuccess(message) {
194
233
  console.log(`
195
- ${chalk.green("\u2713")} ${chalk.bold(message)}`);
234
+ ${chalk2.green("\u2713")} ${chalk2.bold(message)}`);
196
235
  }
197
236
  function printError(message) {
198
237
  console.log(`
199
- ${chalk.red("\u2717")} ${chalk.bold.red(message)}`);
238
+ ${chalk2.red("\u2717")} ${chalk2.bold.red(message)}`);
200
239
  }
201
240
  function printWarning(message) {
202
- console.log(` ${chalk.yellow("\u26A0")} ${message}`);
241
+ console.log(` ${chalk2.yellow("\u26A0")} ${message}`);
203
242
  }
204
243
  function printSummary(command, stage, tiers, startTime) {
205
244
  const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
206
245
  console.log("");
207
- 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"));
208
- console.log(` ${chalk.bold("Command:")} ${command}`);
209
- console.log(` ${chalk.bold("Stage:")} ${stage}`);
210
- console.log(` ${chalk.bold("Tiers:")} ${tiers.map((t) => TIER_LABELS[t] ?? t).join(" \u2192 ")}`);
211
- console.log(` ${chalk.bold("Time:")} ${elapsed}s`);
212
- 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"));
246
+ console.log(chalk2.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"));
247
+ console.log(` ${chalk2.bold("Command:")} ${command}`);
248
+ console.log(` ${chalk2.bold("Stage:")} ${stage}`);
249
+ console.log(` ${chalk2.bold("Tiers:")} ${tiers.map((t) => TIER_LABELS[t] ?? t).join(" \u2192 ")}`);
250
+ console.log(` ${chalk2.bold("Time:")} ${elapsed}s`);
251
+ console.log(chalk2.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"));
213
252
  }
214
253
 
215
- // src/commands/plan.ts
216
- function registerPlanCommand(program2) {
217
- 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) => {
218
- const startTime = Date.now();
219
- const stageCheck = validateStage(opts.stage);
220
- if (!stageCheck.valid) {
221
- printError(stageCheck.error);
222
- process.exit(1);
254
+ // src/lib/resolve-stage.ts
255
+ import { select } from "@inquirer/prompts";
256
+
257
+ // src/aws-discovery.ts
258
+ import { execSync as execSync2 } from "child_process";
259
+ function runAws(cmd) {
260
+ try {
261
+ return execSync2(`aws ${cmd}`, {
262
+ encoding: "utf-8",
263
+ timeout: 15e3,
264
+ stdio: ["pipe", "pipe", "pipe"]
265
+ }).trim();
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+ function listDeployedStages(region) {
271
+ const raw = runAws(
272
+ `lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
273
+ );
274
+ if (!raw) return [];
275
+ try {
276
+ const functions = JSON.parse(raw);
277
+ const stages = /* @__PURE__ */ new Set();
278
+ for (const fn of functions) {
279
+ const m = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
280
+ if (m) stages.add(m[1]);
223
281
  }
224
- const compCheck = validateComponent(opts.component);
225
- if (!compCheck.valid) {
226
- printError(compCheck.error);
282
+ return [...stages].sort();
283
+ } catch {
284
+ return [];
285
+ }
286
+ }
287
+ function getApiEndpoint(stage, region) {
288
+ const raw = runAws(
289
+ `apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
290
+ );
291
+ return raw && raw !== "None" ? raw : null;
292
+ }
293
+ function getApiAuthSecretFromLambda(stage, region) {
294
+ const raw = runAws(
295
+ `lambda get-function-configuration --function-name thinkwork-${stage}-api-tenants --region ${region} --query "Environment.Variables.API_AUTH_SECRET" --output text`
296
+ );
297
+ return raw && raw !== "None" ? raw : null;
298
+ }
299
+
300
+ // src/lib/interactive.ts
301
+ function isCancellation(err) {
302
+ return err instanceof Error && err.name === "ExitPromptError";
303
+ }
304
+ function isInteractive() {
305
+ return Boolean(process.stdin.isTTY);
306
+ }
307
+ function requireTty(label) {
308
+ if (!isInteractive()) {
309
+ printError(
310
+ `${label} is required. Pass it as a flag or re-run in an interactive terminal.`
311
+ );
312
+ process.exit(1);
313
+ }
314
+ }
315
+
316
+ // src/lib/resolve-stage.ts
317
+ async function resolveStage(opts = {}) {
318
+ const region = opts.region ?? "us-east-1";
319
+ const validate = opts.validate ?? true;
320
+ const raw = opts.flag ?? process.env.THINKWORK_STAGE ?? loadCliConfig().defaultStage ?? await pickStage(region);
321
+ if (!raw) {
322
+ printError(
323
+ "No stage specified. Pass `--stage <name>`, set THINKWORK_STAGE, or run `thinkwork login --stage <name>`."
324
+ );
325
+ process.exit(1);
326
+ }
327
+ if (validate) {
328
+ const check = validateStage(raw);
329
+ if (!check.valid) {
330
+ printError(check.error);
227
331
  process.exit(1);
228
332
  }
229
- const identity = getAwsIdentity();
230
- printHeader("plan", opts.stage, identity);
231
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
232
- const tiers = expandComponent(opts.component);
233
- for (let i = 0; i < tiers.length; i++) {
234
- const tier = tiers[i];
235
- printTierHeader(tier, i, tiers.length);
236
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
237
- await ensureInit(cwd);
238
- await ensureWorkspace(cwd, opts.stage);
239
- const code = await runTerraform(cwd, [
240
- "plan",
241
- `-var=stage=${opts.stage}`
242
- ]);
243
- if (code !== 0) {
244
- printError(`Plan failed for ${tier} (exit ${code})`);
245
- process.exit(code);
333
+ }
334
+ return raw;
335
+ }
336
+ async function pickStage(region) {
337
+ const stages = listDeployedStages(region);
338
+ if (stages.length === 0) {
339
+ printError(
340
+ `No Thinkwork deployments found in ${region}. Run \`thinkwork list\` or pass --region.`
341
+ );
342
+ process.exit(1);
343
+ }
344
+ if (stages.length === 1) {
345
+ console.log(` Using the only deployed stage: ${stages[0]}`);
346
+ return stages[0];
347
+ }
348
+ requireTty("Stage");
349
+ return await select({
350
+ message: "Which stage?",
351
+ choices: stages.map((s) => ({ name: s, value: s })),
352
+ loop: false
353
+ });
354
+ }
355
+
356
+ // src/commands/plan.ts
357
+ function registerPlanCommand(program2) {
358
+ program2.command("plan").description("Run terraform plan for a stage. Prompts for stage in a TTY when omitted.").option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").action(async (opts) => {
359
+ const startTime = Date.now();
360
+ try {
361
+ const stage = await resolveStage({ flag: opts.stage });
362
+ const compCheck = validateComponent(opts.component);
363
+ if (!compCheck.valid) {
364
+ printError(compCheck.error);
365
+ process.exit(1);
366
+ }
367
+ const identity = getAwsIdentity();
368
+ printHeader("plan", stage, identity);
369
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
370
+ const tiers = expandComponent(opts.component);
371
+ for (let i = 0; i < tiers.length; i++) {
372
+ const tier = tiers[i];
373
+ printTierHeader(tier, i, tiers.length);
374
+ const cwd = resolveTierDir(terraformDir, stage, tier);
375
+ await ensureInit(cwd);
376
+ await ensureWorkspace(cwd, stage);
377
+ const code = await runTerraform(cwd, ["plan", `-var=stage=${stage}`]);
378
+ if (code !== 0) {
379
+ printError(`Plan failed for ${tier} (exit ${code})`);
380
+ process.exit(code);
381
+ }
246
382
  }
383
+ printSuccess("Plan complete");
384
+ printSummary("plan", stage, tiers, startTime);
385
+ } catch (err) {
386
+ if (isCancellation(err)) return;
387
+ throw err;
247
388
  }
248
- printSuccess("Plan complete");
249
- printSummary("plan", opts.stage, tiers, startTime);
250
389
  });
251
390
  }
252
391
 
@@ -267,131 +406,129 @@ async function confirm(message) {
267
406
 
268
407
  // src/commands/deploy.ts
269
408
  function registerDeployCommand(program2) {
270
- 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) => {
409
+ program2.command("deploy").description("Run terraform apply for a stage. Prompts for stage in a TTY when omitted.").option("-p, --profile <name>", "AWS profile").option("-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) => {
271
410
  const startTime = Date.now();
272
- const stageCheck = validateStage(opts.stage);
273
- if (!stageCheck.valid) {
274
- printError(stageCheck.error);
275
- process.exit(1);
276
- }
277
- const compCheck = validateComponent(opts.component);
278
- if (!compCheck.valid) {
279
- printError(compCheck.error);
280
- process.exit(1);
281
- }
282
- const identity = getAwsIdentity();
283
- printHeader("deploy", opts.stage, identity);
284
- if (!identity) {
285
- printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
286
- }
287
- if (isProdLike(opts.stage) && !opts.yes) {
288
- const ok = await confirm(
289
- ` Stage "${opts.stage}" is production-like. Deploy?`
290
- );
291
- if (!ok) {
292
- console.log(" Aborted.");
293
- process.exit(0);
411
+ try {
412
+ const stage = await resolveStage({ flag: opts.stage });
413
+ const compCheck = validateComponent(opts.component);
414
+ if (!compCheck.valid) {
415
+ printError(compCheck.error);
416
+ process.exit(1);
294
417
  }
295
- } else if (!opts.yes) {
296
- const ok = await confirm(` Deploy to stage "${opts.stage}"?`);
297
- if (!ok) {
298
- console.log(" Aborted.");
299
- process.exit(0);
418
+ const identity = getAwsIdentity();
419
+ printHeader("deploy", stage, identity);
420
+ if (!identity) {
421
+ printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
300
422
  }
301
- }
302
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
303
- const tiers = expandComponent(opts.component);
304
- for (let i = 0; i < tiers.length; i++) {
305
- const tier = tiers[i];
306
- printTierHeader(tier, i, tiers.length);
307
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
308
- await ensureInit(cwd);
309
- await ensureWorkspace(cwd, opts.stage);
310
- const code = await runTerraform(cwd, [
311
- "apply",
312
- "-auto-approve",
313
- `-var=stage=${opts.stage}`
314
- ]);
315
- if (code !== 0) {
316
- printError(`Deploy failed for ${tier} (exit ${code})`);
317
- process.exit(code);
423
+ if (isProdLike(stage) && !opts.yes) {
424
+ const ok = await confirm(` Stage "${stage}" is production-like. Deploy?`);
425
+ if (!ok) {
426
+ console.log(" Aborted.");
427
+ process.exit(0);
428
+ }
429
+ } else if (!opts.yes) {
430
+ const ok = await confirm(` Deploy to stage "${stage}"?`);
431
+ if (!ok) {
432
+ console.log(" Aborted.");
433
+ process.exit(0);
434
+ }
435
+ }
436
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
437
+ const tiers = expandComponent(opts.component);
438
+ for (let i = 0; i < tiers.length; i++) {
439
+ const tier = tiers[i];
440
+ printTierHeader(tier, i, tiers.length);
441
+ const cwd = resolveTierDir(terraformDir, stage, tier);
442
+ await ensureInit(cwd);
443
+ await ensureWorkspace(cwd, stage);
444
+ const code = await runTerraform(cwd, [
445
+ "apply",
446
+ "-auto-approve",
447
+ `-var=stage=${stage}`
448
+ ]);
449
+ if (code !== 0) {
450
+ printError(`Deploy failed for ${tier} (exit ${code})`);
451
+ process.exit(code);
452
+ }
318
453
  }
454
+ printSuccess("Deploy complete");
455
+ printSummary("deploy", stage, tiers, startTime);
456
+ } catch (err) {
457
+ if (isCancellation(err)) return;
458
+ throw err;
319
459
  }
320
- printSuccess("Deploy complete");
321
- printSummary("deploy", opts.stage, tiers, startTime);
322
460
  });
323
461
  }
324
462
 
325
463
  // src/commands/destroy.ts
326
464
  function registerDestroyCommand(program2) {
327
- 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) => {
465
+ program2.command("destroy").description("Run terraform destroy for a stage. Prompts for stage in a TTY when omitted; always requires a destructive confirmation.").option("-p, --profile <name>", "AWS profile").option("-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) => {
328
466
  const startTime = Date.now();
329
- const stageCheck = validateStage(opts.stage);
330
- if (!stageCheck.valid) {
331
- printError(stageCheck.error);
332
- process.exit(1);
333
- }
334
- const compCheck = validateComponent(opts.component);
335
- if (!compCheck.valid) {
336
- printError(compCheck.error);
337
- process.exit(1);
338
- }
339
- const identity = getAwsIdentity();
340
- printHeader("destroy", opts.stage, identity);
341
- if (!identity) {
342
- printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
343
- }
344
- if (isProdLike(opts.stage)) {
345
- printWarning(`Stage "${opts.stage}" is production-like.`);
346
- if (!opts.yes) {
347
- const ok = await confirm(
348
- ` Type 'y' to confirm destruction of stage "${opts.stage}":`
349
- );
467
+ try {
468
+ const stage = await resolveStage({ flag: opts.stage });
469
+ const compCheck = validateComponent(opts.component);
470
+ if (!compCheck.valid) {
471
+ printError(compCheck.error);
472
+ process.exit(1);
473
+ }
474
+ const identity = getAwsIdentity();
475
+ printHeader("destroy", stage, identity);
476
+ if (!identity) {
477
+ printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
478
+ }
479
+ if (isProdLike(stage)) {
480
+ printWarning(`Stage "${stage}" is production-like.`);
481
+ if (!opts.yes) {
482
+ const ok = await confirm(` Type 'y' to confirm destruction of stage "${stage}":`);
483
+ if (!ok) {
484
+ console.log(" Aborted.");
485
+ process.exit(0);
486
+ }
487
+ }
488
+ console.log(` Proceeding with destroy of "${stage}" (--yes provided).`);
489
+ } else if (!opts.yes) {
490
+ const ok = await confirm(` Destroy stage "${stage}"?`);
350
491
  if (!ok) {
351
492
  console.log(" Aborted.");
352
493
  process.exit(0);
353
494
  }
354
495
  }
355
- console.log(` Proceeding with destroy of "${opts.stage}" (--yes provided).`);
356
- } else if (!opts.yes) {
357
- const ok = await confirm(` Destroy stage "${opts.stage}"?`);
358
- if (!ok) {
359
- console.log(" Aborted.");
360
- process.exit(0);
361
- }
362
- }
363
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
364
- const tiers = expandComponent(opts.component).reverse();
365
- for (let i = 0; i < tiers.length; i++) {
366
- const tier = tiers[i];
367
- printTierHeader(tier, i, tiers.length);
368
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
369
- await ensureInit(cwd);
370
- await ensureWorkspace(cwd, opts.stage);
371
- const code = await runTerraform(cwd, [
372
- "destroy",
373
- "-auto-approve",
374
- `-var=stage=${opts.stage}`
375
- ]);
376
- if (code !== 0) {
377
- printError(`Destroy failed for ${tier} (exit ${code})`);
378
- process.exit(code);
496
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
497
+ const tiers = expandComponent(opts.component).reverse();
498
+ for (let i = 0; i < tiers.length; i++) {
499
+ const tier = tiers[i];
500
+ printTierHeader(tier, i, tiers.length);
501
+ const cwd = resolveTierDir(terraformDir, stage, tier);
502
+ await ensureInit(cwd);
503
+ await ensureWorkspace(cwd, stage);
504
+ const code = await runTerraform(cwd, [
505
+ "destroy",
506
+ "-auto-approve",
507
+ `-var=stage=${stage}`
508
+ ]);
509
+ if (code !== 0) {
510
+ printError(`Destroy failed for ${tier} (exit ${code})`);
511
+ process.exit(code);
512
+ }
379
513
  }
514
+ printSuccess("Destroy complete");
515
+ printSummary("destroy", stage, tiers, startTime);
516
+ } catch (err) {
517
+ if (isCancellation(err)) return;
518
+ throw err;
380
519
  }
381
- printSuccess("Destroy complete");
382
- printSummary("destroy", opts.stage, tiers, startTime);
383
520
  });
384
521
  }
385
522
 
386
523
  // src/commands/doctor.ts
387
- import chalk2 from "chalk";
388
- import { execSync as execSync2 } from "child_process";
524
+ import chalk3 from "chalk";
525
+ import { execSync as execSync3 } from "child_process";
389
526
  function checkAwsCli() {
390
527
  return {
391
528
  name: "AWS CLI installed",
392
529
  run: () => {
393
530
  try {
394
- const v = execSync2("aws --version", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
531
+ const v = execSync3("aws --version", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
395
532
  return { pass: true, detail: v.split(" ")[0] ?? v };
396
533
  } catch {
397
534
  return { pass: false, detail: "aws CLI not found. Install: https://aws.amazon.com/cli/" };
@@ -404,7 +541,7 @@ function checkTerraformCli() {
404
541
  name: "Terraform CLI installed",
405
542
  run: () => {
406
543
  try {
407
- const v = execSync2("terraform version -json", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] });
544
+ const v = execSync3("terraform version -json", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] });
408
545
  const parsed = JSON.parse(v);
409
546
  return { pass: true, detail: `v${parsed.terraform_version}` };
410
547
  } catch {
@@ -430,7 +567,7 @@ function checkBedrockAccess() {
430
567
  name: "Bedrock model access",
431
568
  run: () => {
432
569
  try {
433
- execSync2(
570
+ execSync3(
434
571
  "aws bedrock get-foundation-model --model-identifier anthropic.claude-3-haiku-20240307-v1:0 --output json --region us-east-1",
435
572
  { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
436
573
  );
@@ -445,13 +582,15 @@ function checkBedrockAccess() {
445
582
  };
446
583
  }
447
584
  function registerDoctorCommand(program2) {
448
- 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) => {
449
- const stageCheck = validateStage(opts.stage);
450
- if (!stageCheck.valid) {
451
- printError(stageCheck.error);
452
- process.exit(1);
585
+ program2.command("doctor").description("Check AWS account prerequisites for a Thinkwork deployment. Prompts for stage in a TTY when omitted.").option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").action(async (opts) => {
586
+ let stage;
587
+ try {
588
+ stage = await resolveStage({ flag: opts.stage });
589
+ } catch (err) {
590
+ if (isCancellation(err)) return;
591
+ throw err;
453
592
  }
454
- printHeader("doctor", opts.stage);
593
+ printHeader("doctor", stage);
455
594
  const checks = [
456
595
  checkAwsCli(),
457
596
  checkTerraformCli(),
@@ -461,17 +600,17 @@ function registerDoctorCommand(program2) {
461
600
  let allPass = true;
462
601
  for (const check of checks) {
463
602
  const result = check.run();
464
- const icon = result.pass ? chalk2.green("\u2713") : chalk2.red("\u2717");
465
- const detail = result.pass ? chalk2.dim(result.detail) : chalk2.yellow(result.detail);
603
+ const icon = result.pass ? chalk3.green("\u2713") : chalk3.red("\u2717");
604
+ const detail = result.pass ? chalk3.dim(result.detail) : chalk3.yellow(result.detail);
466
605
  console.log(` ${icon} ${check.name} ${detail}`);
467
606
  if (!result.pass) allPass = false;
468
607
  }
469
608
  if (allPass) {
470
609
  console.log(`
471
- ${chalk2.green.bold("All checks passed.")}`);
610
+ ${chalk3.green.bold("All checks passed.")}`);
472
611
  } else {
473
612
  console.log(`
474
- ${chalk2.yellow.bold("Some checks failed.")} Fix the issues above before deploying.`);
613
+ ${chalk3.yellow.bold("Some checks failed.")} Fix the issues above before deploying.`);
475
614
  }
476
615
  process.exit(allPass ? 0 : 1);
477
616
  });
@@ -479,38 +618,39 @@ function registerDoctorCommand(program2) {
479
618
 
480
619
  // src/commands/outputs.ts
481
620
  function registerOutputsCommand(program2) {
482
- 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) => {
483
- const stageCheck = validateStage(opts.stage);
484
- if (!stageCheck.valid) {
485
- printError(stageCheck.error);
486
- process.exit(1);
487
- }
488
- const compCheck = validateComponent(opts.component);
489
- if (!compCheck.valid) {
490
- printError(compCheck.error);
491
- process.exit(1);
492
- }
493
- printHeader("outputs", opts.stage);
494
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
495
- const tiers = expandComponent(opts.component);
496
- for (let i = 0; i < tiers.length; i++) {
497
- const tier = tiers[i];
498
- printTierHeader(tier, i, tiers.length);
499
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
500
- await ensureInit(cwd);
501
- await ensureWorkspace(cwd, opts.stage);
502
- const code = await runTerraform(cwd, ["output"]);
503
- if (code !== 0) {
504
- printError(`Outputs failed for ${tier} (exit ${code})`);
505
- process.exit(code);
621
+ program2.command("outputs").description("Show terraform outputs for a stage. Prompts for stage in a TTY when omitted.").option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").action(async (opts) => {
622
+ try {
623
+ const stage = await resolveStage({ flag: opts.stage });
624
+ const compCheck = validateComponent(opts.component);
625
+ if (!compCheck.valid) {
626
+ printError(compCheck.error);
627
+ process.exit(1);
628
+ }
629
+ printHeader("outputs", stage);
630
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
631
+ const tiers = expandComponent(opts.component);
632
+ for (let i = 0; i < tiers.length; i++) {
633
+ const tier = tiers[i];
634
+ printTierHeader(tier, i, tiers.length);
635
+ const cwd = resolveTierDir(terraformDir, stage, tier);
636
+ await ensureInit(cwd);
637
+ await ensureWorkspace(cwd, stage);
638
+ const code = await runTerraform(cwd, ["output"]);
639
+ if (code !== 0) {
640
+ printError(`Outputs failed for ${tier} (exit ${code})`);
641
+ process.exit(code);
642
+ }
506
643
  }
644
+ } catch (err) {
645
+ if (isCancellation(err)) return;
646
+ throw err;
507
647
  }
508
648
  });
509
649
  }
510
650
 
511
651
  // src/commands/config.ts
512
652
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
513
- import chalk3 from "chalk";
653
+ import chalk4 from "chalk";
514
654
 
515
655
  // src/environments.ts
516
656
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
@@ -604,20 +744,20 @@ function registerConfigCommand(program2) {
604
744
  process.exit(1);
605
745
  }
606
746
  console.log("");
607
- console.log(chalk3.bold.cyan(` \u2B21 ${env.stage}`));
608
- 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"));
609
- console.log(` ${chalk3.bold("Region:")} ${env.region}`);
610
- console.log(` ${chalk3.bold("Account:")} ${env.accountId}`);
611
- console.log(` ${chalk3.bold("Database:")} ${env.databaseEngine}`);
612
- console.log(` ${chalk3.bold("Memory:")} managed (always on)${env.enableHindsight ? " + hindsight" : ""}`);
613
- console.log(` ${chalk3.bold("Terraform dir:")} ${env.terraformDir}`);
614
- console.log(` ${chalk3.bold("Created:")} ${env.createdAt}`);
615
- console.log(` ${chalk3.bold("Updated:")} ${env.updatedAt}`);
616
- 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"));
747
+ console.log(chalk4.bold.cyan(` \u2B21 ${env.stage}`));
748
+ console.log(chalk4.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"));
749
+ console.log(` ${chalk4.bold("Region:")} ${env.region}`);
750
+ console.log(` ${chalk4.bold("Account:")} ${env.accountId}`);
751
+ console.log(` ${chalk4.bold("Database:")} ${env.databaseEngine}`);
752
+ console.log(` ${chalk4.bold("Memory:")} managed (always on)${env.enableHindsight ? " + hindsight" : ""}`);
753
+ console.log(` ${chalk4.bold("Terraform dir:")} ${env.terraformDir}`);
754
+ console.log(` ${chalk4.bold("Created:")} ${env.createdAt}`);
755
+ console.log(` ${chalk4.bold("Updated:")} ${env.updatedAt}`);
756
+ console.log(chalk4.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"));
617
757
  const tfvarsPath = `${env.terraformDir}/terraform.tfvars`;
618
758
  if (existsSync4(tfvarsPath)) {
619
759
  console.log("");
620
- console.log(chalk3.dim(" terraform.tfvars:"));
760
+ console.log(chalk4.dim(" terraform.tfvars:"));
621
761
  const content = readFileSync3(tfvarsPath, "utf-8");
622
762
  for (const line of content.split("\n")) {
623
763
  if (line.trim() && !line.trim().startsWith("#")) {
@@ -631,7 +771,7 @@ function registerConfigCommand(program2) {
631
771
  /^(google_oauth_client_secret\s*=\s*)".*"/,
632
772
  '$1"********"'
633
773
  );
634
- console.log(` ${chalk3.dim(masked)}`);
774
+ console.log(` ${chalk4.dim(masked)}`);
635
775
  }
636
776
  }
637
777
  }
@@ -642,33 +782,35 @@ function registerConfigCommand(program2) {
642
782
  if (envs.length === 0) {
643
783
  console.log("");
644
784
  console.log(" No environments found.");
645
- console.log(` Run ${chalk3.cyan("thinkwork init -s <stage>")} to create one.`);
785
+ console.log(` Run ${chalk4.cyan("thinkwork init -s <stage>")} to create one.`);
646
786
  console.log("");
647
787
  return;
648
788
  }
649
789
  console.log("");
650
- console.log(chalk3.bold(" Environments"));
651
- console.log(chalk3.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
790
+ console.log(chalk4.bold(" Environments"));
791
+ console.log(chalk4.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"));
652
792
  for (const env of envs) {
653
- const memBadge = env.enableHindsight ? chalk3.magenta("managed+hindsight") : chalk3.dim("managed");
654
- const dbBadge = env.databaseEngine === "rds-postgres" ? chalk3.yellow("rds") : chalk3.dim("aurora");
793
+ const memBadge = env.enableHindsight ? chalk4.magenta("managed+hindsight") : chalk4.dim("managed");
794
+ const dbBadge = env.databaseEngine === "rds-postgres" ? chalk4.yellow("rds") : chalk4.dim("aurora");
655
795
  console.log(
656
- ` ${chalk3.bold.cyan(env.stage.padEnd(16))}${env.region.padEnd(14)}${env.accountId.padEnd(16)}${dbBadge.padEnd(20)}${memBadge}`
796
+ ` ${chalk4.bold.cyan(env.stage.padEnd(16))}${env.region.padEnd(14)}${env.accountId.padEnd(16)}${dbBadge.padEnd(20)}${memBadge}`
657
797
  );
658
798
  }
659
- console.log(chalk3.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
660
- console.log(chalk3.dim(` ${envs.length} environment(s)`));
799
+ console.log(chalk4.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"));
800
+ console.log(chalk4.dim(` ${envs.length} environment(s)`));
661
801
  console.log("");
662
- console.log(` Show details: ${chalk3.cyan("thinkwork config list -s <stage>")}`);
802
+ console.log(` Show details: ${chalk4.cyan("thinkwork config list -s <stage>")}`);
663
803
  console.log("");
664
804
  });
665
- config.command("get <key>").description("Get a configuration value (e.g. enable-hindsight)").requiredOption("-s, --stage <name>", "Deployment stage").action((key, opts) => {
666
- const stageCheck = validateStage(opts.stage);
667
- if (!stageCheck.valid) {
668
- printError(stageCheck.error);
669
- process.exit(1);
805
+ config.command("get <key>").description("Get a configuration value (e.g. enable-hindsight). Prompts for stage in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").action(async (key, opts) => {
806
+ let stage;
807
+ try {
808
+ stage = await resolveStage({ flag: opts.stage });
809
+ } catch (err) {
810
+ if (isCancellation(err)) return;
811
+ throw err;
670
812
  }
671
- const tfvarsPath = resolveTfvarsPath(opts.stage);
813
+ const tfvarsPath = resolveTfvarsPath(stage);
672
814
  const tfKey = key.replace(/-/g, "_");
673
815
  const value = readTfVar(tfvarsPath, tfKey);
674
816
  if (value === null) {
@@ -677,11 +819,13 @@ function registerConfigCommand(program2) {
677
819
  console.log(` ${key} = ${value}`);
678
820
  }
679
821
  });
680
- config.command("set <key> <value>").description("Set a configuration value and optionally deploy").requiredOption("-s, --stage <name>", "Deployment stage").option("--apply", "Run terraform apply after changing the value").action(async (key, value, opts) => {
681
- const stageCheck = validateStage(opts.stage);
682
- if (!stageCheck.valid) {
683
- printError(stageCheck.error);
684
- process.exit(1);
822
+ config.command("set <key> <value>").description("Set a configuration value and optionally deploy. Prompts for stage in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("--apply", "Run terraform apply after changing the value").action(async (key, value, opts) => {
823
+ let stage;
824
+ try {
825
+ stage = await resolveStage({ flag: opts.stage });
826
+ } catch (err) {
827
+ if (isCancellation(err)) return;
828
+ throw err;
685
829
  }
686
830
  let tfKey = key.replace(/-/g, "_");
687
831
  let tfValue = value;
@@ -699,13 +843,13 @@ function registerConfigCommand(program2) {
699
843
  process.exit(1);
700
844
  }
701
845
  const identity = getAwsIdentity();
702
- printHeader("config set", opts.stage, identity);
703
- const tfvarsPath = resolveTfvarsPath(opts.stage);
846
+ printHeader("config set", stage, identity);
847
+ const tfvarsPath = resolveTfvarsPath(stage);
704
848
  const oldValue = readTfVar(tfvarsPath, tfKey);
705
849
  setTfVar(tfvarsPath, tfKey, tfValue);
706
850
  console.log(` ${tfKey}: ${oldValue ?? "(unset)"} \u2192 ${tfValue}`);
707
851
  if (opts.apply) {
708
- const tfDir = resolveTerraformDir(opts.stage);
852
+ const tfDir = resolveTerraformDir(stage);
709
853
  if (!tfDir) {
710
854
  printError("Cannot find terraform directory. Run `thinkwork init` first.");
711
855
  process.exit(1);
@@ -713,11 +857,11 @@ function registerConfigCommand(program2) {
713
857
  console.log("");
714
858
  console.log(" Applying configuration change...");
715
859
  await ensureInit(tfDir);
716
- await ensureWorkspace(tfDir, opts.stage);
860
+ await ensureWorkspace(tfDir, stage);
717
861
  const code = await runTerraform(tfDir, [
718
862
  "apply",
719
863
  "-auto-approve",
720
- `-var=stage=${opts.stage}`
864
+ `-var=stage=${stage}`
721
865
  ]);
722
866
  if (code !== 0) {
723
867
  printError(`Deploy failed (exit ${code})`);
@@ -757,18 +901,20 @@ function runScript(scriptPath, args) {
757
901
  });
758
902
  }
759
903
  function registerBootstrapCommand(program2) {
760
- 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) => {
761
- const stageCheck = validateStage(opts.stage);
762
- if (!stageCheck.valid) {
763
- printError(stageCheck.error);
764
- process.exit(1);
904
+ program2.command("bootstrap").description("Seed workspace defaults, skill catalog, and per-tenant files for a stage. Prompts for stage in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").action(async (opts) => {
905
+ let stage;
906
+ try {
907
+ stage = await resolveStage({ flag: opts.stage });
908
+ } catch (err) {
909
+ if (isCancellation(err)) return;
910
+ throw err;
765
911
  }
766
912
  const identity = getAwsIdentity();
767
- printHeader("bootstrap", opts.stage, identity);
913
+ printHeader("bootstrap", stage, identity);
768
914
  const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
769
- const cwd = resolveTierDir(terraformDir, opts.stage, "app");
915
+ const cwd = resolveTierDir(terraformDir, stage, "app");
770
916
  await ensureInit(cwd);
771
- await ensureWorkspace(cwd, opts.stage);
917
+ await ensureWorkspace(cwd, stage);
772
918
  let bucket;
773
919
  let dbEndpoint;
774
920
  let dbPassword;
@@ -776,8 +922,8 @@ function registerBootstrapCommand(program2) {
776
922
  bucket = await getTerraformOutput(cwd, "bucket_name");
777
923
  dbEndpoint = await getTerraformOutput(cwd, "db_cluster_endpoint");
778
924
  const secretArn = await getTerraformOutput(cwd, "db_secret_arn");
779
- const { execSync: execSync10 } = await import("child_process");
780
- const secretJson = execSync10(
925
+ const { execSync: execSync11 } = await import("child_process");
926
+ const secretJson = execSync11(
781
927
  `aws secretsmanager get-secret-value --secret-id "${secretArn}" --query SecretString --output text`,
782
928
  { encoding: "utf-8" }
783
929
  ).trim();
@@ -790,7 +936,7 @@ function registerBootstrapCommand(program2) {
790
936
  const databaseUrl = `postgresql://thinkwork_admin:${encodeURIComponent(dbPassword)}@${dbEndpoint}:5432/thinkwork?sslmode=no-verify`;
791
937
  const repoRoot = resolve(terraformDir);
792
938
  const scriptPath = resolve(repoRoot, "scripts/bootstrap-workspace.sh");
793
- const code = await runScript(scriptPath, [opts.stage, bucket, databaseUrl]);
939
+ const code = await runScript(scriptPath, [stage, bucket, databaseUrl]);
794
940
  if (code !== 0) {
795
941
  printError(`Bootstrap failed (exit ${code})`);
796
942
  process.exit(code);
@@ -800,10 +946,10 @@ function registerBootstrapCommand(program2) {
800
946
  }
801
947
 
802
948
  // src/commands/login.ts
803
- import { execSync as execSync4 } from "child_process";
949
+ import { execSync as execSync6 } from "child_process";
804
950
  import { createInterface as createInterface2 } from "readline";
805
- import { select, Separator } from "@inquirer/prompts";
806
- import chalk5 from "chalk";
951
+ import { select as select2, Separator } from "@inquirer/prompts";
952
+ import chalk7 from "chalk";
807
953
 
808
954
  // src/aws-profiles.ts
809
955
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
@@ -882,14 +1028,14 @@ function listAwsProfiles() {
882
1028
  }
883
1029
 
884
1030
  // src/prerequisites.ts
885
- import { execSync as execSync3 } from "child_process";
1031
+ import { execSync as execSync4 } from "child_process";
886
1032
  import { mkdirSync as mkdirSync3, createWriteStream, chmodSync } from "fs";
887
1033
  import { join as join4 } from "path";
888
1034
  import { homedir as homedir4, platform, arch } from "os";
889
- import chalk4 from "chalk";
1035
+ import chalk5 from "chalk";
890
1036
  function run(cmd, opts) {
891
1037
  try {
892
- return execSync3(cmd, {
1038
+ return execSync4(cmd, {
893
1039
  encoding: "utf-8",
894
1040
  timeout: 3e4,
895
1041
  stdio: opts?.silent ? ["pipe", "pipe", "pipe"] : void 0
@@ -906,12 +1052,12 @@ function hasBrew() {
906
1052
  }
907
1053
  async function ensureAwsCli() {
908
1054
  if (isInstalled("aws")) return true;
909
- console.log(` ${chalk4.yellow("\u2192")} AWS CLI not found. Installing...`);
1055
+ console.log(` ${chalk5.yellow("\u2192")} AWS CLI not found. Installing...`);
910
1056
  const os = platform();
911
1057
  if (os === "darwin" && hasBrew()) {
912
1058
  const result = run("brew install awscli");
913
1059
  if (result !== null && isInstalled("aws")) {
914
- console.log(` ${chalk4.green("\u2713")} AWS CLI installed via Homebrew`);
1060
+ console.log(` ${chalk5.green("\u2713")} AWS CLI installed via Homebrew`);
915
1061
  return true;
916
1062
  }
917
1063
  }
@@ -926,7 +1072,7 @@ async function ensureAwsCli() {
926
1072
  run(`"${tmpDir}/aws/install" --install-dir "${homedir4()}/.thinkwork/aws-cli" --bin-dir "${homedir4()}/.local/bin" --update`);
927
1073
  process.env.PATH = `${homedir4()}/.local/bin:${process.env.PATH}`;
928
1074
  if (isInstalled("aws")) {
929
- console.log(` ${chalk4.green("\u2713")} AWS CLI installed to ~/.local/bin/aws`);
1075
+ console.log(` ${chalk5.green("\u2713")} AWS CLI installed to ~/.local/bin/aws`);
930
1076
  return true;
931
1077
  }
932
1078
  } catch {
@@ -941,24 +1087,24 @@ async function ensureAwsCli() {
941
1087
  run(`curl -sL "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "${pkgPath}"`);
942
1088
  run(`installer -pkg "${pkgPath}" -target CurrentUserHomeDirectory 2>/dev/null || sudo installer -pkg "${pkgPath}" -target /`);
943
1089
  if (isInstalled("aws")) {
944
- console.log(` ${chalk4.green("\u2713")} AWS CLI installed`);
1090
+ console.log(` ${chalk5.green("\u2713")} AWS CLI installed`);
945
1091
  return true;
946
1092
  }
947
1093
  } catch {
948
1094
  }
949
1095
  }
950
- console.log(` ${chalk4.red("\u2717")} Could not auto-install AWS CLI.`);
951
- console.log(` Install manually: ${chalk4.cyan("https://aws.amazon.com/cli/")}`);
1096
+ console.log(` ${chalk5.red("\u2717")} Could not auto-install AWS CLI.`);
1097
+ console.log(` Install manually: ${chalk5.cyan("https://aws.amazon.com/cli/")}`);
952
1098
  return false;
953
1099
  }
954
1100
  async function ensureTerraform() {
955
1101
  if (isInstalled("terraform")) return true;
956
- console.log(` ${chalk4.yellow("\u2192")} Terraform not found. Installing...`);
1102
+ console.log(` ${chalk5.yellow("\u2192")} Terraform not found. Installing...`);
957
1103
  const os = platform();
958
1104
  if ((os === "darwin" || os === "linux") && hasBrew()) {
959
1105
  const result = run("brew install hashicorp/tap/terraform");
960
1106
  if (result !== null && isInstalled("terraform")) {
961
- console.log(` ${chalk4.green("\u2713")} Terraform installed via Homebrew`);
1107
+ console.log(` ${chalk5.green("\u2713")} Terraform installed via Homebrew`);
962
1108
  return true;
963
1109
  }
964
1110
  }
@@ -980,17 +1126,17 @@ async function ensureTerraform() {
980
1126
  process.env.PATH = `${binDir}:${process.env.PATH}`;
981
1127
  }
982
1128
  if (isInstalled("terraform")) {
983
- console.log(` ${chalk4.green("\u2713")} Terraform ${tfVersion} installed to ~/.local/bin/terraform`);
1129
+ console.log(` ${chalk5.green("\u2713")} Terraform ${tfVersion} installed to ~/.local/bin/terraform`);
984
1130
  return true;
985
1131
  }
986
1132
  } catch {
987
1133
  }
988
- console.log(` ${chalk4.red("\u2717")} Could not auto-install Terraform.`);
989
- console.log(` Install manually: ${chalk4.cyan("https://developer.hashicorp.com/terraform/install")}`);
1134
+ console.log(` ${chalk5.red("\u2717")} Could not auto-install Terraform.`);
1135
+ console.log(` Install manually: ${chalk5.cyan("https://developer.hashicorp.com/terraform/install")}`);
990
1136
  return false;
991
1137
  }
992
1138
  async function ensurePrerequisites() {
993
- console.log(chalk4.dim(" Checking prerequisites...\n"));
1139
+ console.log(chalk5.dim(" Checking prerequisites...\n"));
994
1140
  const awsOk = await ensureAwsCli();
995
1141
  const tfOk = await ensureTerraform();
996
1142
  if (awsOk && tfOk) {
@@ -998,33 +1144,356 @@ async function ensurePrerequisites() {
998
1144
  return true;
999
1145
  }
1000
1146
  console.log("");
1001
- console.log(` ${chalk4.red("Missing prerequisites.")} Install them and try again.`);
1147
+ console.log(` ${chalk5.red("Missing prerequisites.")} Install them and try again.`);
1002
1148
  return false;
1003
1149
  }
1004
1150
 
1005
- // src/commands/login.ts
1006
- function ask(prompt2) {
1007
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
1008
- return new Promise((resolve3) => {
1009
- rl.question(prompt2, (answer) => {
1010
- rl.close();
1011
- resolve3(answer.trim());
1012
- });
1013
- });
1151
+ // src/cognito-discovery.ts
1152
+ import { execSync as execSync5 } from "child_process";
1153
+ function runAws2(cmd) {
1154
+ try {
1155
+ return execSync5(`aws ${cmd}`, {
1156
+ encoding: "utf-8",
1157
+ timeout: 15e3,
1158
+ stdio: ["pipe", "pipe", "pipe"]
1159
+ }).trim();
1160
+ } catch {
1161
+ return null;
1162
+ }
1014
1163
  }
1015
- function verifyProfile(profile) {
1164
+ function tryTerraformOutput(stage) {
1165
+ const tfRoot = resolveTerraformDir(stage);
1166
+ if (!tfRoot) return null;
1167
+ let cwd;
1016
1168
  try {
1017
- const raw = execSync4(
1018
- `aws sts get-caller-identity --profile ${profile} --output json`,
1019
- { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
1020
- );
1021
- const parsed = JSON.parse(raw);
1022
- return { account: parsed.Account, arn: parsed.Arn };
1169
+ cwd = resolveTierDir(tfRoot, stage, "foundation");
1023
1170
  } catch {
1024
1171
  return null;
1025
1172
  }
1173
+ const read = (key) => {
1174
+ try {
1175
+ return execSync5(`terraform output -raw ${key}`, {
1176
+ cwd,
1177
+ encoding: "utf-8",
1178
+ timeout: 15e3,
1179
+ stdio: ["pipe", "pipe", "pipe"]
1180
+ }).trim();
1181
+ } catch {
1182
+ return null;
1183
+ }
1184
+ };
1185
+ const userPoolId = read("user_pool_id") ?? void 0;
1186
+ const clientId = read("admin_client_id") ?? void 0;
1187
+ const domain = read("auth_domain") ?? void 0;
1188
+ return { userPoolId, clientId, domain };
1026
1189
  }
1027
- function describeType(type) {
1190
+ function tryAwsDiscovery(stage, region) {
1191
+ const listRaw = runAws2(
1192
+ `cognito-idp list-user-pools --max-results 60 --region ${region} --output json`
1193
+ );
1194
+ if (!listRaw) return {};
1195
+ const poolList = JSON.parse(listRaw);
1196
+ const pool = poolList.UserPools.find(
1197
+ (p) => (
1198
+ // `foundation/cognito/main.tf`:93 pattern
1199
+ p.Name === `thinkwork-${stage}-user-pool` || p.Name === `thinkwork-${stage}-users`
1200
+ )
1201
+ );
1202
+ if (!pool) return {};
1203
+ const clientsRaw = runAws2(
1204
+ `cognito-idp list-user-pool-clients --user-pool-id ${pool.Id} --region ${region} --output json`
1205
+ );
1206
+ let clientId;
1207
+ if (clientsRaw) {
1208
+ const clients = JSON.parse(clientsRaw);
1209
+ const admin = clients.UserPoolClients.find(
1210
+ (c) => c.ClientName === "ThinkworkAdmin"
1211
+ );
1212
+ clientId = admin?.ClientId;
1213
+ }
1214
+ const domain = `thinkwork-${stage}`;
1215
+ return { userPoolId: pool.Id, clientId, domain };
1216
+ }
1217
+ function discoverCognitoConfig(stage, region) {
1218
+ const fromTf = tryTerraformOutput(stage) ?? {};
1219
+ const fromAws = tryAwsDiscovery(stage, region);
1220
+ const userPoolId = fromTf.userPoolId ?? fromAws.userPoolId;
1221
+ const clientId = fromTf.clientId ?? fromAws.clientId;
1222
+ const domain = fromTf.domain ?? fromAws.domain;
1223
+ if (!userPoolId || !clientId || !domain) return null;
1224
+ return {
1225
+ userPoolId,
1226
+ clientId,
1227
+ domain,
1228
+ domainUrl: `https://${domain}.auth.${region}.amazoncognito.com`,
1229
+ region
1230
+ };
1231
+ }
1232
+
1233
+ // src/cognito-oauth.ts
1234
+ import { createServer } from "http";
1235
+ import { randomBytes } from "crypto";
1236
+ import { spawn as spawn3 } from "child_process";
1237
+ import chalk6 from "chalk";
1238
+ var CLI_LOOPBACK_PORT = 42010;
1239
+ var CALLBACK_PATH = "/callback";
1240
+ var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
1241
+ async function loginWithCognito(opts) {
1242
+ const port = opts.port ?? CLI_LOOPBACK_PORT;
1243
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1244
+ const redirectUri = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
1245
+ const state = randomBytes(16).toString("hex");
1246
+ const authorizeUrl = buildAuthorizeUrl(opts.cognito, redirectUri, state);
1247
+ const code = await waitForCallbackCode({
1248
+ port,
1249
+ expectedState: state,
1250
+ timeoutMs,
1251
+ onListening: () => {
1252
+ logStderr("");
1253
+ logStderr(` ${chalk6.cyan("Opening browser to sign in\u2026")}`);
1254
+ logStderr(` ${chalk6.dim("If it doesn't open automatically, visit:")}`);
1255
+ logStderr(` ${chalk6.dim(authorizeUrl)}`);
1256
+ logStderr("");
1257
+ if (opts.openBrowser !== false) {
1258
+ (opts.launchBrowser ?? openInBrowser)(authorizeUrl);
1259
+ }
1260
+ }
1261
+ });
1262
+ return exchangeCodeForTokens(opts.cognito, redirectUri, code);
1263
+ }
1264
+ function buildAuthorizeUrl(cognito, redirectUri, state) {
1265
+ const params = new URLSearchParams({
1266
+ client_id: cognito.clientId,
1267
+ response_type: "code",
1268
+ scope: "openid email profile",
1269
+ redirect_uri: redirectUri,
1270
+ state
1271
+ });
1272
+ return `${cognito.domainUrl}/oauth2/authorize?${params.toString()}`;
1273
+ }
1274
+ function waitForCallbackCode(opts) {
1275
+ return new Promise((resolve3, reject) => {
1276
+ const server = createServer((req, res) => handleRequest(req, res));
1277
+ let finished = false;
1278
+ const finish = (err, code) => {
1279
+ if (finished) return;
1280
+ finished = true;
1281
+ clearTimeout(timer);
1282
+ const closer = server;
1283
+ closer.closeAllConnections?.();
1284
+ server.close(() => {
1285
+ if (err) reject(err);
1286
+ else resolve3(code);
1287
+ });
1288
+ };
1289
+ const timer = setTimeout(() => {
1290
+ finish(
1291
+ new Error(
1292
+ `Timed out waiting for sign-in after ${Math.round(opts.timeoutMs / 1e3)}s. Cancel with Ctrl+C and retry.`
1293
+ )
1294
+ );
1295
+ }, opts.timeoutMs);
1296
+ function handleRequest(req, res) {
1297
+ if (!req.url) return;
1298
+ const parsed = new URL(req.url, `http://127.0.0.1:${opts.port}`);
1299
+ if (parsed.pathname !== CALLBACK_PATH) {
1300
+ res.writeHead(404, { "content-type": "text/plain" });
1301
+ res.end("Not found. Return to your CLI and close this tab.");
1302
+ return;
1303
+ }
1304
+ const code = parsed.searchParams.get("code");
1305
+ const state = parsed.searchParams.get("state");
1306
+ const error = parsed.searchParams.get("error");
1307
+ if (error) {
1308
+ const desc = parsed.searchParams.get("error_description") || error;
1309
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
1310
+ res.end(renderErrorPage(desc));
1311
+ finish(new Error(`Cognito returned an error: ${desc}`));
1312
+ return;
1313
+ }
1314
+ if (!code || !state) {
1315
+ res.writeHead(400, { "content-type": "text/plain" });
1316
+ res.end("Missing code or state.");
1317
+ finish(new Error("Cognito callback missing code or state parameter."));
1318
+ return;
1319
+ }
1320
+ if (state !== opts.expectedState) {
1321
+ res.writeHead(400, { "content-type": "text/plain" });
1322
+ res.end("State mismatch.");
1323
+ finish(
1324
+ new Error(
1325
+ "OAuth state parameter didn't match \u2014 possible CSRF or stale tab. Retry `thinkwork login`."
1326
+ )
1327
+ );
1328
+ return;
1329
+ }
1330
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", connection: "close" });
1331
+ res.end(renderSuccessPage());
1332
+ finish(null, code);
1333
+ }
1334
+ server.on("error", (err) => {
1335
+ if (err.code === "EADDRINUSE") {
1336
+ finish(
1337
+ new Error(
1338
+ `Port ${opts.port} is in use. Stop the conflicting process (another \`thinkwork login\`?) and retry.`
1339
+ )
1340
+ );
1341
+ } else {
1342
+ finish(err);
1343
+ }
1344
+ });
1345
+ server.listen(opts.port, "127.0.0.1", () => {
1346
+ opts.onListening?.();
1347
+ });
1348
+ });
1349
+ }
1350
+ async function exchangeCodeForTokens(cognito, redirectUri, code) {
1351
+ const body = new URLSearchParams({
1352
+ grant_type: "authorization_code",
1353
+ client_id: cognito.clientId,
1354
+ code,
1355
+ redirect_uri: redirectUri
1356
+ });
1357
+ const res = await fetch(`${cognito.domainUrl}/oauth2/token`, {
1358
+ method: "POST",
1359
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1360
+ body: body.toString()
1361
+ });
1362
+ if (!res.ok) {
1363
+ const text = await res.text().catch(() => "");
1364
+ throw new Error(
1365
+ `Token exchange failed (HTTP ${res.status}): ${text || "no body"}`
1366
+ );
1367
+ }
1368
+ const json = await res.json();
1369
+ return {
1370
+ idToken: json.id_token,
1371
+ accessToken: json.access_token,
1372
+ refreshToken: json.refresh_token,
1373
+ expiresAt: Math.floor(Date.now() / 1e3) + json.expires_in
1374
+ };
1375
+ }
1376
+ async function refreshCognitoTokens(cognito, refreshToken) {
1377
+ const body = new URLSearchParams({
1378
+ grant_type: "refresh_token",
1379
+ client_id: cognito.clientId,
1380
+ refresh_token: refreshToken
1381
+ });
1382
+ const res = await fetch(`${cognito.domainUrl}/oauth2/token`, {
1383
+ method: "POST",
1384
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1385
+ body: body.toString()
1386
+ });
1387
+ if (!res.ok) {
1388
+ const text = await res.text().catch(() => "");
1389
+ throw new Error(
1390
+ `Token refresh failed (HTTP ${res.status}): ${text || "no body"}`
1391
+ );
1392
+ }
1393
+ const json = await res.json();
1394
+ return {
1395
+ idToken: json.id_token,
1396
+ accessToken: json.access_token,
1397
+ expiresAt: Math.floor(Date.now() / 1e3) + json.expires_in
1398
+ };
1399
+ }
1400
+ function decodeIdToken(idToken) {
1401
+ const parts = idToken.split(".");
1402
+ if (parts.length !== 3) {
1403
+ throw new Error("Malformed id_token (expected 3 parts).");
1404
+ }
1405
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1406
+ const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
1407
+ const json = Buffer.from(padded, "base64").toString("utf-8");
1408
+ return JSON.parse(json);
1409
+ }
1410
+ function openInBrowser(url) {
1411
+ const platform2 = process.platform;
1412
+ const cmd = platform2 === "darwin" ? "open" : platform2 === "win32" ? "cmd" : "xdg-open";
1413
+ const args = platform2 === "win32" ? ["/c", "start", "", url] : [url];
1414
+ try {
1415
+ spawn3(cmd, args, { stdio: "ignore", detached: true }).unref();
1416
+ } catch {
1417
+ }
1418
+ }
1419
+ function renderSuccessPage() {
1420
+ return `<!doctype html>
1421
+ <html><head><meta charset="utf-8"><title>Thinkwork \u2014 signed in</title>
1422
+ <style>
1423
+ body { font-family: -apple-system, system-ui, sans-serif; background: #0a0a0a; color: #e5e5e5; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
1424
+ .card { padding: 2rem 3rem; background: #171717; border: 1px solid #262626; border-radius: 12px; text-align: center; max-width: 28rem; }
1425
+ h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
1426
+ p { color: #a3a3a3; margin: 0; line-height: 1.5; }
1427
+ .check { color: #10b981; font-size: 2rem; }
1428
+ </style>
1429
+ </head><body>
1430
+ <div class="card">
1431
+ <div class="check">\u2713</div>
1432
+ <h1>Signed in to Thinkwork</h1>
1433
+ <p>You can close this tab and return to your terminal.</p>
1434
+ </div>
1435
+ </body></html>`;
1436
+ }
1437
+ function renderErrorPage(message) {
1438
+ return `<!doctype html>
1439
+ <html><head><meta charset="utf-8"><title>Thinkwork \u2014 sign-in error</title>
1440
+ <style>
1441
+ body { font-family: -apple-system, system-ui, sans-serif; background: #0a0a0a; color: #e5e5e5; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
1442
+ .card { padding: 2rem 3rem; background: #171717; border: 1px solid #262626; border-radius: 12px; text-align: center; max-width: 28rem; }
1443
+ h1 { font-size: 1.25rem; margin: 0 0 0.5rem; }
1444
+ p { color: #fca5a5; margin: 0; line-height: 1.5; }
1445
+ code { display: block; margin-top: 1rem; padding: 0.75rem; background: #0a0a0a; border-radius: 6px; color: #a3a3a3; text-align: left; white-space: pre-wrap; word-break: break-word; font-size: 0.875rem; }
1446
+ </style>
1447
+ </head><body>
1448
+ <div class="card">
1449
+ <h1>Sign-in failed</h1>
1450
+ <p>Return to your terminal for details.</p>
1451
+ <code>${escapeHtml(message)}</code>
1452
+ </div>
1453
+ </body></html>`;
1454
+ }
1455
+ function escapeHtml(s) {
1456
+ return s.replace(/[&<>"']/g, (c) => {
1457
+ switch (c) {
1458
+ case "&":
1459
+ return "&amp;";
1460
+ case "<":
1461
+ return "&lt;";
1462
+ case ">":
1463
+ return "&gt;";
1464
+ case '"':
1465
+ return "&quot;";
1466
+ case "'":
1467
+ return "&#39;";
1468
+ default:
1469
+ return c;
1470
+ }
1471
+ });
1472
+ }
1473
+
1474
+ // src/commands/login.ts
1475
+ function ask(prompt) {
1476
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1477
+ return new Promise((resolve3) => {
1478
+ rl.question(prompt, (answer) => {
1479
+ rl.close();
1480
+ resolve3(answer.trim());
1481
+ });
1482
+ });
1483
+ }
1484
+ function verifyProfile(profile) {
1485
+ try {
1486
+ const raw = execSync6(
1487
+ `aws sts get-caller-identity --profile ${profile} --output json`,
1488
+ { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
1489
+ );
1490
+ const parsed = JSON.parse(raw);
1491
+ return { account: parsed.Account, arn: parsed.Arn };
1492
+ } catch {
1493
+ return null;
1494
+ }
1495
+ }
1496
+ function describeType(type) {
1028
1497
  switch (type) {
1029
1498
  case "keys":
1030
1499
  return "access keys";
@@ -1044,7 +1513,7 @@ async function pickProfile(profiles) {
1044
1513
  return { kind: "cancel" };
1045
1514
  }
1046
1515
  const choices = profiles.map((p) => ({
1047
- name: `${p.name} ${chalk5.dim(`(${describeType(p.type)})`)}`,
1516
+ name: `${p.name} ${chalk7.dim(`(${describeType(p.type)})`)}`,
1048
1517
  value: { kind: "existing", name: p.name }
1049
1518
  }));
1050
1519
  choices.push(new Separator());
@@ -1059,7 +1528,7 @@ async function pickProfile(profiles) {
1059
1528
  description: "Run `aws sso login` against the configured SSO profile."
1060
1529
  });
1061
1530
  try {
1062
- const picked = await select({
1531
+ const picked = await select2({
1063
1532
  message: "Pick an AWS profile for Thinkwork:",
1064
1533
  choices,
1065
1534
  loop: false,
@@ -1091,15 +1560,15 @@ async function runKeyEntry(targetProfile) {
1091
1560
  const region = await ask(" Default region [us-east-1]: ");
1092
1561
  const finalRegion = region || "us-east-1";
1093
1562
  try {
1094
- execSync4(
1563
+ execSync6(
1095
1564
  `aws configure set aws_access_key_id "${accessKeyId}" --profile ${targetProfile}`,
1096
1565
  { stdio: "pipe" }
1097
1566
  );
1098
- execSync4(
1567
+ execSync6(
1099
1568
  `aws configure set aws_secret_access_key "${secretAccessKey}" --profile ${targetProfile}`,
1100
1569
  { stdio: "pipe" }
1101
1570
  );
1102
- execSync4(
1571
+ execSync6(
1103
1572
  `aws configure set region "${finalRegion}" --profile ${targetProfile}`,
1104
1573
  { stdio: "pipe" }
1105
1574
  );
@@ -1113,7 +1582,7 @@ function runSsoLogin(targetProfile) {
1113
1582
  console.log(" Launching AWS SSO login...");
1114
1583
  console.log("");
1115
1584
  try {
1116
- execSync4(`aws sso login --profile ${targetProfile}`, { stdio: "inherit" });
1585
+ execSync6(`aws sso login --profile ${targetProfile}`, { stdio: "inherit" });
1117
1586
  return true;
1118
1587
  } catch {
1119
1588
  printError(
@@ -1122,7 +1591,7 @@ function runSsoLogin(targetProfile) {
1122
1591
  return false;
1123
1592
  }
1124
1593
  }
1125
- function finalize(profile, mode) {
1594
+ function finalizeAws(profile, mode) {
1126
1595
  const identity = getAwsIdentity();
1127
1596
  if (!identity) {
1128
1597
  printError(
@@ -1142,119 +1611,371 @@ function finalize(profile, mode) {
1142
1611
  ` (\`thinkwork list\`, \`thinkwork deploy\`, \u2026) will use it automatically.`
1143
1612
  );
1144
1613
  console.log(
1145
- chalk5.dim(
1614
+ chalk7.dim(
1146
1615
  ` Override per-command with --profile <other>, or unset with \`rm ~/.thinkwork/config.json\`.`
1147
1616
  )
1148
1617
  );
1149
1618
  }
1619
+ async function bootstrapUserAndTenant(stage, region, idToken) {
1620
+ const baseUrl = getApiEndpoint(stage, region);
1621
+ if (!baseUrl) return null;
1622
+ const url = `${baseUrl.replace(/\/+$/, "")}/graphql`;
1623
+ const query = `mutation BootstrapLogin {
1624
+ bootstrapUser {
1625
+ tenant { id slug name }
1626
+ }
1627
+ }`;
1628
+ try {
1629
+ const res = await fetch(url, {
1630
+ method: "POST",
1631
+ headers: {
1632
+ "content-type": "application/json",
1633
+ Authorization: idToken
1634
+ },
1635
+ body: JSON.stringify({ query })
1636
+ });
1637
+ if (!res.ok) return null;
1638
+ const json = await res.json();
1639
+ const tenant = json.data?.bootstrapUser?.tenant;
1640
+ if (!tenant) return null;
1641
+ return {
1642
+ tenantId: tenant.id,
1643
+ tenantSlug: tenant.slug,
1644
+ tenantName: tenant.name
1645
+ };
1646
+ } catch {
1647
+ return null;
1648
+ }
1649
+ }
1650
+ async function doCognitoLogin(opts) {
1651
+ printHeader("login", opts.stage);
1652
+ const cognito = discoverCognitoConfig(opts.stage, opts.region);
1653
+ if (!cognito) {
1654
+ printError(
1655
+ `Could not find a Cognito user pool for stage "${opts.stage}" in ${opts.region}. Is the stack deployed?`
1656
+ );
1657
+ process.exit(1);
1658
+ }
1659
+ console.log(` User pool: ${cognito.userPoolId}`);
1660
+ console.log(` Client: ${cognito.clientId}`);
1661
+ console.log(` Hosted UI: ${cognito.domainUrl}`);
1662
+ console.log(` Callback port: ${opts.port}`);
1663
+ try {
1664
+ const tokens = await loginWithCognito({
1665
+ cognito,
1666
+ port: opts.port,
1667
+ openBrowser: !opts.noBrowser
1668
+ });
1669
+ const claims = decodeIdToken(tokens.idToken);
1670
+ const bootstrap = await bootstrapUserAndTenant(
1671
+ opts.stage,
1672
+ opts.region,
1673
+ tokens.idToken
1674
+ );
1675
+ saveStageSession(opts.stage, {
1676
+ kind: "cognito",
1677
+ idToken: tokens.idToken,
1678
+ accessToken: tokens.accessToken,
1679
+ refreshToken: tokens.refreshToken,
1680
+ expiresAt: tokens.expiresAt,
1681
+ userPoolId: cognito.userPoolId,
1682
+ userPoolClientId: cognito.clientId,
1683
+ cognitoDomain: cognito.domain,
1684
+ region: cognito.region,
1685
+ principalId: claims.sub,
1686
+ email: claims.email,
1687
+ tenantId: bootstrap?.tenantId,
1688
+ tenantSlug: bootstrap?.tenantSlug
1689
+ });
1690
+ saveCliConfig({ defaultStage: opts.stage });
1691
+ printSuccess(`Signed in to ${opts.stage} as ${claims.email ?? claims.sub}`);
1692
+ if (bootstrap) {
1693
+ console.log(
1694
+ ` Tenant: ${bootstrap.tenantName} (slug: ${bootstrap.tenantSlug})`
1695
+ );
1696
+ } else {
1697
+ printWarning(
1698
+ "Signed in, but could not resolve a default tenant. Commands will prompt or require --tenant <slug> until one is cached."
1699
+ );
1700
+ }
1701
+ console.log("");
1702
+ console.log(
1703
+ chalk7.dim(
1704
+ ` Token expires: ${new Date(tokens.expiresAt * 1e3).toISOString()}. Refreshed automatically.`
1705
+ )
1706
+ );
1707
+ } catch (err) {
1708
+ if (isCancellation(err)) {
1709
+ console.log("");
1710
+ console.log(" Cancelled.");
1711
+ return;
1712
+ }
1713
+ printError(
1714
+ `Sign-in failed: ${err instanceof Error ? err.message : String(err)}`
1715
+ );
1716
+ process.exit(1);
1717
+ }
1718
+ }
1719
+ async function doApiKeyLogin(opts) {
1720
+ printHeader("login", opts.stage);
1721
+ const baseUrl = getApiEndpoint(opts.stage, opts.region);
1722
+ if (!baseUrl) {
1723
+ printError(
1724
+ `Cannot discover API endpoint for stage "${opts.stage}" in ${opts.region}. Is the stack deployed?`
1725
+ );
1726
+ process.exit(1);
1727
+ }
1728
+ saveStageSession(opts.stage, {
1729
+ kind: "api-key",
1730
+ authSecret: opts.apiKey,
1731
+ tenantId: opts.tenantId,
1732
+ tenantSlug: opts.tenantSlug
1733
+ });
1734
+ saveCliConfig({ defaultStage: opts.stage });
1735
+ printSuccess(`Stored api-key session for stage "${opts.stage}"`);
1736
+ if (opts.tenantSlug) {
1737
+ console.log(` Default tenant: ${opts.tenantSlug}`);
1738
+ } else {
1739
+ printWarning(
1740
+ "No tenant cached. Commands will require --tenant <slug> or THINKWORK_TENANT."
1741
+ );
1742
+ }
1743
+ }
1150
1744
  function registerLoginCommand(program2) {
1151
1745
  program2.command("login").description(
1152
- "Configure AWS credentials for Thinkwork. Picks from existing ~/.aws profiles by default; falls back to new keys or SSO."
1746
+ "Sign in. Without --stage: configure AWS credentials (for deploy / destroy). With --stage <s>: sign in to that stack's Cognito / API and cache a session for API-backed commands."
1153
1747
  ).option(
1154
1748
  "--profile <name>",
1155
1749
  "AWS profile name to configure (used when entering new keys or SSO)",
1156
1750
  "thinkwork"
1157
- ).option("--sso", "Skip the picker and go straight to SSO login").option("--keys", "Skip the picker and go straight to access-key entry").addHelpText(
1751
+ ).option("--sso", "Skip the picker and go straight to SSO login").option("--keys", "Skip the picker and go straight to access-key entry").option(
1752
+ "-s, --stage <name>",
1753
+ "Sign in to a deployed stack instead of configuring AWS credentials"
1754
+ ).option(
1755
+ "-r, --region <region>",
1756
+ "AWS region for the stack (defaults to us-east-1)",
1757
+ "us-east-1"
1758
+ ).option(
1759
+ "--api-key <secret>",
1760
+ "Non-interactive path: store the api_auth_secret as the session for --stage <s>. Skips the browser."
1761
+ ).option(
1762
+ "--tenant <slug>",
1763
+ "Cache this tenant slug on the session (used with --api-key, or to override the tenant chosen by bootstrapUser)."
1764
+ ).option(
1765
+ "--port <number>",
1766
+ `Loopback port for Cognito OAuth callback. Must match a registered callback URL. Defaults to ${CLI_LOOPBACK_PORT}.`,
1767
+ String(CLI_LOOPBACK_PORT)
1768
+ ).option(
1769
+ "--no-browser",
1770
+ "Don't attempt to open the browser automatically \u2014 print the URL instead."
1771
+ ).addHelpText(
1158
1772
  "after",
1159
1773
  `
1160
1774
  Examples:
1161
- # Interactive picker \u2014 lists profiles from ~/.aws, verifies the one you pick,
1162
- # and saves it as your Thinkwork default.
1775
+ # Configure AWS credentials (profile picker) \u2014 used before deploy/destroy/list.
1163
1776
  $ thinkwork login
1164
1777
 
1165
- # Skip the picker, enter fresh access keys into a named profile
1166
- $ thinkwork login --keys --profile thinkwork
1778
+ # Sign in to a deployed stack with Cognito (opens your browser, supports Google SSO).
1779
+ $ thinkwork login --stage dev
1780
+
1781
+ # Non-interactive CI login against prod using the api_auth_secret.
1782
+ $ thinkwork login --stage prod --api-key "$THINKWORK_API_KEY" --tenant acme
1167
1783
 
1168
- # Skip the picker, log in via AWS SSO
1784
+ # Print the URL instead of auto-opening (useful over SSH).
1785
+ $ thinkwork login --stage dev --no-browser
1786
+
1787
+ # AWS SSO (no stage)
1169
1788
  $ thinkwork login --sso --profile work-sso
1170
1789
 
1171
- After login, commands resolve the AWS profile in this order:
1172
- 1. --profile <name> (per-command override)
1173
- 2. $AWS_PROFILE env var
1174
- 3. defaultProfile from ~/.thinkwork/config.json (set by this command)
1790
+ How the session is stored:
1791
+ ~/.thinkwork/config.json gains \`sessions["<stage>"]\` with either a Cognito
1792
+ id/refresh token pair or the api-key secret + tenant. Subsequent commands
1793
+ resolve auth from this file; Cognito tokens are refreshed transparently.
1794
+
1795
+ Registered callback URL:
1796
+ The Cognito admin client must list \`http://127.0.0.1:${CLI_LOOPBACK_PORT}/callback\` in its
1797
+ callback URLs. The default terraform module already does \u2014 if you deployed
1798
+ before that default existed, run \`terraform apply\` in the foundation tier
1799
+ to pick it up. Or use \`--api-key\` to skip the browser entirely.
1175
1800
  `
1176
- ).action(async (opts) => {
1177
- printHeader("login", opts.profile);
1178
- const awsOk = await ensureAwsCli();
1179
- if (!awsOk) process.exit(1);
1180
- if (opts.sso) {
1181
- if (!runSsoLogin(opts.profile)) process.exit(1);
1182
- process.env.AWS_PROFILE = opts.profile;
1183
- finalize(opts.profile, "SSO");
1184
- return;
1185
- }
1186
- if (opts.keys) {
1187
- if (!await runKeyEntry(opts.profile)) process.exit(1);
1188
- process.env.AWS_PROFILE = opts.profile;
1189
- finalize(opts.profile, "access keys");
1190
- return;
1191
- }
1192
- const profiles = listAwsProfiles();
1193
- if (profiles.length === 0) {
1194
- console.log("");
1195
- console.log(chalk5.dim(" No AWS profiles found in ~/.aws/."));
1196
- console.log(
1197
- chalk5.dim(" Falling through to access-key entry for a new profile.")
1198
- );
1199
- if (!await runKeyEntry(opts.profile)) process.exit(1);
1200
- process.env.AWS_PROFILE = opts.profile;
1201
- finalize(opts.profile, "access keys");
1202
- return;
1203
- }
1204
- const choice = await pickProfile(profiles);
1205
- if (choice.kind === "cancel") {
1801
+ ).action(
1802
+ async (opts) => {
1803
+ if (opts.stage) {
1804
+ const check = validateStage(opts.stage);
1805
+ if (!check.valid) {
1806
+ printError(check.error);
1807
+ process.exit(1);
1808
+ }
1809
+ if (opts.apiKey) {
1810
+ await doApiKeyLogin({
1811
+ stage: opts.stage,
1812
+ region: opts.region,
1813
+ apiKey: opts.apiKey,
1814
+ tenantSlug: opts.tenant
1815
+ });
1816
+ return;
1817
+ }
1818
+ const port = Number.parseInt(opts.port, 10);
1819
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
1820
+ printError(`Invalid --port value: "${opts.port}".`);
1821
+ process.exit(1);
1822
+ }
1823
+ await doCognitoLogin({
1824
+ stage: opts.stage,
1825
+ region: opts.region,
1826
+ port,
1827
+ noBrowser: opts.browser === false
1828
+ });
1829
+ return;
1830
+ }
1831
+ printHeader("login", opts.profile);
1832
+ const awsOk = await ensureAwsCli();
1833
+ if (!awsOk) process.exit(1);
1834
+ if (opts.sso) {
1835
+ if (!runSsoLogin(opts.profile)) process.exit(1);
1836
+ process.env.AWS_PROFILE = opts.profile;
1837
+ finalizeAws(opts.profile, "SSO");
1838
+ return;
1839
+ }
1840
+ if (opts.keys) {
1841
+ if (!await runKeyEntry(opts.profile)) process.exit(1);
1842
+ process.env.AWS_PROFILE = opts.profile;
1843
+ finalizeAws(opts.profile, "access keys");
1844
+ return;
1845
+ }
1846
+ const profiles = listAwsProfiles();
1847
+ if (profiles.length === 0) {
1848
+ console.log("");
1849
+ console.log(chalk7.dim(" No AWS profiles found in ~/.aws/."));
1850
+ console.log(
1851
+ chalk7.dim(" Falling through to access-key entry for a new profile.")
1852
+ );
1853
+ if (!await runKeyEntry(opts.profile)) process.exit(1);
1854
+ process.env.AWS_PROFILE = opts.profile;
1855
+ finalizeAws(opts.profile, "access keys");
1856
+ return;
1857
+ }
1858
+ const choice = await pickProfile(profiles);
1859
+ if (choice.kind === "cancel") {
1860
+ console.log("");
1861
+ console.log(chalk7.dim(" Cancelled. No changes made."));
1862
+ return;
1863
+ }
1864
+ if (choice.kind === "keys") {
1865
+ if (!await runKeyEntry(opts.profile)) process.exit(1);
1866
+ process.env.AWS_PROFILE = opts.profile;
1867
+ finalizeAws(opts.profile, "access keys");
1868
+ return;
1869
+ }
1870
+ if (choice.kind === "sso") {
1871
+ if (!runSsoLogin(opts.profile)) process.exit(1);
1872
+ process.env.AWS_PROFILE = opts.profile;
1873
+ finalizeAws(opts.profile, "SSO");
1874
+ return;
1875
+ }
1876
+ const picked = choice.name;
1206
1877
  console.log("");
1207
- console.log(chalk5.dim(" Cancelled. No changes made."));
1208
- return;
1209
- }
1210
- if (choice.kind === "keys") {
1211
- if (!await runKeyEntry(opts.profile)) process.exit(1);
1212
- process.env.AWS_PROFILE = opts.profile;
1213
- finalize(opts.profile, "access keys");
1214
- return;
1215
- }
1216
- if (choice.kind === "sso") {
1217
- if (!runSsoLogin(opts.profile)) process.exit(1);
1218
- process.env.AWS_PROFILE = opts.profile;
1219
- finalize(opts.profile, "SSO");
1220
- return;
1878
+ console.log(` Verifying "${picked}"...`);
1879
+ const identity = verifyProfile(picked);
1880
+ if (!identity) {
1881
+ printError(
1882
+ `Could not authenticate with profile "${picked}". If it's an SSO profile, try \`aws sso login --profile ${picked}\` first.`
1883
+ );
1884
+ process.exit(1);
1885
+ }
1886
+ process.env.AWS_PROFILE = picked;
1887
+ finalizeAws(picked, "existing profile");
1221
1888
  }
1222
- const picked = choice.name;
1223
- console.log("");
1224
- console.log(` Verifying "${picked}"...`);
1225
- const identity = verifyProfile(picked);
1226
- if (!identity) {
1889
+ );
1890
+ }
1891
+
1892
+ // src/commands/logout.ts
1893
+ import { select as select3 } from "@inquirer/prompts";
1894
+ function registerLogoutCommand(program2) {
1895
+ program2.command("logout").description(
1896
+ "Forget a stored session. Touches only ~/.thinkwork/config.json; your AWS profile and Cognito pool are untouched."
1897
+ ).option("-s, --stage <name>", "Stage whose session to forget").option("--all", "Forget every stage's session").addHelpText(
1898
+ "after",
1899
+ `
1900
+ Examples:
1901
+ # Forget the session for one stage
1902
+ $ thinkwork logout --stage dev
1903
+
1904
+ # Forget every saved session (doesn't affect your AWS profile)
1905
+ $ thinkwork logout --all
1906
+
1907
+ # Pick interactively
1908
+ $ thinkwork logout
1909
+ `
1910
+ ).action(async (opts) => {
1911
+ try {
1912
+ if (opts.all) {
1913
+ saveCliConfig({ sessions: {}, defaultStage: void 0 });
1914
+ printHeader("logout", "(all stages)");
1915
+ printSuccess("Cleared every saved stack session.");
1916
+ return;
1917
+ }
1918
+ let stage = opts.stage;
1919
+ if (!stage) {
1920
+ const config2 = loadCliConfig();
1921
+ const keys = Object.keys(config2.sessions ?? {});
1922
+ if (keys.length === 0) {
1923
+ printSuccess("No sessions stored \u2014 nothing to forget.");
1924
+ return;
1925
+ }
1926
+ if (keys.length === 1) {
1927
+ stage = keys[0];
1928
+ console.log(` Only one session stored: ${stage}`);
1929
+ } else {
1930
+ requireTty("Stage");
1931
+ stage = await select3({
1932
+ message: "Forget which stage's session?",
1933
+ choices: keys.map((s) => ({ name: s, value: s })),
1934
+ loop: false
1935
+ });
1936
+ }
1937
+ }
1938
+ clearStageSession(stage);
1939
+ const config = loadCliConfig();
1940
+ if (config.defaultStage === stage) {
1941
+ saveCliConfig({ defaultStage: void 0 });
1942
+ }
1943
+ printHeader("logout", stage);
1944
+ printSuccess(`Forgot session for "${stage}".`);
1945
+ } catch (err) {
1946
+ if (isCancellation(err)) {
1947
+ console.log(" Cancelled.");
1948
+ return;
1949
+ }
1227
1950
  printError(
1228
- `Could not authenticate with profile "${picked}". If it's an SSO profile, try \`aws sso login --profile ${picked}\` first.`
1951
+ `Logout failed: ${err instanceof Error ? err.message : String(err)}`
1229
1952
  );
1230
1953
  process.exit(1);
1231
1954
  }
1232
- process.env.AWS_PROFILE = picked;
1233
- finalize(picked, "existing profile");
1234
1955
  });
1235
1956
  }
1236
1957
 
1237
1958
  // src/commands/init.ts
1238
1959
  import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, cpSync } from "fs";
1239
1960
  import { resolve as resolve2, join as join5, dirname as dirname2 } from "path";
1240
- import { execSync as execSync5 } from "child_process";
1961
+ import { execSync as execSync7 } from "child_process";
1241
1962
  import { fileURLToPath } from "url";
1242
1963
  import { createInterface as createInterface3 } from "readline";
1243
- import chalk6 from "chalk";
1964
+ import chalk8 from "chalk";
1244
1965
  var __dirname = dirname2(fileURLToPath(import.meta.url));
1245
- function ask2(prompt2, defaultVal = "") {
1966
+ function ask2(prompt, defaultVal = "") {
1246
1967
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
1247
- const suffix = defaultVal ? chalk6.dim(` [${defaultVal}]`) : "";
1968
+ const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
1248
1969
  return new Promise((resolve3) => {
1249
- rl.question(` ${prompt2}${suffix}: `, (answer) => {
1970
+ rl.question(` ${prompt}${suffix}: `, (answer) => {
1250
1971
  rl.close();
1251
1972
  resolve3(answer.trim() || defaultVal);
1252
1973
  });
1253
1974
  });
1254
1975
  }
1255
- function choose(prompt2, options, defaultVal) {
1256
- const optStr = options.map((o) => o === defaultVal ? chalk6.bold(o) : chalk6.dim(o)).join(" / ");
1257
- return ask2(`${prompt2} (${optStr})`, defaultVal);
1976
+ function choose(prompt, options, defaultVal) {
1977
+ const optStr = options.map((o) => o === defaultVal ? chalk8.bold(o) : chalk8.dim(o)).join(" / ");
1978
+ return ask2(`${prompt} (${optStr})`, defaultVal);
1258
1979
  }
1259
1980
  function generateSecret(length = 32) {
1260
1981
  const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
@@ -1321,14 +2042,34 @@ function buildTfvars(config) {
1321
2042
  return lines.join("\n");
1322
2043
  }
1323
2044
  function registerInitCommand(program2) {
1324
- 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) => {
1325
- const stageCheck = validateStage(opts.stage);
2045
+ program2.command("init").description("Initialize a new Thinkwork environment. Prompts for a stage name in a TTY when omitted (init creates a stage \u2014 the picker isn't applicable here).").option("-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) => {
2046
+ let stage = opts.stage;
2047
+ if (!stage) {
2048
+ if (!process.stdin.isTTY) {
2049
+ printError("Stage name is required. Pass -s <name> or re-run in an interactive terminal.");
2050
+ process.exit(1);
2051
+ }
2052
+ const { input: input3 } = await import("@inquirer/prompts");
2053
+ try {
2054
+ stage = await input3({
2055
+ message: "Stage name (e.g. dev, staging, prod):",
2056
+ validate: (v) => validateStage(v).error ?? true
2057
+ });
2058
+ } catch (err) {
2059
+ if (err instanceof Error && err.name === "ExitPromptError") {
2060
+ console.log(" Cancelled.");
2061
+ return;
2062
+ }
2063
+ throw err;
2064
+ }
2065
+ }
2066
+ const stageCheck = validateStage(stage);
1326
2067
  if (!stageCheck.valid) {
1327
2068
  printError(stageCheck.error);
1328
2069
  process.exit(1);
1329
2070
  }
1330
2071
  const identity = getAwsIdentity();
1331
- printHeader("init", opts.stage, identity);
2072
+ printHeader("init", stage, identity);
1332
2073
  const prereqsOk = await ensurePrerequisites();
1333
2074
  if (!prereqsOk) {
1334
2075
  process.exit(1);
@@ -1350,10 +2091,10 @@ function registerInitCommand(program2) {
1350
2091
  console.log("");
1351
2092
  }
1352
2093
  const config = {
1353
- stage: opts.stage,
2094
+ stage,
1354
2095
  account_id: identity.account,
1355
2096
  db_password: generateSecret(24),
1356
- api_auth_secret: `tw-${opts.stage}-${generateSecret(16)}`
2097
+ api_auth_secret: `tw-${stage}-${generateSecret(16)}`
1357
2098
  };
1358
2099
  if (opts.defaults) {
1359
2100
  config.region = identity.region !== "unknown" ? identity.region : "us-east-1";
@@ -1364,21 +2105,21 @@ function registerInitCommand(program2) {
1364
2105
  config.admin_url = "http://localhost:5174";
1365
2106
  config.mobile_scheme = "thinkwork";
1366
2107
  } else {
1367
- console.log(chalk6.bold(" Configure your Thinkwork environment\n"));
2108
+ console.log(chalk8.bold(" Configure your Thinkwork environment\n"));
1368
2109
  const defaultRegion = identity.region !== "unknown" ? identity.region : "us-east-1";
1369
2110
  config.region = await ask2("AWS Region", defaultRegion);
1370
2111
  console.log("");
1371
- console.log(chalk6.dim(" \u2500\u2500 Database \u2500\u2500"));
2112
+ console.log(chalk8.dim(" \u2500\u2500 Database \u2500\u2500"));
1372
2113
  config.database_engine = await choose("Database engine", ["aurora-serverless", "rds-postgres"], "aurora-serverless");
1373
2114
  console.log("");
1374
- console.log(chalk6.dim(" \u2500\u2500 Memory \u2500\u2500"));
1375
- console.log(chalk6.dim(" AgentCore managed memory is always on (automatic retention)."));
1376
- console.log(chalk6.dim(" Hindsight is an optional add-on: ECS Fargate service for"));
1377
- console.log(chalk6.dim(" semantic + entity-graph retrieval (~$75/mo)."));
2115
+ console.log(chalk8.dim(" \u2500\u2500 Memory \u2500\u2500"));
2116
+ console.log(chalk8.dim(" AgentCore managed memory is always on (automatic retention)."));
2117
+ console.log(chalk8.dim(" Hindsight is an optional add-on: ECS Fargate service for"));
2118
+ console.log(chalk8.dim(" semantic + entity-graph retrieval (~$75/mo)."));
1378
2119
  const hindsightAnswer = await ask2("Enable Hindsight long-term memory add-on? (y/N)", "N");
1379
2120
  config.enable_hindsight = hindsightAnswer.toLowerCase() === "y" ? "true" : "false";
1380
2121
  console.log("");
1381
- console.log(chalk6.dim(" \u2500\u2500 Auth \u2500\u2500"));
2122
+ console.log(chalk8.dim(" \u2500\u2500 Auth \u2500\u2500"));
1382
2123
  const useGoogle = await ask2("Enable Google OAuth login? (y/N)", "N");
1383
2124
  if (useGoogle.toLowerCase() === "y") {
1384
2125
  config.google_oauth_client_id = await ask2("Google OAuth Client ID");
@@ -1388,13 +2129,13 @@ function registerInitCommand(program2) {
1388
2129
  config.google_oauth_client_secret = "";
1389
2130
  }
1390
2131
  console.log("");
1391
- console.log(chalk6.dim(" \u2500\u2500 Frontend URLs \u2500\u2500"));
2132
+ console.log(chalk8.dim(" \u2500\u2500 Frontend URLs \u2500\u2500"));
1392
2133
  config.admin_url = await ask2("Admin UI URL", "http://localhost:5174");
1393
2134
  config.mobile_scheme = await ask2("Mobile app URL scheme", "thinkwork");
1394
2135
  console.log("");
1395
- console.log(chalk6.dim(" \u2500\u2500 Secrets (auto-generated) \u2500\u2500"));
1396
- console.log(chalk6.dim(` DB password: ${config.db_password.slice(0, 8)}...`));
1397
- console.log(chalk6.dim(` API auth secret: ${config.api_auth_secret.slice(0, 16)}...`));
2136
+ console.log(chalk8.dim(" \u2500\u2500 Secrets (auto-generated) \u2500\u2500"));
2137
+ console.log(chalk8.dim(` DB password: ${config.db_password.slice(0, 8)}...`));
2138
+ console.log(chalk8.dim(` API auth secret: ${config.api_auth_secret.slice(0, 16)}...`));
1398
2139
  }
1399
2140
  console.log("");
1400
2141
  console.log(" Scaffolding Terraform modules...");
@@ -1599,22 +2340,22 @@ output "agentcore_memory_id" {
1599
2340
  }
1600
2341
  `);
1601
2342
  }
1602
- console.log(` Wrote ${chalk6.cyan(tfDir + "/")}`);
2343
+ console.log(` Wrote ${chalk8.cyan(tfDir + "/")}`);
1603
2344
  console.log("");
1604
- 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"));
1605
- console.log(` ${chalk6.bold("Stage:")} ${config.stage}`);
1606
- console.log(` ${chalk6.bold("Region:")} ${config.region}`);
1607
- console.log(` ${chalk6.bold("Account:")} ${config.account_id}`);
1608
- console.log(` ${chalk6.bold("Database:")} ${config.database_engine}`);
1609
- console.log(` ${chalk6.bold("Memory:")} managed (always on)${config.enable_hindsight === "true" ? " + hindsight" : ""}`);
1610
- console.log(` ${chalk6.bold("Google OAuth:")} ${config.google_oauth_client_id ? "enabled" : "disabled"}`);
1611
- console.log(` ${chalk6.bold("Directory:")} ${tfDir}`);
1612
- 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"));
2345
+ console.log(chalk8.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"));
2346
+ console.log(` ${chalk8.bold("Stage:")} ${config.stage}`);
2347
+ console.log(` ${chalk8.bold("Region:")} ${config.region}`);
2348
+ console.log(` ${chalk8.bold("Account:")} ${config.account_id}`);
2349
+ console.log(` ${chalk8.bold("Database:")} ${config.database_engine}`);
2350
+ console.log(` ${chalk8.bold("Memory:")} managed (always on)${config.enable_hindsight === "true" ? " + hindsight" : ""}`);
2351
+ console.log(` ${chalk8.bold("Google OAuth:")} ${config.google_oauth_client_id ? "enabled" : "disabled"}`);
2352
+ console.log(` ${chalk8.bold("Directory:")} ${tfDir}`);
2353
+ console.log(chalk8.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"));
1613
2354
  console.log("\n Initializing Terraform...\n");
1614
2355
  try {
1615
- execSync5("terraform init", { cwd: tfDir, stdio: "inherit" });
2356
+ execSync7("terraform init", { cwd: tfDir, stdio: "inherit" });
1616
2357
  } catch {
1617
- printWarning("Terraform init failed. Run `thinkwork doctor -s " + opts.stage + "` to check prerequisites.");
2358
+ printWarning("Terraform init failed. Run `thinkwork doctor -s " + stage + "` to check prerequisites.");
1618
2359
  return;
1619
2360
  }
1620
2361
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -1628,27 +2369,27 @@ output "agentcore_memory_id" {
1628
2369
  createdAt: now,
1629
2370
  updatedAt: now
1630
2371
  });
1631
- printSuccess(`Environment "${opts.stage}" initialized`);
2372
+ printSuccess(`Environment "${stage}" initialized`);
1632
2373
  console.log("");
1633
2374
  console.log(" Next steps:");
1634
- console.log(` ${chalk6.cyan("1.")} thinkwork plan -s ${opts.stage} ${chalk6.dim("# Review infrastructure plan")}`);
1635
- console.log(` ${chalk6.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk6.dim("# Deploy to AWS (~5 min)")}`);
1636
- console.log(` ${chalk6.cyan("3.")} thinkwork bootstrap -s ${opts.stage} ${chalk6.dim("# Seed workspace files + skills")}`);
1637
- console.log(` ${chalk6.cyan("4.")} thinkwork outputs -s ${opts.stage} ${chalk6.dim("# Show API URL, Cognito IDs, etc.")}`);
2375
+ console.log(` ${chalk8.cyan("1.")} thinkwork plan -s ${stage} ${chalk8.dim("# Review infrastructure plan")}`);
2376
+ console.log(` ${chalk8.cyan("2.")} thinkwork deploy -s ${stage} ${chalk8.dim("# Deploy to AWS (~5 min)")}`);
2377
+ console.log(` ${chalk8.cyan("3.")} thinkwork bootstrap -s ${stage} ${chalk8.dim("# Seed workspace files + skills")}`);
2378
+ console.log(` ${chalk8.cyan("4.")} thinkwork outputs -s ${stage} ${chalk8.dim("# Show API URL, Cognito IDs, etc.")}`);
1638
2379
  console.log("");
1639
2380
  });
1640
2381
  }
1641
2382
 
1642
2383
  // src/commands/status.ts
1643
- import { execSync as execSync6 } from "child_process";
1644
- import chalk7 from "chalk";
2384
+ import { execSync as execSync8 } from "child_process";
2385
+ import chalk9 from "chalk";
1645
2386
  function link(url, label) {
1646
2387
  const text = label || url;
1647
2388
  return `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
1648
2389
  }
1649
- function runAws(cmd) {
2390
+ function runAws3(cmd) {
1650
2391
  try {
1651
- return execSync6(`aws ${cmd}`, {
2392
+ return execSync8(`aws ${cmd}`, {
1652
2393
  encoding: "utf-8",
1653
2394
  timeout: 15e3,
1654
2395
  stdio: ["pipe", "pipe", "pipe"]
@@ -1659,7 +2400,7 @@ function runAws(cmd) {
1659
2400
  }
1660
2401
  function discoverAwsStages(region) {
1661
2402
  const stages = /* @__PURE__ */ new Map();
1662
- const raw = runAws(
2403
+ const raw = runAws3(
1663
2404
  `lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
1664
2405
  );
1665
2406
  if (!raw) return stages;
@@ -1674,38 +2415,38 @@ function discoverAwsStages(region) {
1674
2415
  for (const [stage, info] of stages) {
1675
2416
  const count = functions.filter((f) => f.startsWith(`thinkwork-${stage}-`)).length;
1676
2417
  info.lambdaCount = count;
1677
- const apiRaw = runAws(
2418
+ const apiRaw = runAws3(
1678
2419
  `apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
1679
2420
  );
1680
2421
  if (apiRaw && apiRaw !== "None") info.apiEndpoint = apiRaw;
1681
- const appsyncRaw = runAws(
2422
+ const appsyncRaw = runAws3(
1682
2423
  `appsync list-graphql-apis --region ${region} --query "graphqlApis[?name=='thinkwork-${stage}-subscriptions'].uris.REALTIME|[0]" --output text`
1683
2424
  );
1684
2425
  if (appsyncRaw && appsyncRaw !== "None") info.appsyncUrl = appsyncRaw;
1685
- const appsyncApiRaw = runAws(
2426
+ const appsyncApiRaw = runAws3(
1686
2427
  `appsync list-graphql-apis --region ${region} --query "graphqlApis[?name=='thinkwork-${stage}-subscriptions'].uris.GRAPHQL|[0]" --output text`
1687
2428
  );
1688
2429
  if (appsyncApiRaw && appsyncApiRaw !== "None") info.appsyncApiUrl = appsyncApiRaw;
1689
- const acRaw = runAws(
2430
+ const acRaw = runAws3(
1690
2431
  `lambda get-function --function-name thinkwork-${stage}-agentcore --region ${region} --query "Configuration.State" --output text 2>/dev/null`
1691
2432
  );
1692
2433
  info.agentcoreStatus = acRaw || "not deployed";
1693
- const bucketRaw = runAws(
2434
+ const bucketRaw = runAws3(
1694
2435
  `s3api head-bucket --bucket thinkwork-${stage}-storage --region ${region} 2>/dev/null && echo "exists"`
1695
2436
  );
1696
2437
  info.bucketName = bucketRaw ? `thinkwork-${stage}-storage` : void 0;
1697
- const ecsRaw = runAws(
2438
+ const ecsRaw = runAws3(
1698
2439
  `ecs describe-services --cluster thinkwork-${stage}-cluster --services thinkwork-${stage}-hindsight --region ${region} --query "services[0].runningCount" --output text 2>/dev/null`
1699
2440
  );
1700
2441
  if (ecsRaw && ecsRaw !== "None" && ecsRaw !== "0") {
1701
2442
  info.hindsightEnabled = true;
1702
- const albRaw = runAws(
2443
+ const albRaw = runAws3(
1703
2444
  `elbv2 describe-load-balancers --region ${region} --query "LoadBalancers[?contains(LoadBalancerName, 'tw-${stage}-hindsight')].DNSName|[0]" --output text`
1704
2445
  );
1705
2446
  if (albRaw && albRaw !== "None") {
1706
2447
  info.hindsightEndpoint = `http://${albRaw}`;
1707
2448
  try {
1708
- const health = execSync6(`curl -s --max-time 3 http://${albRaw}/health`, { encoding: "utf-8" }).trim();
2449
+ const health = execSync8(`curl -s --max-time 3 http://${albRaw}/health`, { encoding: "utf-8" }).trim();
1709
2450
  info.hindsightHealth = health.includes("healthy") ? "healthy" : "unhealthy";
1710
2451
  } catch {
1711
2452
  info.hindsightHealth = "unreachable";
@@ -1714,15 +2455,15 @@ function discoverAwsStages(region) {
1714
2455
  } else {
1715
2456
  info.hindsightEnabled = false;
1716
2457
  }
1717
- const dbRaw = runAws(
2458
+ const dbRaw = runAws3(
1718
2459
  `rds describe-db-clusters --region ${region} --query "DBClusters[?starts_with(DBClusterIdentifier, 'thinkwork-${stage}')].Endpoint|[0]" --output text`
1719
2460
  );
1720
2461
  if (dbRaw && dbRaw !== "None") info.dbEndpoint = dbRaw;
1721
- const ecrRaw = runAws(
2462
+ const ecrRaw = runAws3(
1722
2463
  `ecr describe-repositories --region ${region} --query "repositories[?repositoryName=='thinkwork-${stage}-agentcore'].repositoryUri|[0]" --output text`
1723
2464
  );
1724
2465
  if (ecrRaw && ecrRaw !== "None") info.ecrUrl = ecrRaw;
1725
- const cfJson = runAws(
2466
+ const cfJson = runAws3(
1726
2467
  `cloudfront list-distributions --query "DistributionList.Items[?contains(Origins.Items[0].DomainName, 'thinkwork-${stage}-')].{Origin:Origins.Items[0].DomainName,Domain:DomainName}" --output json`
1727
2468
  );
1728
2469
  if (cfJson) {
@@ -1739,33 +2480,33 @@ function discoverAwsStages(region) {
1739
2480
  return stages;
1740
2481
  }
1741
2482
  function printStageDetail(info) {
1742
- console.log(chalk7.bold.cyan(` \u2B21 ${info.stage}`));
1743
- console.log(chalk7.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"));
1744
- console.log(` ${chalk7.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
1745
- console.log(` ${chalk7.bold("Region:")} ${info.region}`);
1746
- console.log(` ${chalk7.bold("Account:")} ${info.accountId}`);
1747
- console.log(` ${chalk7.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
1748
- console.log(` ${chalk7.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
1749
- console.log(` ${chalk7.bold("Memory:")} managed (always on)`);
2483
+ console.log(chalk9.bold.cyan(` \u2B21 ${info.stage}`));
2484
+ console.log(chalk9.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"));
2485
+ console.log(` ${chalk9.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
2486
+ console.log(` ${chalk9.bold("Region:")} ${info.region}`);
2487
+ console.log(` ${chalk9.bold("Account:")} ${info.accountId}`);
2488
+ console.log(` ${chalk9.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
2489
+ console.log(` ${chalk9.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
2490
+ console.log(` ${chalk9.bold("Memory:")} managed (always on)`);
1750
2491
  const hindsightLabel = info.hindsightEnabled === void 0 ? "unknown" : info.hindsightEnabled ? info.hindsightHealth || "running" : "disabled";
1751
- console.log(` ${chalk7.bold("Hindsight:")} ${hindsightLabel}`);
1752
- if (info.bucketName) console.log(` ${chalk7.bold("S3 bucket:")} ${info.bucketName}`);
1753
- if (info.dbEndpoint) console.log(` ${chalk7.bold("Database:")} ${info.dbEndpoint}`);
1754
- if (info.ecrUrl) console.log(` ${chalk7.bold("ECR:")} ${info.ecrUrl}`);
2492
+ console.log(` ${chalk9.bold("Hindsight:")} ${hindsightLabel}`);
2493
+ if (info.bucketName) console.log(` ${chalk9.bold("S3 bucket:")} ${info.bucketName}`);
2494
+ if (info.dbEndpoint) console.log(` ${chalk9.bold("Database:")} ${info.dbEndpoint}`);
2495
+ if (info.ecrUrl) console.log(` ${chalk9.bold("ECR:")} ${info.ecrUrl}`);
1755
2496
  console.log("");
1756
- console.log(chalk7.bold(" URLs:"));
2497
+ console.log(chalk9.bold(" URLs:"));
1757
2498
  if (info.adminUrl) console.log(` Admin: ${link(info.adminUrl)}`);
1758
2499
  if (info.docsUrl) console.log(` Docs: ${link(info.docsUrl)}`);
1759
2500
  if (info.apiEndpoint) console.log(` API: ${link(info.apiEndpoint)}`);
1760
2501
  if (info.appsyncApiUrl) console.log(` AppSync: ${link(info.appsyncApiUrl)}`);
1761
2502
  if (info.appsyncUrl) console.log(` WebSocket: ${link(info.appsyncUrl)}`);
1762
2503
  if (info.hindsightEndpoint) console.log(` Hindsight: ${link(info.hindsightEndpoint)}`);
1763
- console.log(chalk7.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"));
2504
+ console.log(chalk9.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"));
1764
2505
  const local = loadEnvironment(info.stage);
1765
2506
  if (local) {
1766
- console.log(chalk7.dim(` Terraform dir: ${local.terraformDir}`));
2507
+ console.log(chalk9.dim(` Terraform dir: ${local.terraformDir}`));
1767
2508
  } else {
1768
- console.log(chalk7.dim(` No local config. Run: thinkwork init -s ${info.stage}`));
2509
+ console.log(chalk9.dim(` No local config. Run: thinkwork init -s ${info.stage}`));
1769
2510
  }
1770
2511
  console.log("");
1771
2512
  }
@@ -1802,7 +2543,7 @@ and AgentCore for per-stage detail.
1802
2543
  printError("AWS credentials not configured. Run `thinkwork login` first.");
1803
2544
  process.exit(1);
1804
2545
  }
1805
- console.log(chalk7.dim(" Scanning AWS account for Thinkwork deployments...\n"));
2546
+ console.log(chalk9.dim(" Scanning AWS account for Thinkwork deployments...\n"));
1806
2547
  const awsStages = discoverAwsStages(opts.region);
1807
2548
  const localEnvs = listEnvironments();
1808
2549
  const merged = /* @__PURE__ */ new Map();
@@ -1837,7 +2578,7 @@ and AgentCore for per-stage detail.
1837
2578
  }
1838
2579
  if (merged.size === 0) {
1839
2580
  console.log(" No Thinkwork environments found.");
1840
- console.log(` Run ${chalk7.cyan("thinkwork init -s <stage>")} to create one.`);
2581
+ console.log(` Run ${chalk9.cyan("thinkwork init -s <stage>")} to create one.`);
1841
2582
  console.log("");
1842
2583
  return;
1843
2584
  }
@@ -1848,55 +2589,16 @@ and AgentCore for per-stage detail.
1848
2589
  }
1849
2590
 
1850
2591
  // src/commands/mcp.ts
1851
- import chalk8 from "chalk";
2592
+ import chalk10 from "chalk";
1852
2593
 
1853
2594
  // src/api-client.ts
1854
2595
  import { readFileSync as readFileSync5, existsSync as existsSync8 } from "fs";
1855
- import { execSync as execSync8 } from "child_process";
1856
-
1857
- // src/aws-discovery.ts
1858
- import { execSync as execSync7 } from "child_process";
1859
- function runAws2(cmd) {
1860
- try {
1861
- return execSync7(`aws ${cmd}`, {
1862
- encoding: "utf-8",
1863
- timeout: 15e3,
1864
- stdio: ["pipe", "pipe", "pipe"]
1865
- }).trim();
1866
- } catch {
1867
- return null;
1868
- }
1869
- }
1870
- function listDeployedStages(region) {
1871
- const raw = runAws2(
1872
- `lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
1873
- );
1874
- if (!raw) return [];
1875
- try {
1876
- const functions = JSON.parse(raw);
1877
- const stages = /* @__PURE__ */ new Set();
1878
- for (const fn of functions) {
1879
- const m = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
1880
- if (m) stages.add(m[1]);
1881
- }
1882
- return [...stages].sort();
1883
- } catch {
1884
- return [];
1885
- }
1886
- }
1887
- function getApiAuthSecretFromLambda(stage, region) {
1888
- const raw = runAws2(
1889
- `lambda get-function-configuration --function-name thinkwork-${stage}-api-tenants --region ${region} --query "Environment.Variables.API_AUTH_SECRET" --output text`
1890
- );
1891
- return raw && raw !== "None" ? raw : null;
1892
- }
1893
-
1894
- // src/api-client.ts
1895
- function readTfVar2(tfvarsPath, key) {
1896
- if (!existsSync8(tfvarsPath)) return null;
1897
- const content = readFileSync5(tfvarsPath, "utf-8");
1898
- const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
1899
- return match ? match[1] : null;
2596
+ import { execSync as execSync9 } from "child_process";
2597
+ function readTfVar2(tfvarsPath, key) {
2598
+ if (!existsSync8(tfvarsPath)) return null;
2599
+ const content = readFileSync5(tfvarsPath, "utf-8");
2600
+ const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
2601
+ return match ? match[1] : null;
1900
2602
  }
1901
2603
  function resolveTfvarsPath2(stage) {
1902
2604
  const tfDir = resolveTerraformDir(stage);
@@ -1908,9 +2610,9 @@ function resolveTfvarsPath2(stage) {
1908
2610
  const cwd = resolveTierDir(terraformDir, stage, "app");
1909
2611
  return `${cwd}/terraform.tfvars`;
1910
2612
  }
1911
- function getApiEndpoint(stage, region) {
2613
+ function getApiEndpoint2(stage, region) {
1912
2614
  try {
1913
- const raw = execSync8(
2615
+ const raw = execSync9(
1914
2616
  `aws apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`,
1915
2617
  { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
1916
2618
  ).trim();
@@ -1953,7 +2655,7 @@ function resolveApiConfig(stage, regionOverride) {
1953
2655
  const tfAuthSecret = readTfVar2(tfvarsPath, "api_auth_secret");
1954
2656
  const tfRegion = readTfVar2(tfvarsPath, "region");
1955
2657
  const region = regionOverride || tfRegion || "us-east-1";
1956
- const apiUrl = getApiEndpoint(stage, region);
2658
+ const apiUrl = getApiEndpoint2(stage, region);
1957
2659
  if (!apiUrl) {
1958
2660
  printError(
1959
2661
  `Cannot discover API endpoint for stage "${stage}" in ${region}. Is the stack deployed?`
@@ -1970,108 +2672,232 @@ function resolveApiConfig(stage, regionOverride) {
1970
2672
  return { apiUrl, authSecret };
1971
2673
  }
1972
2674
 
2675
+ // src/lib/resolve-tenant.ts
2676
+ import { select as select4 } from "@inquirer/prompts";
2677
+ async function resolveTenant(opts) {
2678
+ const override = opts.flag ?? process.env.THINKWORK_TENANT;
2679
+ if (override) {
2680
+ const cached = loadStageSession(opts.stage);
2681
+ if (cached && cached.tenantSlug === override) {
2682
+ return { slug: override, id: cached.tenantId };
2683
+ }
2684
+ return { slug: override };
2685
+ }
2686
+ const session = loadStageSession(opts.stage);
2687
+ if (session?.tenantSlug) {
2688
+ return { slug: session.tenantSlug, id: session.tenantId };
2689
+ }
2690
+ if (!opts.listTenants) {
2691
+ printError(
2692
+ `No tenant resolved for stage "${opts.stage}". Pass --tenant <slug>, set THINKWORK_TENANT, or re-run \`thinkwork login --stage ${opts.stage}\`.`
2693
+ );
2694
+ process.exit(1);
2695
+ }
2696
+ const tenants = await opts.listTenants();
2697
+ if (tenants.length === 0) {
2698
+ printError(
2699
+ "No tenants available. You may need to be invited to a workspace first."
2700
+ );
2701
+ process.exit(1);
2702
+ }
2703
+ if (tenants.length === 1) {
2704
+ const only = tenants[0];
2705
+ console.log(` Using the only tenant: ${only.name} (${only.slug})`);
2706
+ cacheTenant(opts.stage, only);
2707
+ return { slug: only.slug, id: only.id };
2708
+ }
2709
+ requireTty("Tenant");
2710
+ const slug = await select4({
2711
+ message: "Which tenant?",
2712
+ choices: tenants.map((t) => ({
2713
+ name: `${t.name} (slug: ${t.slug})`,
2714
+ value: t.slug
2715
+ })),
2716
+ loop: false
2717
+ });
2718
+ const picked = tenants.find((t) => t.slug === slug);
2719
+ cacheTenant(opts.stage, picked);
2720
+ return { slug: picked.slug, id: picked.id };
2721
+ }
2722
+ function cacheTenant(stage, tenant) {
2723
+ const session = loadStageSession(stage);
2724
+ if (!session) return;
2725
+ saveStageSession(stage, { ...session, tenantId: tenant.id, tenantSlug: tenant.slug });
2726
+ }
2727
+
2728
+ // src/lib/resolve-tenant-rest.ts
2729
+ async function resolveTenantRest(opts) {
2730
+ return resolveTenant({
2731
+ flag: opts.flag,
2732
+ stage: opts.stage,
2733
+ listTenants: async () => {
2734
+ const list = await apiFetch(
2735
+ opts.apiUrl,
2736
+ opts.authSecret,
2737
+ "/api/tenants"
2738
+ );
2739
+ return list;
2740
+ }
2741
+ });
2742
+ }
2743
+
1973
2744
  // src/commands/mcp.ts
2745
+ async function resolveMcpContext(opts) {
2746
+ const stage = await resolveStage({ flag: opts.stage });
2747
+ const api = resolveApiConfig(stage);
2748
+ if (!api) process.exit(1);
2749
+ const tenant = await resolveTenantRest({
2750
+ flag: opts.tenant,
2751
+ stage,
2752
+ apiUrl: api.apiUrl,
2753
+ authSecret: api.authSecret
2754
+ });
2755
+ return { stage, api, tenant };
2756
+ }
1974
2757
  function registerMcpCommand(program2) {
1975
2758
  const mcp = program2.command("mcp").description("Manage MCP servers for your tenant");
1976
- mcp.command("list").description("List registered MCP servers").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
1977
- const check = validateStage(opts.stage);
1978
- if (!check.valid) {
1979
- printError(check.error);
1980
- process.exit(1);
1981
- }
1982
- const api = resolveApiConfig(opts.stage);
1983
- if (!api) process.exit(1);
1984
- printHeader("mcp list", opts.stage);
2759
+ mcp.command("list").alias("ls").description("List registered MCP servers. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
2760
+ "after",
2761
+ `
2762
+ Examples:
2763
+ # Interactive \u2014 picks stage + tenant from the deployed ones
2764
+ $ thinkwork mcp list
2765
+
2766
+ # Scriptable
2767
+ $ thinkwork mcp list -s dev -t acme
2768
+ $ thinkwork mcp list -s prod -t acme --json | jq '.[].slug'
2769
+ `
2770
+ ).action(async (opts) => {
1985
2771
  try {
1986
- const { servers } = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {}, { "x-tenant-slug": opts.tenant });
2772
+ const { stage, api, tenant } = await resolveMcpContext(opts);
2773
+ printHeader("mcp list", stage);
2774
+ const { servers } = await apiFetch(
2775
+ api.apiUrl,
2776
+ api.authSecret,
2777
+ "/api/skills/mcp-servers",
2778
+ {},
2779
+ { "x-tenant-slug": tenant.slug }
2780
+ );
1987
2781
  if (!servers || servers.length === 0) {
1988
- console.log(chalk8.dim(" No MCP servers registered."));
2782
+ console.log(chalk10.dim(" No MCP servers registered."));
1989
2783
  return;
1990
2784
  }
1991
2785
  console.log("");
1992
2786
  for (const s of servers) {
1993
- const status = s.enabled ? chalk8.green("enabled") : chalk8.dim("disabled");
2787
+ const status = s.enabled ? chalk10.green("enabled") : chalk10.dim("disabled");
1994
2788
  const authLabel = s.authType === "per_user_oauth" ? `OAuth (${s.oauthProvider})` : s.authType === "tenant_api_key" ? "API Key" : "none";
1995
- console.log(` ${chalk8.bold(s.name)} ${chalk8.dim(s.slug)} ${status}`);
2789
+ console.log(` ${chalk10.bold(s.name)} ${chalk10.dim(s.slug)} ${status}`);
1996
2790
  console.log(` URL: ${s.url}`);
1997
2791
  console.log(` Transport: ${s.transport}`);
1998
2792
  console.log(` Auth: ${authLabel}`);
1999
- if (s.tools?.length) {
2000
- console.log(` Tools: ${s.tools.length} cached`);
2001
- }
2793
+ if (s.tools?.length) console.log(` Tools: ${s.tools.length} cached`);
2002
2794
  console.log("");
2003
2795
  }
2004
2796
  } catch (err) {
2005
- printError(err.message);
2006
- process.exit(1);
2007
- }
2008
- });
2009
- mcp.command("add <name>").description("Register an MCP server").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").requiredOption("--url <url>", "MCP server URL").option("--transport <type>", "Transport type (streamable-http|sse)", "streamable-http").option("--auth-type <type>", "Auth type (none|tenant_api_key|per_user_oauth)", "none").option("--api-key <token>", "API key (for tenant_api_key auth)").option("--oauth-provider <name>", "OAuth provider name (for per_user_oauth auth)").action(async (name, opts) => {
2010
- const check = validateStage(opts.stage);
2011
- if (!check.valid) {
2012
- printError(check.error);
2013
- process.exit(1);
2014
- }
2015
- const api = resolveApiConfig(opts.stage);
2016
- if (!api) process.exit(1);
2017
- const body = {
2018
- name,
2019
- url: opts.url,
2020
- transport: opts.transport
2021
- };
2022
- if (opts.authType !== "none") body.authType = opts.authType;
2023
- if (opts.apiKey) body.apiKey = opts.apiKey;
2024
- if (opts.oauthProvider) body.oauthProvider = opts.oauthProvider;
2025
- try {
2026
- const result = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {
2027
- method: "POST",
2028
- body: JSON.stringify(body)
2029
- }, { "x-tenant-slug": opts.tenant });
2030
- printSuccess(`MCP server "${name}" ${result.created ? "registered" : "updated"} (slug: ${result.slug})`);
2031
- } catch (err) {
2032
- printError(err.message);
2797
+ if (isCancellation(err)) return;
2798
+ printError(err instanceof Error ? err.message : String(err));
2033
2799
  process.exit(1);
2034
2800
  }
2035
2801
  });
2036
- mcp.command("remove <id>").description("Remove an MCP server").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (id, opts) => {
2037
- const check = validateStage(opts.stage);
2038
- if (!check.valid) {
2039
- printError(check.error);
2040
- process.exit(1);
2802
+ mcp.command("add [name]").description("Register an MCP server. Prompts for missing fields in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--url <url>", "MCP server URL").option("--transport <type>", "Transport type (streamable-http|sse)", "streamable-http").option("--auth-type <type>", "Auth type (none|tenant_api_key|per_user_oauth)", "none").option("--api-key <token>", "API key (for tenant_api_key auth)").option("--oauth-provider <name>", "OAuth provider name (for per_user_oauth auth)").addHelpText(
2803
+ "after",
2804
+ `
2805
+ Examples:
2806
+ # Fully interactive \u2014 prompts for name, URL, and auth
2807
+ $ thinkwork mcp add
2808
+
2809
+ # Scripted \u2014 API-key auth
2810
+ $ thinkwork mcp add my-tools --url https://mcp.example.com/crm \\
2811
+ --auth-type tenant_api_key --api-key sk-abc -s dev -t acme
2812
+
2813
+ # OAuth connector (users connect from the mobile app)
2814
+ $ thinkwork mcp add lastmile --url https://mcp-dev.lastmile-tei.com/crm \\
2815
+ --auth-type per_user_oauth --oauth-provider lastmile -s dev -t acme
2816
+ `
2817
+ ).action(
2818
+ async (nameArg, opts) => {
2819
+ try {
2820
+ const { input: input3 } = await import("@inquirer/prompts");
2821
+ const { stage, api, tenant } = await resolveMcpContext(opts);
2822
+ let name = nameArg;
2823
+ if (!name) {
2824
+ if (!process.stdin.isTTY) {
2825
+ printError("Name is required. Pass it as a positional arg.");
2826
+ process.exit(1);
2827
+ }
2828
+ name = await input3({ message: "Server name:" });
2829
+ }
2830
+ let url = opts.url;
2831
+ if (!url) {
2832
+ if (!process.stdin.isTTY) {
2833
+ printError("--url is required. Pass it as a flag.");
2834
+ process.exit(1);
2835
+ }
2836
+ url = await input3({
2837
+ message: "MCP server URL:",
2838
+ validate: (v) => v.startsWith("http://") || v.startsWith("https://") ? true : "URL must start with http:// or https://"
2839
+ });
2840
+ }
2841
+ const body = { name, url, transport: opts.transport };
2842
+ if (opts.authType !== "none") body.authType = opts.authType;
2843
+ if (opts.apiKey) body.apiKey = opts.apiKey;
2844
+ if (opts.oauthProvider) body.oauthProvider = opts.oauthProvider;
2845
+ printHeader("mcp add", stage);
2846
+ const result = await apiFetch(
2847
+ api.apiUrl,
2848
+ api.authSecret,
2849
+ "/api/skills/mcp-servers",
2850
+ { method: "POST", body: JSON.stringify(body) },
2851
+ { "x-tenant-slug": tenant.slug }
2852
+ );
2853
+ printSuccess(
2854
+ `MCP server "${name}" ${result.created ? "registered" : "updated"} (slug: ${result.slug})`
2855
+ );
2856
+ } catch (err) {
2857
+ if (isCancellation(err)) return;
2858
+ printError(err instanceof Error ? err.message : String(err));
2859
+ process.exit(1);
2860
+ }
2041
2861
  }
2042
- const api = resolveApiConfig(opts.stage);
2043
- if (!api) process.exit(1);
2862
+ );
2863
+ mcp.command("remove <id>").alias("rm").description("Remove an MCP server. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (id, opts) => {
2044
2864
  try {
2045
- await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}`, {
2046
- method: "DELETE"
2047
- }, { "x-tenant-slug": opts.tenant });
2048
- printSuccess(`MCP server removed.`);
2865
+ const { api, tenant } = await resolveMcpContext(opts);
2866
+ await apiFetch(
2867
+ api.apiUrl,
2868
+ api.authSecret,
2869
+ `/api/skills/mcp-servers/${id}`,
2870
+ { method: "DELETE" },
2871
+ { "x-tenant-slug": tenant.slug }
2872
+ );
2873
+ printSuccess("MCP server removed.");
2049
2874
  } catch (err) {
2050
- printError(err.message);
2875
+ if (isCancellation(err)) return;
2876
+ printError(err instanceof Error ? err.message : String(err));
2051
2877
  process.exit(1);
2052
2878
  }
2053
2879
  });
2054
- mcp.command("test <id>").description("Test connection and discover tools").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (id, opts) => {
2055
- const check = validateStage(opts.stage);
2056
- if (!check.valid) {
2057
- printError(check.error);
2058
- process.exit(1);
2059
- }
2060
- const api = resolveApiConfig(opts.stage);
2061
- if (!api) process.exit(1);
2062
- printHeader("mcp test", opts.stage);
2880
+ mcp.command("test <id>").description("Test connection and discover tools. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (id, opts) => {
2063
2881
  try {
2064
- const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}/test`, {
2065
- method: "POST"
2066
- }, { "x-tenant-slug": opts.tenant });
2882
+ const { stage, api, tenant } = await resolveMcpContext(opts);
2883
+ printHeader("mcp test", stage);
2884
+ const result = await apiFetch(
2885
+ api.apiUrl,
2886
+ api.authSecret,
2887
+ `/api/skills/mcp-servers/${id}/test`,
2888
+ { method: "POST" },
2889
+ { "x-tenant-slug": tenant.slug }
2890
+ );
2067
2891
  if (result.ok) {
2068
2892
  printSuccess("Connection successful.");
2069
2893
  if (result.tools?.length) {
2070
- console.log(chalk8.bold(`
2894
+ console.log(chalk10.bold(`
2071
2895
  Discovered tools (${result.tools.length}):
2072
2896
  `));
2073
2897
  for (const t of result.tools) {
2074
- console.log(` ${chalk8.cyan(t.name)}${t.description ? chalk8.dim(` - ${t.description}`) : ""}`);
2898
+ console.log(
2899
+ ` ${chalk10.cyan(t.name)}${t.description ? chalk10.dim(` - ${t.description}`) : ""}`
2900
+ );
2075
2901
  }
2076
2902
  console.log("");
2077
2903
  } else {
@@ -2082,165 +2908,200 @@ function registerMcpCommand(program2) {
2082
2908
  process.exit(1);
2083
2909
  }
2084
2910
  } catch (err) {
2085
- printError(err.message);
2086
- process.exit(1);
2087
- }
2088
- });
2089
- mcp.command("assign <mcpServerId>").description("Assign an MCP server to an agent").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--agent <id>", "Agent ID").action(async (mcpServerId, opts) => {
2090
- const check = validateStage(opts.stage);
2091
- if (!check.valid) {
2092
- printError(check.error);
2093
- process.exit(1);
2094
- }
2095
- const api = resolveApiConfig(opts.stage);
2096
- if (!api) process.exit(1);
2097
- try {
2098
- const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agents/${opts.agent}/mcp-servers`, {
2099
- method: "POST",
2100
- body: JSON.stringify({ mcpServerId })
2101
- });
2102
- printSuccess(`MCP server assigned to agent. (${result.created ? "new" : "updated"})`);
2103
- } catch (err) {
2104
- printError(err.message);
2911
+ if (isCancellation(err)) return;
2912
+ printError(err instanceof Error ? err.message : String(err));
2105
2913
  process.exit(1);
2106
2914
  }
2107
2915
  });
2108
- mcp.command("unassign <mcpServerId>").description("Remove an MCP server from an agent").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--agent <id>", "Agent ID").action(async (mcpServerId, opts) => {
2109
- const check = validateStage(opts.stage);
2110
- if (!check.valid) {
2111
- printError(check.error);
2112
- process.exit(1);
2916
+ mcp.command("assign <mcpServerId>").description("Assign an MCP server to an agent. Prompts for stage/agent when omitted in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
2917
+ async (mcpServerId, opts) => {
2918
+ try {
2919
+ const { input: input3 } = await import("@inquirer/prompts");
2920
+ const { api } = await resolveMcpContext(opts);
2921
+ let agent = opts.agent;
2922
+ if (!agent) {
2923
+ if (!process.stdin.isTTY) {
2924
+ printError("--agent is required. Pass it as a flag.");
2925
+ process.exit(1);
2926
+ }
2927
+ agent = await input3({ message: "Agent ID:" });
2928
+ }
2929
+ const result = await apiFetch(
2930
+ api.apiUrl,
2931
+ api.authSecret,
2932
+ `/api/skills/agents/${agent}/mcp-servers`,
2933
+ { method: "POST", body: JSON.stringify({ mcpServerId }) }
2934
+ );
2935
+ printSuccess(`MCP server assigned to agent. (${result.created ? "new" : "updated"})`);
2936
+ } catch (err) {
2937
+ if (isCancellation(err)) return;
2938
+ printError(err instanceof Error ? err.message : String(err));
2939
+ process.exit(1);
2940
+ }
2113
2941
  }
2114
- const api = resolveApiConfig(opts.stage);
2115
- if (!api) process.exit(1);
2116
- try {
2117
- await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agents/${opts.agent}/mcp-servers/${mcpServerId}`, {
2118
- method: "DELETE"
2119
- });
2120
- printSuccess("MCP server unassigned from agent.");
2121
- } catch (err) {
2122
- printError(err.message);
2123
- process.exit(1);
2942
+ );
2943
+ mcp.command("unassign <mcpServerId>").description("Remove an MCP server from an agent. Prompts for stage/agent when omitted in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
2944
+ async (mcpServerId, opts) => {
2945
+ try {
2946
+ const { input: input3 } = await import("@inquirer/prompts");
2947
+ const { api } = await resolveMcpContext(opts);
2948
+ let agent = opts.agent;
2949
+ if (!agent) {
2950
+ if (!process.stdin.isTTY) {
2951
+ printError("--agent is required. Pass it as a flag.");
2952
+ process.exit(1);
2953
+ }
2954
+ agent = await input3({ message: "Agent ID:" });
2955
+ }
2956
+ await apiFetch(
2957
+ api.apiUrl,
2958
+ api.authSecret,
2959
+ `/api/skills/agents/${agent}/mcp-servers/${mcpServerId}`,
2960
+ { method: "DELETE" }
2961
+ );
2962
+ printSuccess("MCP server unassigned from agent.");
2963
+ } catch (err) {
2964
+ if (isCancellation(err)) return;
2965
+ printError(err instanceof Error ? err.message : String(err));
2966
+ process.exit(1);
2967
+ }
2124
2968
  }
2125
- });
2969
+ );
2126
2970
  }
2127
2971
 
2128
2972
  // src/commands/tools.ts
2129
- import { createInterface as createInterface4 } from "readline";
2130
- import chalk9 from "chalk";
2131
- function prompt(question) {
2132
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
2133
- return new Promise((resolve3) => {
2134
- rl.question(question, (answer) => {
2135
- rl.close();
2136
- resolve3(answer.trim());
2137
- });
2138
- });
2139
- }
2973
+ import { select as select5, password } from "@inquirer/prompts";
2974
+ import chalk11 from "chalk";
2140
2975
  var TOOL_PROVIDERS = {
2141
2976
  "web-search": ["exa", "serpapi"]
2142
2977
  };
2978
+ async function resolveCtx(opts) {
2979
+ const stage = await resolveStage({ flag: opts.stage });
2980
+ const api = resolveApiConfig(stage);
2981
+ if (!api) process.exit(1);
2982
+ const tenant = await resolveTenantRest({
2983
+ flag: opts.tenant,
2984
+ stage,
2985
+ apiUrl: api.apiUrl,
2986
+ authSecret: api.authSecret
2987
+ });
2988
+ return { stage, api, tenant };
2989
+ }
2143
2990
  function registerToolsCommand(program2) {
2144
2991
  const tools = program2.command("tools").description("Configure built-in agent tools (web_search, \u2026) for your tenant");
2145
- tools.command("list").description("List configured built-in tools").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2146
- const check = validateStage(opts.stage);
2147
- if (!check.valid) {
2148
- printError(check.error);
2149
- process.exit(1);
2150
- }
2151
- const api = resolveApiConfig(opts.stage);
2152
- if (!api) process.exit(1);
2153
- printHeader("tools list", opts.stage);
2992
+ tools.command("list").alias("ls").description("List configured built-in tools. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
2993
+ "after",
2994
+ `
2995
+ Examples:
2996
+ # Interactive \u2014 picks stage + tenant
2997
+ $ thinkwork tools list
2998
+
2999
+ # Scripted
3000
+ $ thinkwork tools list -s dev -t acme
3001
+ $ thinkwork tools list --json | jq '.[] | .toolSlug'
3002
+ `
3003
+ ).action(async (opts) => {
2154
3004
  try {
3005
+ const { stage, api, tenant } = await resolveCtx(opts);
3006
+ printHeader("tools list", stage);
2155
3007
  const { tools: rows } = await apiFetch(
2156
3008
  api.apiUrl,
2157
3009
  api.authSecret,
2158
3010
  "/api/skills/builtin-tools",
2159
3011
  {},
2160
- { "x-tenant-slug": opts.tenant }
3012
+ { "x-tenant-slug": tenant.slug }
2161
3013
  );
2162
3014
  if (!rows || rows.length === 0) {
2163
- console.log(chalk9.dim(" No built-in tools configured."));
2164
- console.log(chalk9.dim(" Try: thinkwork tools web-search set --tenant <slug> -s <stage>"));
3015
+ console.log(chalk11.dim(" No built-in tools configured."));
3016
+ console.log(chalk11.dim(" Try: thinkwork tools web-search set"));
2165
3017
  return;
2166
3018
  }
2167
3019
  console.log("");
2168
3020
  for (const r of rows) {
2169
- const status = r.enabled ? chalk9.green("enabled") : chalk9.dim("disabled");
2170
- const key = r.hasSecret ? chalk9.green("yes") : chalk9.red("no");
2171
- const provider = r.provider ?? chalk9.dim("\u2014");
2172
- console.log(` ${chalk9.bold(r.toolSlug)} ${status}`);
3021
+ const status = r.enabled ? chalk11.green("enabled") : chalk11.dim("disabled");
3022
+ const key = r.hasSecret ? chalk11.green("yes") : chalk11.red("no");
3023
+ const provider = r.provider ?? chalk11.dim("\u2014");
3024
+ console.log(` ${chalk11.bold(r.toolSlug)} ${status}`);
2173
3025
  console.log(` Provider: ${provider}`);
2174
3026
  console.log(` Has key: ${key}`);
2175
- if (r.lastTestedAt) {
2176
- console.log(` Tested: ${new Date(r.lastTestedAt).toLocaleString()}`);
2177
- }
3027
+ if (r.lastTestedAt) console.log(` Tested: ${new Date(r.lastTestedAt).toLocaleString()}`);
2178
3028
  console.log("");
2179
3029
  }
2180
3030
  } catch (err) {
2181
- printError(err.message);
3031
+ if (isCancellation(err)) return;
3032
+ printError(err instanceof Error ? err.message : String(err));
2182
3033
  process.exit(1);
2183
3034
  }
2184
3035
  });
2185
3036
  const webSearch = tools.command("web-search").description("Configure the web_search built-in tool");
2186
- webSearch.command("set").description("Set or update web_search provider + API key (enables the tool)").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").option("--provider <name>", `Provider (${TOOL_PROVIDERS["web-search"].join("|")})`).option("--key <key>", "API key").action(async (opts) => {
2187
- const check = validateStage(opts.stage);
2188
- if (!check.valid) {
2189
- printError(check.error);
2190
- process.exit(1);
2191
- }
2192
- const api = resolveApiConfig(opts.stage);
2193
- if (!api) process.exit(1);
2194
- let provider = opts.provider;
2195
- if (!provider) {
2196
- provider = (await prompt(`Provider [${TOOL_PROVIDERS["web-search"].join("/")}]: `)).toLowerCase();
2197
- }
2198
- if (!TOOL_PROVIDERS["web-search"].includes(provider)) {
2199
- printError(`provider must be one of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
2200
- process.exit(1);
2201
- }
2202
- let apiKey = opts.key;
2203
- if (!apiKey) {
2204
- apiKey = await prompt(`${provider} API key: `);
2205
- }
2206
- if (!apiKey) {
2207
- printError("API key is required");
2208
- process.exit(1);
2209
- }
2210
- try {
2211
- await apiFetch(
2212
- api.apiUrl,
2213
- api.authSecret,
2214
- "/api/skills/builtin-tools/web-search",
2215
- {
2216
- method: "PUT",
2217
- body: JSON.stringify({ provider, apiKey, enabled: true })
2218
- },
2219
- { "x-tenant-slug": opts.tenant }
2220
- );
2221
- printSuccess(`web_search configured with provider=${provider}, enabled=true`);
2222
- printWarning("Run `thinkwork tools web-search test` to verify connectivity.");
2223
- } catch (err) {
2224
- printError(err.message);
2225
- process.exit(1);
2226
- }
2227
- });
2228
- webSearch.command("test").description("Test the stored web_search provider + key against the provider API").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2229
- const check = validateStage(opts.stage);
2230
- if (!check.valid) {
2231
- printError(check.error);
2232
- process.exit(1);
3037
+ webSearch.command("set").description("Set or update web_search provider + API key (enables the tool). Prompts when flags are missing in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--provider <name>", `Provider (${TOOL_PROVIDERS["web-search"].join("|")})`).option("--key <key>", "API key (will prompt hidden if omitted)").addHelpText(
3038
+ "after",
3039
+ `
3040
+ Examples:
3041
+ # Fully interactive \u2014 stage/tenant picker, provider picker, hidden key prompt
3042
+ $ thinkwork tools web-search set
3043
+
3044
+ # Scripted (secret from env)
3045
+ $ thinkwork tools web-search set -s dev -t acme --provider exa --key "$EXA_KEY"
3046
+ `
3047
+ ).action(
3048
+ async (opts) => {
3049
+ try {
3050
+ const { api, tenant } = await resolveCtx(opts);
3051
+ let provider = opts.provider;
3052
+ if (!provider) {
3053
+ if (!process.stdin.isTTY) {
3054
+ printError(`--provider is required. One of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
3055
+ process.exit(1);
3056
+ }
3057
+ provider = await select5({
3058
+ message: "Provider:",
3059
+ choices: TOOL_PROVIDERS["web-search"].map((p) => ({ name: p, value: p })),
3060
+ loop: false
3061
+ });
3062
+ }
3063
+ if (!TOOL_PROVIDERS["web-search"].includes(provider)) {
3064
+ printError(`provider must be one of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
3065
+ process.exit(1);
3066
+ }
3067
+ let apiKey = opts.key;
3068
+ if (!apiKey) {
3069
+ if (!process.stdin.isTTY) {
3070
+ printError("--key is required. Pass it as a flag or pipe via env.");
3071
+ process.exit(1);
3072
+ }
3073
+ apiKey = await password({ message: `${provider} API key:`, mask: "*" });
3074
+ }
3075
+ if (!apiKey) {
3076
+ printError("API key is required");
3077
+ process.exit(1);
3078
+ }
3079
+ await apiFetch(
3080
+ api.apiUrl,
3081
+ api.authSecret,
3082
+ "/api/skills/builtin-tools/web-search",
3083
+ { method: "PUT", body: JSON.stringify({ provider, apiKey, enabled: true }) },
3084
+ { "x-tenant-slug": tenant.slug }
3085
+ );
3086
+ printSuccess(`web_search configured with provider=${provider}, enabled=true`);
3087
+ printWarning("Run `thinkwork tools web-search test` to verify connectivity.");
3088
+ } catch (err) {
3089
+ if (isCancellation(err)) return;
3090
+ printError(err instanceof Error ? err.message : String(err));
3091
+ process.exit(1);
3092
+ }
2233
3093
  }
2234
- const api = resolveApiConfig(opts.stage);
2235
- if (!api) process.exit(1);
2236
- printHeader("tools web-search test", opts.stage);
3094
+ );
3095
+ webSearch.command("test").description("Test the stored web_search provider + key against the provider API.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (opts) => {
2237
3096
  try {
3097
+ const { stage, api, tenant } = await resolveCtx(opts);
3098
+ printHeader("tools web-search test", stage);
2238
3099
  const result = await apiFetch(
2239
3100
  api.apiUrl,
2240
3101
  api.authSecret,
2241
3102
  "/api/skills/builtin-tools/web-search/test",
2242
3103
  { method: "POST", body: "{}" },
2243
- { "x-tenant-slug": opts.tenant }
3104
+ { "x-tenant-slug": tenant.slug }
2244
3105
  );
2245
3106
  if (result.ok) {
2246
3107
  printSuccess(`${result.provider}: ${result.resultCount} result(s) returned.`);
@@ -2249,63 +3110,54 @@ function registerToolsCommand(program2) {
2249
3110
  process.exit(1);
2250
3111
  }
2251
3112
  } catch (err) {
2252
- printError(err.message);
3113
+ if (isCancellation(err)) return;
3114
+ printError(err instanceof Error ? err.message : String(err));
2253
3115
  process.exit(1);
2254
3116
  }
2255
3117
  });
2256
- webSearch.command("disable").description("Disable web_search without deleting the stored key").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2257
- const check = validateStage(opts.stage);
2258
- if (!check.valid) {
2259
- printError(check.error);
2260
- process.exit(1);
2261
- }
2262
- const api = resolveApiConfig(opts.stage);
2263
- if (!api) process.exit(1);
3118
+ webSearch.command("disable").description("Disable web_search without deleting the stored key.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (opts) => {
2264
3119
  try {
3120
+ const { api, tenant } = await resolveCtx(opts);
2265
3121
  await apiFetch(
2266
3122
  api.apiUrl,
2267
3123
  api.authSecret,
2268
3124
  "/api/skills/builtin-tools/web-search",
2269
3125
  { method: "PUT", body: JSON.stringify({ enabled: false }) },
2270
- { "x-tenant-slug": opts.tenant }
3126
+ { "x-tenant-slug": tenant.slug }
2271
3127
  );
2272
3128
  printSuccess("web_search disabled.");
2273
3129
  } catch (err) {
2274
- printError(err.message);
3130
+ if (isCancellation(err)) return;
3131
+ printError(err instanceof Error ? err.message : String(err));
2275
3132
  process.exit(1);
2276
3133
  }
2277
3134
  });
2278
- webSearch.command("clear").description("Remove web_search config entirely (deletes stored key)").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2279
- const check = validateStage(opts.stage);
2280
- if (!check.valid) {
2281
- printError(check.error);
2282
- process.exit(1);
2283
- }
2284
- const api = resolveApiConfig(opts.stage);
2285
- if (!api) process.exit(1);
3135
+ webSearch.command("clear").description("Remove web_search config entirely (deletes stored key).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (opts) => {
2286
3136
  try {
3137
+ const { api, tenant } = await resolveCtx(opts);
2287
3138
  await apiFetch(
2288
3139
  api.apiUrl,
2289
3140
  api.authSecret,
2290
3141
  "/api/skills/builtin-tools/web-search",
2291
3142
  { method: "DELETE" },
2292
- { "x-tenant-slug": opts.tenant }
3143
+ { "x-tenant-slug": tenant.slug }
2293
3144
  );
2294
3145
  printSuccess("web_search configuration cleared.");
2295
3146
  } catch (err) {
2296
- printError(err.message);
3147
+ if (isCancellation(err)) return;
3148
+ printError(err instanceof Error ? err.message : String(err));
2297
3149
  process.exit(1);
2298
3150
  }
2299
3151
  });
2300
3152
  }
2301
3153
 
2302
3154
  // src/commands/update.ts
2303
- import { execSync as execSync9 } from "child_process";
3155
+ import { execSync as execSync10 } from "child_process";
2304
3156
  import { realpathSync } from "fs";
2305
- import chalk10 from "chalk";
3157
+ import chalk12 from "chalk";
2306
3158
  function getLatestVersion() {
2307
3159
  try {
2308
- return execSync9("npm view thinkwork-cli version", {
3160
+ return execSync10("npm view thinkwork-cli version", {
2309
3161
  encoding: "utf-8",
2310
3162
  timeout: 1e4,
2311
3163
  stdio: ["pipe", "pipe", "pipe"]
@@ -2316,7 +3168,7 @@ function getLatestVersion() {
2316
3168
  }
2317
3169
  function detectInstallMethod() {
2318
3170
  try {
2319
- const which = execSync9("which thinkwork", {
3171
+ const which = execSync10("which thinkwork", {
2320
3172
  encoding: "utf-8",
2321
3173
  timeout: 5e3,
2322
3174
  stdio: ["pipe", "pipe", "pipe"]
@@ -2344,28 +3196,28 @@ function compareVersions(a, b) {
2344
3196
  function registerUpdateCommand(program2) {
2345
3197
  program2.command("update").description("Check for and install CLI updates").option("--check", "Only check for updates, don't install").action(async (opts) => {
2346
3198
  printHeader("update", "", null);
2347
- console.log(` Current version: ${chalk10.bold(VERSION)}`);
3199
+ console.log(` Current version: ${chalk12.bold(VERSION)}`);
2348
3200
  const latest = getLatestVersion();
2349
3201
  if (!latest) {
2350
- console.log(chalk10.yellow(" Could not check npm registry for updates."));
3202
+ console.log(chalk12.yellow(" Could not check npm registry for updates."));
2351
3203
  return;
2352
3204
  }
2353
- console.log(` Latest version: ${chalk10.bold(latest)}`);
3205
+ console.log(` Latest version: ${chalk12.bold(latest)}`);
2354
3206
  console.log("");
2355
3207
  const cmp = compareVersions(VERSION, latest);
2356
3208
  if (cmp >= 0) {
2357
- console.log(chalk10.green(" \u2713 You're on the latest version."));
3209
+ console.log(chalk12.green(" \u2713 You're on the latest version."));
2358
3210
  console.log("");
2359
3211
  return;
2360
3212
  }
2361
- console.log(chalk10.cyan(` Update available: ${VERSION} \u2192 ${latest}`));
3213
+ console.log(chalk12.cyan(` Update available: ${VERSION} \u2192 ${latest}`));
2362
3214
  console.log("");
2363
3215
  if (opts.check) {
2364
3216
  const method2 = detectInstallMethod();
2365
3217
  if (method2 === "homebrew") {
2366
- console.log(` Run: ${chalk10.cyan("brew upgrade thinkwork-ai/tap/thinkwork")}`);
3218
+ console.log(` Run: ${chalk12.cyan("brew upgrade thinkwork-ai/tap/thinkwork")}`);
2367
3219
  } else {
2368
- console.log(` Run: ${chalk10.cyan(`npm install -g thinkwork-cli@${latest}`)}`);
3220
+ console.log(` Run: ${chalk12.cyan(`npm install -g thinkwork-cli@${latest}`)}`);
2369
3221
  }
2370
3222
  console.log("");
2371
3223
  return;
@@ -2373,27 +3225,27 @@ function registerUpdateCommand(program2) {
2373
3225
  const method = detectInstallMethod();
2374
3226
  const cmd = method === "homebrew" ? "brew upgrade thinkwork-ai/tap/thinkwork" : `npm install -g thinkwork-cli@${latest}`;
2375
3227
  console.log(` Installing via ${method}...`);
2376
- console.log(chalk10.dim(` $ ${cmd}`));
3228
+ console.log(chalk12.dim(` $ ${cmd}`));
2377
3229
  console.log("");
2378
3230
  try {
2379
- execSync9(cmd, { stdio: "inherit", timeout: 12e4 });
3231
+ execSync10(cmd, { stdio: "inherit", timeout: 12e4 });
2380
3232
  console.log("");
2381
- console.log(chalk10.green(` \u2713 Upgraded to thinkwork-cli@${latest}`));
3233
+ console.log(chalk12.green(` \u2713 Upgraded to thinkwork-cli@${latest}`));
2382
3234
  } catch {
2383
3235
  console.log("");
2384
- console.log(chalk10.red(` Failed to upgrade. Try manually:`));
2385
- console.log(` ${chalk10.cyan(cmd)}`);
3236
+ console.log(chalk12.red(` Failed to upgrade. Try manually:`));
3237
+ console.log(` ${chalk12.cyan(cmd)}`);
2386
3238
  }
2387
3239
  console.log("");
2388
3240
  });
2389
3241
  }
2390
3242
 
2391
3243
  // src/commands/user.ts
2392
- import { spawn as spawn3 } from "child_process";
2393
- import { input, select as select2 } from "@inquirer/prompts";
3244
+ import { spawn as spawn4 } from "child_process";
3245
+ import { input as input2, select as select6 } from "@inquirer/prompts";
2394
3246
  function getTerraformOutput2(cwd, key) {
2395
3247
  return new Promise((resolve3, reject) => {
2396
- const proc = spawn3("terraform", ["output", "-raw", key], {
3248
+ const proc = spawn4("terraform", ["output", "-raw", key], {
2397
3249
  cwd,
2398
3250
  stdio: ["pipe", "pipe", "pipe"]
2399
3251
  });
@@ -2425,7 +3277,7 @@ function runAwsCognitoReset(userPoolId, username, region) {
2425
3277
  "json"
2426
3278
  ];
2427
3279
  if (region) args.push("--region", region);
2428
- const proc = spawn3("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
3280
+ const proc = spawn4("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
2429
3281
  let stdout = "";
2430
3282
  let stderr = "";
2431
3283
  proc.stdout.on("data", (d) => stdout += d);
@@ -2433,10 +3285,7 @@ function runAwsCognitoReset(userPoolId, username, region) {
2433
3285
  proc.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
2434
3286
  });
2435
3287
  }
2436
- function isCancellation(err) {
2437
- return err instanceof Error && err.name === "ExitPromptError";
2438
- }
2439
- function requireTty(label) {
3288
+ function requireTty2(label) {
2440
3289
  if (!process.stdin.isTTY) {
2441
3290
  printError(
2442
3291
  `${label} is required. Pass it as a flag or re-run in an interactive terminal.`
@@ -2445,14 +3294,14 @@ function requireTty(label) {
2445
3294
  }
2446
3295
  }
2447
3296
  async function promptEmail() {
2448
- requireTty("Email");
2449
- return await input({
3297
+ requireTty2("Email");
3298
+ return await input2({
2450
3299
  message: "Email address of the person to invite:",
2451
3300
  validate: (v) => v.trim().includes("@") ? true : "That doesn't look like an email."
2452
3301
  });
2453
3302
  }
2454
3303
  async function promptStage(region) {
2455
- requireTty("Stage");
3304
+ requireTty2("Stage");
2456
3305
  const stages = listDeployedStages(region);
2457
3306
  if (stages.length === 0) {
2458
3307
  printError(
@@ -2464,14 +3313,14 @@ async function promptStage(region) {
2464
3313
  console.log(` Using the only deployed stage: ${stages[0]}`);
2465
3314
  return stages[0];
2466
3315
  }
2467
- return await select2({
3316
+ return await select6({
2468
3317
  message: "Which stage?",
2469
3318
  choices: stages.map((s) => ({ name: s, value: s })),
2470
3319
  loop: false
2471
3320
  });
2472
3321
  }
2473
3322
  async function promptTenant(apiUrl, authSecret) {
2474
- requireTty("Tenant");
3323
+ requireTty2("Tenant");
2475
3324
  const list = await apiFetch(apiUrl, authSecret, "/api/tenants");
2476
3325
  if (!list || list.length === 0) {
2477
3326
  printError(
@@ -2483,7 +3332,7 @@ async function promptTenant(apiUrl, authSecret) {
2483
3332
  console.log(` Using the only tenant: ${list[0].name} (${list[0].slug})`);
2484
3333
  return list[0].slug;
2485
3334
  }
2486
- return await select2({
3335
+ return await select6({
2487
3336
  message: "Which tenant?",
2488
3337
  choices: list.map((t) => ({
2489
3338
  name: `${t.name} (slug: ${t.slug})`,
@@ -2494,7 +3343,7 @@ async function promptTenant(apiUrl, authSecret) {
2494
3343
  }
2495
3344
  async function promptOptionalName() {
2496
3345
  if (!process.stdin.isTTY) return void 0;
2497
- const answer = await input({
3346
+ const answer = await input2({
2498
3347
  message: "Display name (optional, press Enter to skip):",
2499
3348
  default: ""
2500
3349
  });
@@ -2502,7 +3351,7 @@ async function promptOptionalName() {
2502
3351
  }
2503
3352
  async function promptRole() {
2504
3353
  if (!process.stdin.isTTY) return "member";
2505
- return await select2({
3354
+ return await select6({
2506
3355
  message: "Role:",
2507
3356
  choices: [
2508
3357
  { name: "member \u2014 regular access", value: "member" },
@@ -2624,15 +3473,18 @@ Agents / scripts that pass all flags stay non-interactive.
2624
3473
  }
2625
3474
  );
2626
3475
  user.command("reset-password <email>").description(
2627
- "Trigger Cognito's forgot-password flow for a user (admin-initiated). Sends them a verification code email."
2628
- ).option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage (e.g. dev, prod)").option(
3476
+ "Trigger Cognito's forgot-password flow for a user (admin-initiated). Prompts for stage in a TTY when omitted."
3477
+ ).option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage (e.g. dev, prod)").option(
2629
3478
  "-r, --region <name>",
2630
3479
  "AWS region (defaults to AWS CLI default / AWS_REGION)"
2631
3480
  ).addHelpText(
2632
3481
  "after",
2633
3482
  `
2634
3483
  Examples:
2635
- # Admin-triggered password reset \u2014 works even if the account is locked
3484
+ # Fully interactive \u2014 picks stage from the deployed ones
3485
+ $ thinkwork user reset-password alice@example.com
3486
+
3487
+ # Scripted
2636
3488
  $ thinkwork user reset-password alice@example.com -s dev
2637
3489
 
2638
3490
  # Target a specific AWS profile + region
@@ -2645,10 +3497,12 @@ FORCE_CHANGE_PASSWORD or has been disabled.
2645
3497
  `
2646
3498
  ).action(
2647
3499
  async (email, opts) => {
2648
- const stageCheck = validateStage(opts.stage);
2649
- if (!stageCheck.valid) {
2650
- printError(stageCheck.error);
2651
- process.exit(1);
3500
+ let stage;
3501
+ try {
3502
+ stage = await resolveStage({ flag: opts.stage, region: opts.region });
3503
+ } catch (err) {
3504
+ if (isCancellation(err)) return;
3505
+ throw err;
2652
3506
  }
2653
3507
  if (!email || !email.includes("@")) {
2654
3508
  printError(
@@ -2656,11 +3510,11 @@ FORCE_CHANGE_PASSWORD or has been disabled.
2656
3510
  );
2657
3511
  process.exit(1);
2658
3512
  }
2659
- printHeader("user reset-password", opts.stage);
3513
+ printHeader("user reset-password", stage);
2660
3514
  const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
2661
- const cwd = resolveTierDir(terraformDir, opts.stage, "app");
3515
+ const cwd = resolveTierDir(terraformDir, stage, "app");
2662
3516
  await ensureInit(cwd);
2663
- await ensureWorkspace(cwd, opts.stage);
3517
+ await ensureWorkspace(cwd, stage);
2664
3518
  let userPoolId;
2665
3519
  try {
2666
3520
  userPoolId = await getTerraformOutput2(cwd, "user_pool_id");
@@ -2704,27 +3558,904 @@ FORCE_CHANGE_PASSWORD or has been disabled.
2704
3558
  );
2705
3559
  }
2706
3560
 
2707
- // src/cli.ts
2708
- var program = new Command();
2709
- program.name("thinkwork").description(
2710
- "Thinkwork CLI \u2014 deploy, manage, and interact with your Thinkwork stack"
2711
- ).version(VERSION, "-v, --version", "Print the CLI version").option(
2712
- "-p, --profile <name>",
2713
- "AWS profile to use (sets AWS_PROFILE for Terraform and AWS CLI)"
2714
- );
2715
- program.hook("preAction", (_thisCommand, actionCommand) => {
2716
- const explicit = actionCommand.opts().profile ?? program.opts().profile;
2717
- if (explicit) {
2718
- process.env.AWS_PROFILE = explicit;
2719
- return;
2720
- }
2721
- if (process.env.AWS_PROFILE) return;
2722
- const fallback = loadCliConfig().defaultProfile;
2723
- if (fallback) process.env.AWS_PROFILE = fallback;
3561
+ // src/commands/me.ts
3562
+ import { gql } from "@urql/core";
3563
+
3564
+ // src/lib/gql-client.ts
3565
+ import {
3566
+ Client,
3567
+ cacheExchange,
3568
+ fetchExchange
3569
+ } from "@urql/core";
3570
+
3571
+ // src/lib/resolve-auth.ts
3572
+ var REFRESH_WINDOW_SECONDS = 5 * 60;
3573
+ async function resolveAuth(opts) {
3574
+ const region = opts.region ?? "us-east-1";
3575
+ const session = loadStageSession(opts.stage);
3576
+ if (session?.kind === "cognito") {
3577
+ const fresh = await ensureCognitoFresh(opts.stage, session, region);
3578
+ return cognitoAuth(fresh);
3579
+ }
3580
+ if (opts.requireCognito) {
3581
+ printError(
3582
+ `Stage "${opts.stage}" has no Cognito session. Run \`thinkwork login --stage ${opts.stage}\` (not --api-key \u2014 this command needs a user identity).`
3583
+ );
3584
+ process.exit(1);
3585
+ }
3586
+ if (session?.kind === "api-key") {
3587
+ return apiKeyAuth(session);
3588
+ }
3589
+ const api = resolveApiConfig(opts.stage, region);
3590
+ if (!api) process.exit(1);
3591
+ return {
3592
+ mode: "api-key",
3593
+ headers: {
3594
+ Authorization: `Bearer ${api.authSecret}`
3595
+ }
3596
+ };
3597
+ }
3598
+ function cognitoAuth(session) {
3599
+ return {
3600
+ mode: "cognito",
3601
+ headers: {
3602
+ // graphql-http Lambda reads the id_token from `Authorization` (no
3603
+ // `Bearer ` prefix per admin's fetchOptions). Keep the same shape.
3604
+ Authorization: session.idToken
3605
+ },
3606
+ principalId: session.principalId,
3607
+ tenantId: session.tenantId,
3608
+ tenantSlug: session.tenantSlug
3609
+ };
3610
+ }
3611
+ function apiKeyAuth(session) {
3612
+ const headers = {
3613
+ Authorization: `Bearer ${session.authSecret}`
3614
+ };
3615
+ if (session.tenantId) headers["x-tenant-id"] = session.tenantId;
3616
+ return {
3617
+ mode: "api-key",
3618
+ headers,
3619
+ tenantId: session.tenantId,
3620
+ tenantSlug: session.tenantSlug
3621
+ };
3622
+ }
3623
+ async function ensureCognitoFresh(stage, session, region) {
3624
+ const nowSeconds = Math.floor(Date.now() / 1e3);
3625
+ const safeUntil = session.expiresAt - REFRESH_WINDOW_SECONDS;
3626
+ if (nowSeconds < safeUntil) return session;
3627
+ const cognito = discoverCognitoConfig(stage, region) ?? {
3628
+ userPoolId: session.userPoolId,
3629
+ clientId: session.userPoolClientId,
3630
+ domain: session.cognitoDomain,
3631
+ domainUrl: `https://${session.cognitoDomain}.auth.${session.region}.amazoncognito.com`,
3632
+ region: session.region
3633
+ };
3634
+ try {
3635
+ const refreshed = await refreshCognitoTokens(cognito, session.refreshToken);
3636
+ const next = {
3637
+ ...session,
3638
+ idToken: refreshed.idToken,
3639
+ accessToken: refreshed.accessToken,
3640
+ expiresAt: refreshed.expiresAt
3641
+ };
3642
+ saveStageSession(stage, next);
3643
+ return next;
3644
+ } catch (err) {
3645
+ printError(
3646
+ `Session refresh failed for stage "${stage}": ${err instanceof Error ? err.message : String(err)}. Run \`thinkwork login --stage ${stage}\` to sign in again.`
3647
+ );
3648
+ process.exit(1);
3649
+ }
3650
+ }
3651
+
3652
+ // src/lib/gql-client.ts
3653
+ async function getGqlClient(opts) {
3654
+ const region = opts.region ?? "us-east-1";
3655
+ const baseUrl = getApiEndpoint(opts.stage, region);
3656
+ if (!baseUrl) {
3657
+ printError(
3658
+ `Cannot discover API endpoint for stage "${opts.stage}" in ${region}. Is the stack deployed?`
3659
+ );
3660
+ process.exit(1);
3661
+ }
3662
+ const url = `${baseUrl.replace(/\/+$/, "")}/graphql`;
3663
+ const auth = await resolveAuth({ stage: opts.stage, region });
3664
+ const client = new Client({
3665
+ url,
3666
+ exchanges: [cacheExchange, fetchExchange],
3667
+ fetchOptions: () => ({
3668
+ method: "POST",
3669
+ headers: {
3670
+ "content-type": "application/json",
3671
+ ...auth.headers
3672
+ }
3673
+ }),
3674
+ // CLI calls are short-lived and we want server truth on every run —
3675
+ // bypass the in-memory cache to avoid stale reads between quick commands.
3676
+ requestPolicy: "network-only"
3677
+ });
3678
+ return {
3679
+ client,
3680
+ url,
3681
+ tenantId: auth.tenantId,
3682
+ tenantSlug: auth.tenantSlug
3683
+ };
3684
+ }
3685
+
3686
+ // src/commands/me.ts
3687
+ var ME_QUERY = gql`
3688
+ query CliMe {
3689
+ me {
3690
+ id
3691
+ email
3692
+ name
3693
+ tenantId
3694
+ }
3695
+ }
3696
+ `;
3697
+ function registerMeCommand(program2) {
3698
+ program2.command("me").description(
3699
+ "Print the identity behind the current session for a stage. Use after `thinkwork login` to verify everything works, or as a scriptable introspection (`--json | jq`)."
3700
+ ).option("-s, --stage <name>", "Stage to introspect (defaults to the saved default stage)").option("-r, --region <region>", "AWS region", "us-east-1").addHelpText(
3701
+ "after",
3702
+ `
3703
+ Examples:
3704
+ # Check who you're signed in as on the default stage
3705
+ $ thinkwork me
3706
+
3707
+ # Explicit stage
3708
+ $ thinkwork me --stage prod
3709
+
3710
+ # Machine-readable \u2014 pipe to jq
3711
+ $ thinkwork me --stage dev --json | jq .tenantSlug
3712
+ `
3713
+ ).action(async (opts) => {
3714
+ const stage = await resolveStage({ flag: opts.stage, region: opts.region });
3715
+ const session = loadStageSession(stage);
3716
+ if (!session) {
3717
+ printError(
3718
+ `Not signed in to stage "${stage}". Run \`thinkwork login --stage ${stage}\`.`
3719
+ );
3720
+ process.exit(1);
3721
+ }
3722
+ if (!isJsonMode()) printHeader("me", stage);
3723
+ const { client, tenantSlug } = await getGqlClient({ stage, region: opts.region });
3724
+ let me = null;
3725
+ try {
3726
+ const res = await client.query(ME_QUERY, {}).toPromise();
3727
+ if (res.error) throw res.error;
3728
+ me = res.data?.me ?? null;
3729
+ } catch (err) {
3730
+ printError(
3731
+ `me query failed: ${err instanceof Error ? err.message : String(err)}`
3732
+ );
3733
+ process.exit(1);
3734
+ }
3735
+ if (isJsonMode()) {
3736
+ printJson({
3737
+ stage,
3738
+ mode: session.kind,
3739
+ user: me,
3740
+ tenant: { id: me?.tenantId, slug: tenantSlug }
3741
+ });
3742
+ return;
3743
+ }
3744
+ printKeyValue([
3745
+ ["Stage", stage],
3746
+ ["Mode", session.kind],
3747
+ ["User ID", me?.id],
3748
+ ["Email", me?.email ?? (session.kind === "cognito" ? session.email : void 0)],
3749
+ ["Name", me?.name ?? void 0],
3750
+ ["Tenant ID", me?.tenantId ?? session.tenantId],
3751
+ ["Tenant slug", tenantSlug ?? session.tenantSlug]
3752
+ ]);
3753
+ });
3754
+ }
3755
+
3756
+ // src/lib/stub.ts
3757
+ import chalk13 from "chalk";
3758
+ var ROADMAP_URL = "https://github.com/thinkwork-ai/thinkwork/blob/main/apps/cli/README.md#roadmap";
3759
+ function notYetImplemented(commandPath, phase) {
3760
+ const label = chalk13.yellow(`\u29D7 not yet implemented`);
3761
+ const line = chalk13.bold(`thinkwork ${commandPath}`);
3762
+ process.stderr.write(
3763
+ `
3764
+ ${label}: ${line} ships in Phase ${phase}.
3765
+ ${chalk13.dim(`See the roadmap: ${ROADMAP_URL}`)}
3766
+
3767
+ `
3768
+ );
3769
+ process.exit(2);
3770
+ }
3771
+
3772
+ // src/commands/thread.ts
3773
+ function registerThreadCommand(program2) {
3774
+ const thread = program2.command("thread").alias("threads").description(
3775
+ "Create, list, update, and comment on threads (tasks, chats, bugs, questions) in a tenant."
3776
+ );
3777
+ thread.command("list").alias("ls").description("List threads in a tenant with optional filters.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--status <status>", "Filter: BACKLOG | TODO | IN_PROGRESS | IN_REVIEW | BLOCKED | DONE | CANCELLED").option("--priority <p>", "Filter: LOW | MEDIUM | HIGH | URGENT").option("--assignee <id>", "Filter by assignee (user or agent ID). Use `me` to match the caller.").option("--agent <id>", "Filter threads worked on by a specific agent").option("--search <q>", "Full-text search over title/body").option("--limit <n>", "Max rows (default 50)", "50").option("--archived", "Include archived threads").addHelpText(
3778
+ "after",
3779
+ `
3780
+ Examples:
3781
+ # Open work on the default stage/tenant
3782
+ $ thinkwork thread list --status IN_PROGRESS
3783
+
3784
+ # Pipe to jq
3785
+ $ thinkwork thread list --json | jq '.[] | select(.priority=="URGENT")'
3786
+
3787
+ # Everything assigned to me
3788
+ $ thinkwork thread list --assignee me
3789
+ `
3790
+ ).action(() => notYetImplemented("thread list", 1));
3791
+ thread.command("get <idOrNumber>").description("Fetch one thread by ID or by its tenant-scoped issue number.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
3792
+ "after",
3793
+ `
3794
+ Examples:
3795
+ $ thinkwork thread get thr-abc123
3796
+ $ thinkwork thread get 42 # by issue number
3797
+ $ thinkwork thread get 42 --json | jq .assignee
3798
+ `
3799
+ ).action(() => notYetImplemented("thread get", 1));
3800
+ thread.command("create [title]").description("Create a new thread. Prompts for missing fields when running in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--type <type>", "TASK | CHAT | BUG | QUESTION", "TASK").option("--priority <p>", "LOW | MEDIUM | HIGH | URGENT", "MEDIUM").option("--assignee <id>", "Assign on create (user or agent ID)").option("--body <text>", "Description body (markdown)").option("--due <iso>", "Due date as ISO-8601").option("--label <name...>", "Attach label(s) by name (repeatable)").addHelpText(
3801
+ "after",
3802
+ `
3803
+ Examples:
3804
+ # Fully interactive \u2014 walkthrough prompts for title, type, priority, assignee.
3805
+ $ thinkwork thread create
3806
+
3807
+ # Scripted
3808
+ $ thinkwork thread create "Investigate latency spike" \\
3809
+ --priority HIGH --assignee agt-obs-1 --label ops --label oncall
3810
+
3811
+ # Mix: pass the title, prompt for the rest.
3812
+ $ thinkwork thread create "Investigate latency spike"
3813
+ `
3814
+ ).action(() => notYetImplemented("thread create", 1));
3815
+ thread.command("update <id>").description("Update a thread's title, status, priority, assignee, labels, or due date.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--title <t>", "Rename").option("--status <s>", "Move to a new status").option("--priority <p>", "LOW | MEDIUM | HIGH | URGENT").option("--assignee <id>", "Reassign (user or agent ID)").option("--due <iso>", "Due date").option("--body <text>", "Replace description body").addHelpText(
3816
+ "after",
3817
+ `
3818
+ Examples:
3819
+ $ thinkwork thread update thr-abc --status IN_REVIEW
3820
+ $ thinkwork thread update thr-abc --assignee agt-ops --priority URGENT
3821
+ `
3822
+ ).action(() => notYetImplemented("thread update", 1));
3823
+ thread.command("close <id>").description("Mark a thread DONE. Shortcut for `thread update <id> --status DONE`.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--comment <text>", "Add a closing comment").addHelpText(
3824
+ "after",
3825
+ `
3826
+ Examples:
3827
+ $ thinkwork thread close thr-abc
3828
+ $ thinkwork thread close thr-abc --comment "fixed in #124"
3829
+ `
3830
+ ).action(() => notYetImplemented("thread close", 1));
3831
+ thread.command("reopen <id>").description("Move a thread from DONE/CANCELLED back to TODO.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread reopen", 1));
3832
+ thread.command("checkout <id>").description("Claim a thread so an agent can work it (locks other agents out).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent to check it out to (defaults to the caller)").addHelpText(
3833
+ "after",
3834
+ `
3835
+ Examples:
3836
+ $ thinkwork thread checkout thr-abc --agent agt-fixer
3837
+ `
3838
+ ).action(() => notYetImplemented("thread checkout", 1));
3839
+ thread.command("release <id>").description("Release a checked-out thread, optionally moving it to a new status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--status <s>", "Status to release into").action(() => notYetImplemented("thread release", 1));
3840
+ thread.command("comment <id> [content]").description("Add a comment to a thread. Prompts for content if omitted and TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--file <path>", "Read comment content from a file (markdown)").addHelpText(
3841
+ "after",
3842
+ `
3843
+ Examples:
3844
+ $ thinkwork thread comment thr-abc "Looks good, shipping"
3845
+ $ thinkwork thread comment thr-abc --file /tmp/review.md
3846
+ $ thinkwork thread comment thr-abc # prompts interactively
3847
+ `
3848
+ ).action(() => notYetImplemented("thread comment", 1));
3849
+ thread.command("label <assign|remove> <threadId> <labelId>").description("Attach or detach a label on a thread. Labels are managed via `thinkwork label`.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
3850
+ "after",
3851
+ `
3852
+ Examples:
3853
+ $ thinkwork thread label assign thr-abc lbl-ops
3854
+ $ thinkwork thread label remove thr-abc lbl-ops
3855
+ `
3856
+ ).action(() => notYetImplemented("thread label", 1));
3857
+ thread.command("escalate <id>").description("Escalate a thread to another agent (carries context, records actor).").option("--to-agent <id>", "Agent to escalate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--reason <text>", "Reason note (appears in activity log)").action(() => notYetImplemented("thread escalate", 1));
3858
+ thread.command("delegate <id>").description("Delegate ownership to another agent without the 'escalation' semantics.").option("--to-agent <id>", "Agent to delegate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread delegate", 1));
3859
+ thread.command("delete <id>").description("Permanently delete a thread (not just close). Requires confirmation.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip the confirmation prompt").addHelpText(
3860
+ "after",
3861
+ `
3862
+ Examples:
3863
+ # Prompts 'Are you sure?'
3864
+ $ thinkwork thread delete thr-abc
3865
+
3866
+ # Scripted / destructive-no-prompt
3867
+ $ thinkwork thread delete thr-abc --yes
3868
+ `
3869
+ ).action(() => notYetImplemented("thread delete", 1));
3870
+ }
3871
+
3872
+ // src/commands/message.ts
3873
+ function registerMessageCommand(program2) {
3874
+ const msg = program2.command("message").alias("messages").alias("msg").description("Send and list messages inside a thread.");
3875
+ msg.command("send <threadId> [content]").description("Send a message to a thread. Prompts for content if omitted and TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--file <path>", "Read message content from a file").option("--as-agent <id>", "Send as a specific agent (api-key auth only)").addHelpText(
3876
+ "after",
3877
+ `
3878
+ Examples:
3879
+ $ thinkwork message send thr-abc "Investigating now"
3880
+ $ thinkwork message send thr-abc --file notes.md
3881
+ $ thinkwork message send thr-abc # interactive
3882
+ `
3883
+ ).action(() => notYetImplemented("message send", 1));
3884
+ msg.command("list <threadId>").alias("ls").description("List messages in a thread (paginated).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max messages to return", "50").option("--cursor <c>", "Pagination cursor from a previous page").addHelpText(
3885
+ "after",
3886
+ `
3887
+ Examples:
3888
+ $ thinkwork message list thr-abc
3889
+ $ thinkwork message list thr-abc --limit 10 --json | jq '.[].author'
3890
+ `
3891
+ ).action(() => notYetImplemented("message list", 1));
3892
+ }
3893
+
3894
+ // src/commands/label.ts
3895
+ function registerLabelCommand(program2) {
3896
+ const label = program2.command("label").alias("labels").description("Manage tenant-wide thread labels (tags).");
3897
+ label.command("list").alias("ls").description("List all labels in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("label list", 1));
3898
+ label.command("create [name]").description("Create a new label. Prompts for missing fields in TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--color <hex>", "Label color as a hex string (e.g. #10b981)").option("--description <text>", "What the label is for").addHelpText(
3899
+ "after",
3900
+ `
3901
+ Examples:
3902
+ $ thinkwork label create ops --color "#10b981"
3903
+ $ thinkwork label create # interactive
3904
+ `
3905
+ ).action(() => notYetImplemented("label create", 1));
3906
+ label.command("update <id>").description("Rename or recolor an existing label.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--color <hex>").option("--description <text>").action(() => notYetImplemented("label update", 1));
3907
+ label.command("delete <id>").description("Delete a label. Any thread assignments are removed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip the confirmation prompt").action(() => notYetImplemented("label delete", 1));
3908
+ }
3909
+
3910
+ // src/commands/inbox.ts
3911
+ function registerInboxCommand(program2) {
3912
+ const inbox = program2.command("inbox").description("View and act on approval requests routed to you or your workspace.");
3913
+ inbox.command("list").alias("ls").description("List inbox items, optionally filtered by status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option(
3914
+ "--status <s>",
3915
+ "PENDING | APPROVED | REJECTED | REVISION_REQUESTED | EXPIRED | CANCELLED (default: PENDING)",
3916
+ "PENDING"
3917
+ ).option("--entity-type <type>", "Filter by entity type (thread, agent, artifact, \u2026)").option("--entity-id <id>", "Filter by entity ID").option("--mine", "Only items routed to the caller").addHelpText(
3918
+ "after",
3919
+ `
3920
+ Examples:
3921
+ # What's waiting for me to approve?
3922
+ $ thinkwork inbox list --mine
3923
+
3924
+ # All pending approvals in the tenant
3925
+ $ thinkwork inbox list
3926
+
3927
+ # Closed items for audit
3928
+ $ thinkwork inbox list --status APPROVED --json
3929
+ `
3930
+ ).action(() => notYetImplemented("inbox list", 1));
3931
+ inbox.command("get <id>").description("Fetch one inbox item with its comments, links, and history.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox get", 1));
3932
+ inbox.command("approve <id>").description("Approve an inbox item. Downstream agents resume.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--notes <text>", "Approval notes (stored on the decision)").addHelpText(
3933
+ "after",
3934
+ `
3935
+ Examples:
3936
+ $ thinkwork inbox approve ibx-abc
3937
+ $ thinkwork inbox approve ibx-abc --notes "Budget confirmed."
3938
+ `
3939
+ ).action(() => notYetImplemented("inbox approve", 1));
3940
+ inbox.command("reject <id>").description("Reject an inbox item. Downstream agents stop.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--notes <text>", "Rejection reason").action(() => notYetImplemented("inbox reject", 1));
3941
+ inbox.command("request-revision <id>").description("Ask for changes \u2014 the agent gets the item back with your notes.").option("--notes <text>", "What needs to change").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox request-revision", 1));
3942
+ inbox.command("resubmit <id>").description("Resubmit a revised inbox item for approval.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--notes <text>", "What changed").action(() => notYetImplemented("inbox resubmit", 1));
3943
+ inbox.command("cancel <id>").description("Cancel a pending approval request (originator or admin).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox cancel", 1));
3944
+ inbox.command("comment <id> [content]").description("Add a comment to an inbox item without deciding on it yet.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--file <path>", "Read comment body from a file").action(() => notYetImplemented("inbox comment", 1));
3945
+ }
3946
+
3947
+ // src/commands/agent.ts
3948
+ function registerAgentCommand(program2) {
3949
+ const agent = program2.command("agent").alias("agents").description("Manage agents \u2014 create, configure, inspect, budget, and key-rotate.");
3950
+ agent.command("list").alias("ls").description("List agents in a tenant. Cognito users see paired agents; admins see all.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--status <s>", "IDLE | BUSY | OFFLINE | ERROR").option("--type <t>", "Filter by agent type (HUMAN_PAIR, TEAM_AGENT, SUB_AGENT, \u2026)").option("--include-system", "Include internal system agents").option("--all", "Admin-only: list every agent in the tenant (not just paired ones)").addHelpText(
3951
+ "after",
3952
+ `
3953
+ Examples:
3954
+ # Agents you're paired with
3955
+ $ thinkwork agent list
3956
+
3957
+ # Tenant-wide (admin only)
3958
+ $ thinkwork agent list --all
3959
+
3960
+ # Offline agents only, as JSON
3961
+ $ thinkwork agent list --status OFFLINE --json
3962
+ `
3963
+ ).action(() => notYetImplemented("agent list", 2));
3964
+ agent.command("get <id>").description("Fetch one agent with its skills, capabilities, budget, and recent activity.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent get", 2));
3965
+ agent.command("create [name]").description("Create a new agent. Prompts walkthrough for missing fields in TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--template <id>", "Clone from an existing template (strongly recommended)").option("--role <role>", "Role description shown to users").option("--type <type>", "TEAM_AGENT | SUB_AGENT | HUMAN_PAIR").option("--parent <agentId>", "Parent agent (for SUB_AGENT)").option("--reports-to <agentId>", "Reporting manager (for org-chart display)").option("--system-prompt <text>", "Raw system-prompt override (use with care)").option("--system-prompt-file <path>", "Load the system prompt from a file").option("--model <id>", "Model ID override (see `thinkwork config models`)").addHelpText(
3966
+ "after",
3967
+ `
3968
+ Examples:
3969
+ # Fully interactive
3970
+ $ thinkwork agent create
3971
+
3972
+ # From a template (recommended)
3973
+ $ thinkwork agent create "Ops Analyst" --template tpl-ops-analyst
3974
+
3975
+ # Scripted, raw system prompt
3976
+ $ thinkwork agent create "Bot" --role "on-call summarizer" \\
3977
+ --type TEAM_AGENT --model claude-sonnet-4-6 \\
3978
+ --system-prompt-file prompts/bot.md
3979
+ `
3980
+ ).action(() => notYetImplemented("agent create", 2));
3981
+ agent.command("update <id>").description("Update any mutable agent field.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--role <r>").option("--type <t>").option("--parent <agentId>").option("--reports-to <agentId>").option("--system-prompt <text>").option("--system-prompt-file <path>").option("--model <id>").action(() => notYetImplemented("agent update", 2));
3982
+ agent.command("delete <id>").description("Archive (soft-delete) an agent. Existing threads stay; no new work routed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("agent delete", 2));
3983
+ agent.command("status <id> <status>").description("Manually set agent status. Useful to pause/resume.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
3984
+ "after",
3985
+ `
3986
+ Examples:
3987
+ $ thinkwork agent status agt-ops IDLE
3988
+ $ thinkwork agent status agt-ops OFFLINE
3989
+ `
3990
+ ).action(() => notYetImplemented("agent status", 2));
3991
+ agent.command("unpause <id>").description("Resume an agent paused by a budget policy trigger.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent unpause", 2));
3992
+ const capabilities = agent.command("capabilities").alias("cap").description("Toggle built-in capabilities (email inbox, web search, etc.).");
3993
+ capabilities.command("set <agentId>").description("Enable/disable capabilities on an agent.").option("--capability <name>", "Capability name (email, web-search, file-upload, \u2026)").option("--enabled", "Enable (default if flag present)").option("--disabled", "Disable").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent capabilities set", 2));
3994
+ const skills = agent.command("skills").description("Attach or configure MCP-backed skills on an agent.");
3995
+ skills.command("set <agentId>").description("Enable/disable/configure a skill for an agent.").option("--skill <id>", "Skill ID (see `thinkwork skill list`)").option("--enabled", "Enable").option("--disabled", "Disable").option("--config <json>", "Inline JSON config for the skill").option("--rate-limit <rps>", "Rate limit in requests/sec").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent skills set", 2));
3996
+ const budget = agent.command("budget").description("Per-agent spend caps \u2014 pause or alert when exceeded.");
3997
+ budget.command("set <agentId>").description("Set or update an agent's budget policy.").option("--limit-usd <amount>", "USD ceiling for the window").option("--window <w>", "daily | weekly | monthly", "monthly").option("--action <a>", "PAUSE | ALERT", "PAUSE").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent budget set", 2));
3998
+ budget.command("clear <agentId>").description("Remove an agent's budget policy (falls back to tenant-wide).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent budget clear", 2));
3999
+ const apiKey = agent.command("api-key").description("Agent API keys \u2014 service-to-service credentials tied to one agent.");
4000
+ apiKey.command("list <agentId>").description("List API keys for an agent (metadata only; plaintext shown on create).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent api-key list", 2));
4001
+ apiKey.command("create <agentId>").description("Generate a new API key. The plaintext is printed once \u2014 save it.").option("--name <n>", "Human label for the key (e.g. 'GitHub Actions')").option("--expires <iso>", "Expiration (ISO-8601). Omit for no expiry.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4002
+ "after",
4003
+ `
4004
+ Examples:
4005
+ $ thinkwork agent api-key create agt-ops --name "GitHub Actions"
4006
+ $ thinkwork agent api-key create agt-ops --name "nightly" --expires 2026-12-31T00:00:00Z --json
4007
+ `
4008
+ ).action(() => notYetImplemented("agent api-key create", 2));
4009
+ apiKey.command("revoke <keyId>").description("Revoke an API key. Subsequent calls return 401.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("agent api-key revoke", 2));
4010
+ const email = agent.command("email").description("Inbound email addresses \u2014 let an agent receive email + optionally reply.");
4011
+ email.command("enable <agentId>").description("Enable inbound email for an agent.").option("--local-part <x>", "Custom localpart (e.g. ops@)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent email enable", 2));
4012
+ email.command("disable <agentId>").description("Disable inbound email for an agent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent email disable", 2));
4013
+ email.command("allowlist <agentId> <senders...>").description("Replace the allowlist of sender email addresses for an agent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4014
+ "after",
4015
+ `
4016
+ Examples:
4017
+ $ thinkwork agent email allowlist agt-ops oncall@example.com pagerduty@example.com
4018
+ `
4019
+ ).action(() => notYetImplemented("agent email allowlist", 2));
4020
+ const version = agent.command("version").description("Agent configuration version history.");
4021
+ version.command("list <agentId>").description("List version snapshots of an agent's config (prompt, model, skills, \u2026).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max versions", "20").action(() => notYetImplemented("agent version list", 2));
4022
+ version.command("rollback <agentId> <versionId>").description("Restore an agent to a prior version. Creates a new version pointing at the old config.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("agent version rollback", 2));
4023
+ }
4024
+
4025
+ // src/commands/template.ts
4026
+ function registerTemplateCommand(program2) {
4027
+ const tpl = program2.command("template").alias("templates").description("Manage agent templates \u2014 reusable configs you spawn agents from.");
4028
+ tpl.command("list").alias("ls").description("List templates in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template list", 2));
4029
+ tpl.command("get <id>").description("Fetch one template with its linked agents.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template get", 2));
4030
+ tpl.command("create [name]").description("Create a new template from a set of config defaults.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from-agent <id>", "Clone config from an existing agent").option("--system-prompt-file <path>", "Prompt markdown path").option("--model <id>", "Default model").option("--description <text>", "What this template is for").addHelpText(
4031
+ "after",
4032
+ `
4033
+ Examples:
4034
+ # Capture an existing agent's config as a template
4035
+ $ thinkwork template create "Ops Analyst" --from-agent agt-ops-1
4036
+
4037
+ # Fresh template
4038
+ $ thinkwork template create --system-prompt-file prompts/ops.md --model claude-sonnet-4-6
4039
+ `
4040
+ ).action(() => notYetImplemented("template create", 2));
4041
+ tpl.command("update <id>").description("Update a template. Linked agents are NOT auto-synced \u2014 use `template sync-*`.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--system-prompt-file <path>").option("--model <id>").option("--description <text>").action(() => notYetImplemented("template update", 2));
4042
+ tpl.command("delete <id>").description("Delete a template. Linked agents are unaffected; they just stop being in sync.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("template delete", 2));
4043
+ tpl.command("diff <templateId> <agentId>").description("Show what would change if we synced <agentId> to <templateId>.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template diff", 2));
4044
+ tpl.command("sync-agent <templateId> <agentId>").description("Apply template changes to one linked agent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("template sync-agent", 2));
4045
+ tpl.command("sync-all <templateId>").description("Apply template changes to every linked agent. Requires confirmation.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").addHelpText(
4046
+ "after",
4047
+ `
4048
+ Examples:
4049
+ # Preview first
4050
+ $ thinkwork template diff tpl-ops agt-ops-1
4051
+
4052
+ # Apply to one agent
4053
+ $ thinkwork template sync-agent tpl-ops agt-ops-1
4054
+
4055
+ # Apply to every linked agent
4056
+ $ thinkwork template sync-all tpl-ops
4057
+ `
4058
+ ).action(() => notYetImplemented("template sync-all", 2));
4059
+ }
4060
+
4061
+ // src/commands/tenant.ts
4062
+ function registerTenantCommand(program2) {
4063
+ const tenant = program2.command("tenant").alias("tenants").description("Manage tenants (workspaces) \u2014 create, rename, and configure plans / defaults.");
4064
+ tenant.command("list").alias("ls").description("List tenants the caller can see.").option("-s, --stage <name>", "Deployment stage").action(() => notYetImplemented("tenant list", 2));
4065
+ tenant.command("get <idOrSlug>").description("Fetch one tenant by ID or slug.").option("-s, --stage <name>", "Deployment stage").action(() => notYetImplemented("tenant get", 2));
4066
+ tenant.command("create [name]").description("Create a new tenant. The caller becomes its first owner.").option("-s, --stage <name>", "Deployment stage").option("--slug <slug>", "URL-safe slug (lowercase, hyphens). Generated from name if omitted.").option("--plan <plan>", "Plan tier (free, team, enterprise, \u2026)", "team").option("--issue-prefix <prefix>", "Issue-number prefix for thread numbers (e.g. ACME)").addHelpText(
4067
+ "after",
4068
+ `
4069
+ Examples:
4070
+ $ thinkwork tenant create "Acme Corp" --slug acme --plan team
4071
+ `
4072
+ ).action(() => notYetImplemented("tenant create", 2));
4073
+ tenant.command("update <id>").description("Update tenant name, plan, or issue-prefix.").option("-s, --stage <name>", "Deployment stage").option("--name <n>").option("--plan <plan>").option("--issue-prefix <prefix>").action(() => notYetImplemented("tenant update", 2));
4074
+ const settings = tenant.command("settings").description("Tenant-wide defaults \u2014 model, budget, auto-close, feature flags.");
4075
+ settings.command("get [tenant]").description("Print the current TenantSettings (human) or the full object (--json).").option("-s, --stage <name>", "Deployment stage").action(() => notYetImplemented("tenant settings get", 2));
4076
+ settings.command("set [tenant]").description("Set one or more TenantSettings fields. Each --<field> flag is independent.").option("-s, --stage <name>", "Deployment stage").option("--default-model <id>").option("--monthly-budget-usd <n>").option("--max-agents <n>").option("--auto-close-after-days <n>").option("--feature <key=value...>", "Toggle a feature flag (repeatable)").addHelpText(
4077
+ "after",
4078
+ `
4079
+ Examples:
4080
+ $ thinkwork tenant settings set --default-model claude-sonnet-4-6
4081
+ $ thinkwork tenant settings set --monthly-budget-usd 5000 --feature hindsight=true
4082
+ `
4083
+ ).action(() => notYetImplemented("tenant settings set", 2));
4084
+ }
4085
+
4086
+ // src/commands/member.ts
4087
+ function registerMemberCommand(program2) {
4088
+ const mem = program2.command("member").alias("members").description("List and manage tenant members (users + agents with access).");
4089
+ mem.command("list").alias("ls").description("List every member of the current tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--principal-type <t>", "Filter: USER | AGENT").option("--role <r>", "Filter by role (member, admin, owner)").action(() => notYetImplemented("member list", 2));
4090
+ mem.command("invite [email]").description("Invite a user by email. GraphQL path (sends invite email, creates Cognito user).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--role <role>", "member | admin | owner", "member").option("--name <n>", "Optional display name").addHelpText(
4091
+ "after",
4092
+ `
4093
+ Examples:
4094
+ $ thinkwork member invite alice@example.com --role admin
4095
+ $ thinkwork member invite # interactive
4096
+ `
4097
+ ).action(() => notYetImplemented("member invite", 2));
4098
+ mem.command("update <memberId>").description("Change a member's role or status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--role <r>").option("--status <s>", "active | suspended").action(() => notYetImplemented("member update", 2));
4099
+ mem.command("remove <memberId>").description("Remove a member from the tenant. The underlying Cognito user is NOT deleted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("member remove", 2));
4100
+ }
4101
+
4102
+ // src/commands/team.ts
4103
+ function registerTeamCommand(program2) {
4104
+ const team = program2.command("team").alias("teams").description("Manage teams within a tenant.");
4105
+ team.command("list").alias("ls").description("List teams in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team list", 2));
4106
+ team.command("get <id>").description("Fetch one team with its members and agents.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team get", 2));
4107
+ team.command("create [name]").description("Create a new team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--description <text>").option("--budget-usd <n>", "Optional sub-budget").addHelpText(
4108
+ "after",
4109
+ `
4110
+ Examples:
4111
+ $ thinkwork team create "Ops" --description "24/7 on-call" --budget-usd 2000
4112
+ `
4113
+ ).action(() => notYetImplemented("team create", 2));
4114
+ team.command("update <id>").description("Update team name, description, status, or budget.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--status <s>", "active | archived").option("--budget-usd <n>").action(() => notYetImplemented("team update", 2));
4115
+ team.command("delete <id>").description("Delete (archive) a team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("team delete", 2));
4116
+ team.command("add-agent <teamId> <agentId>").description("Add an agent to a team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team add-agent", 2));
4117
+ team.command("remove-agent <teamId> <agentId>").description("Remove an agent from a team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team remove-agent", 2));
4118
+ team.command("add-user <teamId> <userId>").description("Add a user to a team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team add-user", 2));
4119
+ team.command("remove-user <teamId> <userId>").description("Remove a user from a team.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("team remove-user", 2));
4120
+ }
4121
+
4122
+ // src/commands/kb.ts
4123
+ function registerKbCommand(program2) {
4124
+ const kb = program2.command("kb").alias("knowledge-base").description("Manage knowledge bases (RAG stores) and attach them to agents.");
4125
+ kb.command("list").alias("ls").description("List knowledge bases in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("kb list", 2));
4126
+ kb.command("get <id>").description("Fetch one knowledge base with its S3 source + sync status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("kb get", 2));
4127
+ kb.command("create [name]").description("Create a new knowledge base. Interactive prompts for missing fields.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--s3-uri <uri>", "S3 source location (s3://bucket/prefix)").option("--description <text>").option("--embedding-model <id>", "Bedrock embedding model ID").addHelpText(
4128
+ "after",
4129
+ `
4130
+ Examples:
4131
+ $ thinkwork kb create "Runbooks" --s3-uri s3://ops-docs/runbooks
4132
+ $ thinkwork kb create # interactive
4133
+ `
4134
+ ).action(() => notYetImplemented("kb create", 2));
4135
+ kb.command("update <id>").description("Update knowledge base metadata (name, description). Source changes need re-create.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").action(() => notYetImplemented("kb update", 2));
4136
+ kb.command("delete <id>").description("Delete a knowledge base. Embeddings + index are destroyed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("kb delete", 2));
4137
+ kb.command("sync <id>").description("Re-embed from S3. Idempotent; safe to re-run.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--wait", "Block until the sync finishes").action(() => notYetImplemented("kb sync", 2));
4138
+ kb.command("attach <kbId>").description("Attach a knowledge base to an agent.").option("--agent <id>", "Agent ID").option("--config <json>", "Retrieval config (topK, score threshold, \u2026)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4139
+ "after",
4140
+ `
4141
+ Examples:
4142
+ $ thinkwork kb attach kb-runbooks --agent agt-oncall
4143
+ $ thinkwork kb attach kb-runbooks --agent agt-oncall --config '{"topK":5}'
4144
+ `
4145
+ ).action(() => notYetImplemented("kb attach", 2));
4146
+ kb.command("detach <kbId>").description("Detach a knowledge base from an agent.").option("--agent <id>", "Agent ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("kb detach", 2));
4147
+ }
4148
+
4149
+ // src/commands/routine.ts
4150
+ function registerRoutineCommand(program2) {
4151
+ const routine = program2.command("routine").alias("routines").description("Manage routines \u2014 saved workflows, their triggers, and past runs.");
4152
+ routine.command("list").alias("ls").description("List routines in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Filter by agent").option("--team <id>", "Filter by team").option("--status <s>", "ACTIVE | PAUSED | ARCHIVED").action(() => notYetImplemented("routine list", 3));
4153
+ routine.command("get <id>").description("Fetch one routine with its triggers and recent runs.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("routine get", 3));
4154
+ routine.command("create [name]").description("Create a new routine. Walkthrough for missing fields in TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent that runs the routine").option("--team <id>", "Team to route runs to (instead of a single agent)").option("--description <text>").option("--config <json>", "Inline routine config JSON").option("--config-file <path>", "Load routine config from a JSON file").addHelpText(
4155
+ "after",
4156
+ `
4157
+ Examples:
4158
+ $ thinkwork routine create "Nightly digest" --agent agt-editor --config-file routines/digest.json
4159
+ $ thinkwork routine create # interactive walkthrough
4160
+ `
4161
+ ).action(() => notYetImplemented("routine create", 3));
4162
+ routine.command("update <id>").description("Update a routine.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--status <s>", "ACTIVE | PAUSED | ARCHIVED").option("--agent <id>").option("--team <id>").option("--config-file <path>").action(() => notYetImplemented("routine update", 3));
4163
+ routine.command("delete <id>").description("Delete a routine. Past runs and triggers are removed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("routine delete", 3));
4164
+ routine.command("trigger <id>").description("Trigger a routine run now (ad-hoc, outside its scheduled cadence).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--wait", "Block until the run finishes, then print result").option("--input <json>", "Optional input payload").action(() => notYetImplemented("routine trigger", 3));
4165
+ const run2 = routine.command("run").description("Inspect routine run history.");
4166
+ run2.command("list <routineId>").alias("ls").description("List recent runs of a routine.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max rows", "25").option("--cursor <c>", "Pagination cursor").action(() => notYetImplemented("routine run list", 3));
4167
+ run2.command("get <runId>").description("Fetch one run with its step outputs.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("routine run get", 3));
4168
+ const trigger = routine.command("trigger-config").description("Manage a routine's triggers (cron, webhook, event).");
4169
+ trigger.command("set <routineId>").description("Set or replace a trigger for a routine.").option("--type <t>", "CRON | WEBHOOK | EVENT").option("--schedule <cron>", "Cron expression (for CRON triggers)").option("--event <name>", "Event name (for EVENT triggers)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4170
+ "after",
4171
+ `
4172
+ Examples:
4173
+ $ thinkwork routine trigger-config set rtn-digest --type CRON --schedule "0 9 * * *"
4174
+ `
4175
+ ).action(() => notYetImplemented("routine trigger-config set", 3));
4176
+ trigger.command("delete <triggerId>").description("Remove a trigger.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("routine trigger-config delete", 3));
4177
+ }
4178
+
4179
+ // src/commands/scheduled-job.ts
4180
+ function registerScheduledJobCommand(program2) {
4181
+ const job = program2.command("scheduled-job").alias("cron").description("Manage AWS-Scheduler-backed recurring agent jobs (wakeups on a cadence).");
4182
+ job.command("list").alias("ls").description("List scheduled jobs for the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Filter by agent").option("--routine <id>", "Filter by routine").option("--enabled <bool>", "true | false").action(() => notYetImplemented("scheduled-job list", 3));
4183
+ job.command("get <id>").description("Fetch one scheduled job.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("scheduled-job get", 3));
4184
+ job.command("create [name]").description("Create a new scheduled job. Supports cron() or rate() schedules.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent to wake up").option("--routine <id>", "Or: routine to trigger").option("--schedule <expr>", "EventBridge schedule (cron(\u2026) or rate(\u2026))").option("--timezone <tz>", "IANA timezone (default: UTC)", "UTC").option("--payload <json>", "Payload to pass to the agent/routine").option("--disabled", "Create in disabled state (enable later with update)").addHelpText(
4185
+ "after",
4186
+ `
4187
+ Examples:
4188
+ $ thinkwork scheduled-job create "Daily ops digest" \\
4189
+ --agent agt-editor --schedule "cron(0 9 * * ? *)" --timezone America/New_York
4190
+
4191
+ # rate() \u2014 note rate means "every N time from creation", NOT wall-clock.
4192
+ $ thinkwork scheduled-job create "Hourly check" --agent agt-check --schedule "rate(1 hour)"
4193
+ `
4194
+ ).action(() => notYetImplemented("scheduled-job create", 3));
4195
+ job.command("update <id>").description("Update a scheduled job's schedule, payload, or enabled state.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--schedule <expr>").option("--timezone <tz>").option("--payload <json>").option("--enable").option("--disable").action(() => notYetImplemented("scheduled-job update", 3));
4196
+ job.command("delete <id>").description("Delete a scheduled job. The underlying EventBridge rule is removed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("scheduled-job delete", 3));
4197
+ job.command("run <id>").description("Trigger a scheduled job immediately (ad-hoc; ignores the schedule).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--wait", "Block until the run completes").action(() => notYetImplemented("scheduled-job run", 3));
4198
+ }
4199
+
4200
+ // src/commands/turn.ts
4201
+ function registerTurnCommand(program2) {
4202
+ const turn = program2.command("turn").alias("turns").description("Inspect and cancel agent invocations (thread turns).");
4203
+ turn.command("list").alias("ls").description("List recent thread turns across the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Filter by agent").option("--routine <id>", "Filter by routine").option("--trigger <id>", "Filter by trigger ID").option("--thread <id>", "Filter by thread").option("--status <s>", "QUEUED | RUNNING | SUCCEEDED | FAILED | CANCELLED").option("--limit <n>", "Max rows", "50").addHelpText(
4204
+ "after",
4205
+ `
4206
+ Examples:
4207
+ # What's running right now?
4208
+ $ thinkwork turn list --status RUNNING
4209
+
4210
+ # Recent failures for one agent
4211
+ $ thinkwork turn list --agent agt-ops --status FAILED --limit 20
4212
+ `
4213
+ ).action(() => notYetImplemented("turn list", 3));
4214
+ turn.command("get <id>").description("Fetch one thread turn with its event stream.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("turn get", 3));
4215
+ turn.command("cancel <id>").description("Cancel an in-progress thread turn. No-op if already finished.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("turn cancel", 3));
4216
+ }
4217
+
4218
+ // src/commands/wakeup.ts
4219
+ function registerWakeupCommand(program2) {
4220
+ const wake = program2.command("wakeup").alias("wakeups").description("View and create agent wakeup requests (deferred/enqueued invocations).");
4221
+ wake.command("list").alias("ls").description("List queued wakeups in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("wakeup list", 3));
4222
+ wake.command("create").description("Queue a wakeup for an agent (ad-hoc or deferred).").option("--agent <id>", "Target agent").option("--thread <id>", "Thread to operate on (optional)").option("--delay-seconds <n>", "Wait N seconds before firing", "0").option("--payload <json>", "Optional input payload").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4223
+ "after",
4224
+ `
4225
+ Examples:
4226
+ $ thinkwork wakeup create --agent agt-ops --thread thr-abc
4227
+ $ thinkwork wakeup create --agent agt-ops --delay-seconds 900 # fire in 15 min
4228
+ `
4229
+ ).action(() => notYetImplemented("wakeup create", 3));
4230
+ }
4231
+
4232
+ // src/commands/webhook.ts
4233
+ function registerWebhookCommand(program2) {
4234
+ const wh = program2.command("webhook").alias("webhooks").description("Manage inbound webhooks that dispatch to agents or routines.");
4235
+ wh.command("list").alias("ls").description("List webhooks in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--enabled <bool>", "true | false").option("--target-type <t>", "AGENT | ROUTINE").action(() => notYetImplemented("webhook list", 3));
4236
+ wh.command("get <id>").description("Fetch one webhook including its secret prefix + rate limit.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("webhook get", 3));
4237
+ wh.command("create [name]").description("Create a new webhook. The full secret is printed once.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--target-type <t>", "AGENT | ROUTINE").option("--target-id <id>", "ID of the agent or routine").option("--rate-limit <rpm>", "Max requests per minute").option("--allowed-ips <csv>", "Restrict to a CIDR list").option("--disabled", "Create in disabled state").addHelpText(
4238
+ "after",
4239
+ `
4240
+ Examples:
4241
+ $ thinkwork webhook create "GitHub PR opened" \\
4242
+ --target-type AGENT --target-id agt-reviewer --rate-limit 30
4243
+
4244
+ # CI use \u2014 capture the secret on create
4245
+ $ thinkwork webhook create "CI" --target-type ROUTINE --target-id rtn-ci --json | jq -r .secret
4246
+ `
4247
+ ).action(() => notYetImplemented("webhook create", 3));
4248
+ wh.command("update <id>").description("Update a webhook's target, rate limit, or enabled state.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--target-type <t>").option("--target-id <id>").option("--rate-limit <rpm>").option("--allowed-ips <csv>").option("--enable").option("--disable").action(() => notYetImplemented("webhook update", 3));
4249
+ wh.command("delete <id>").description("Delete a webhook (its URL stops working immediately).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("webhook delete", 3));
4250
+ wh.command("test <id>").description("Send a synthetic payload to the webhook and print the resulting run ID.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--payload <json>").action(() => notYetImplemented("webhook test", 3));
4251
+ wh.command("rotate <id>").description("Generate a new secret for an existing webhook. Old secret is invalidated.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("webhook rotate", 3));
4252
+ wh.command("deliveries <id>").description("Show recent delivery attempts (success/failure, response status).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max rows", "25").action(() => notYetImplemented("webhook deliveries", 3));
4253
+ }
4254
+
4255
+ // src/commands/connector.ts
4256
+ function registerConnectorCommand(program2) {
4257
+ const conn = program2.command("connector").alias("connectors").description("Manage third-party integrations (Slack, GitHub, Linear, \u2026).");
4258
+ conn.command("list").alias("ls").description("List available connectors and which are enabled for this tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--enabled-only", "Only show tenant-enabled connectors").action(() => notYetImplemented("connector list", 3));
4259
+ conn.command("get <slug>").description("Fetch one connector with its config schema + current status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("connector get", 3));
4260
+ conn.command("enable <slug>").description("Enable a connector. Opens the OAuth flow in your browser when required.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--config <json>", "Inline config JSON (for API-key style connectors)").option("--config-file <path>", "Load config from a file").addHelpText(
4261
+ "after",
4262
+ `
4263
+ Examples:
4264
+ # OAuth connector (Slack)
4265
+ $ thinkwork connector enable slack
4266
+
4267
+ # API-key connector (inline)
4268
+ $ thinkwork connector enable linear --config '{"apiKey":"lin_\u2026"}'
4269
+ `
4270
+ ).action(() => notYetImplemented("connector enable", 3));
4271
+ conn.command("disable <slug>").description("Disable a connector. Stored credentials are cleared.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("connector disable", 3));
4272
+ conn.command("test <slug>").description("Round-trip the connector's credentials against its API. Prints pass/fail + latency.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("connector test", 3));
4273
+ }
4274
+
4275
+ // src/commands/skill.ts
4276
+ function registerSkillCommand(program2) {
4277
+ const skill = program2.command("skill").alias("skills").description("Browse the catalog, install, upgrade, or publish custom skills.");
4278
+ skill.command("catalog").description("Browse the public skill catalog (not tenant-scoped).").option("-s, --stage <name>", "Deployment stage").option("--search <q>", "Filter by keyword").option("--tag <t>", "Filter by tag").action(() => notYetImplemented("skill catalog", 3));
4279
+ skill.command("list").alias("ls").description("List skills installed / published in the current tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--custom-only", "Only show tenant-owned custom skills").action(() => notYetImplemented("skill list", 3));
4280
+ skill.command("install <slug>").description("Install a public skill into the tenant. Idempotent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--version <v>", "Pin to a specific version (default: latest)").addHelpText(
4281
+ "after",
4282
+ `
4283
+ Examples:
4284
+ $ thinkwork skill install web-search
4285
+ $ thinkwork skill install pagerduty --version 1.4.2
4286
+ `
4287
+ ).action(() => notYetImplemented("skill install", 3));
4288
+ skill.command("upgrade <slug>").description("Upgrade an installed skill to the latest catalog version.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("skill upgrade", 3));
4289
+ skill.command("create [slug]").description("Publish a custom tenant-scoped skill (walkthrough for missing fields in TTY).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--manifest-file <path>", "Path to the MCP server manifest JSON").option("--endpoint <url>", "MCP server HTTP/SSE endpoint").addHelpText(
4290
+ "after",
4291
+ `
4292
+ Examples:
4293
+ $ thinkwork skill create my-skill --manifest-file ./skills/my-skill.json
4294
+ $ thinkwork skill create # interactive
4295
+ `
4296
+ ).action(() => notYetImplemented("skill create", 3));
4297
+ skill.command("update <slug>").description("Update a custom skill's manifest, endpoint, or description.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--manifest-file <path>").option("--endpoint <url>").action(() => notYetImplemented("skill update", 3));
4298
+ skill.command("delete <slug>").description("Delete a custom skill. Public catalog skills are uninstalled via this too.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("skill delete", 3));
4299
+ }
4300
+
4301
+ // src/commands/memory.ts
4302
+ function registerMemoryCommand(program2) {
4303
+ const memory = program2.command("memory").description("Inspect, search, and edit an agent's memory records and graph.");
4304
+ memory.command("list").alias("ls").description("List memory records for an agent in a namespace.").option("--agent <id>", "Agent (assistant) ID").option(
4305
+ "--namespace <ns>",
4306
+ "Memory namespace (semantic | preferences | episodes | reflections)",
4307
+ "semantic"
4308
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory list", 4));
4309
+ memory.command("search").description("Search an agent's memory by query string.").option("--agent <id>", "Agent (assistant) ID").option("--query <q>", "Search query").option("--strategy <s>", "Retrieval strategy (semantic | keyword | hybrid)", "semantic").option("--limit <n>", "Max results", "10").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4310
+ "after",
4311
+ `
4312
+ Examples:
4313
+ $ thinkwork memory search --agent agt-ops --query "escalation procedure"
4314
+ $ thinkwork memory search --agent agt-ops --query "p0 runbook" --strategy hybrid --json
4315
+ `
4316
+ ).action(() => notYetImplemented("memory search", 4));
4317
+ memory.command("get <recordId>").description("Fetch one memory record in full.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory get", 4));
4318
+ memory.command("update <recordId>").description("Replace a memory record's content.").option("--content <text>", "New content").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory update", 4));
4319
+ memory.command("delete <recordId>").description("Remove a memory record.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("memory delete", 4));
4320
+ memory.command("graph").description("Print the agent's memory graph (summary in human mode; full JSON with --json).").option("--agent <id>", "Agent (assistant) ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory graph", 4));
4321
+ }
4322
+
4323
+ // src/commands/recipe.ts
4324
+ function registerRecipeCommand(program2) {
4325
+ const recipe = program2.command("recipe").alias("recipes").description("Manage saved MCP tool invocations (parameterized one-click actions).");
4326
+ recipe.command("list").alias("ls").description("List recipes in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--thread <id>", "Filter by thread scope").option("--agent <id>", "Filter by agent scope").option("--limit <n>", "Max rows", "25").action(() => notYetImplemented("recipe list", 4));
4327
+ recipe.command("get <id>").description("Fetch one recipe.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("recipe get", 4));
4328
+ recipe.command("create [name]").description("Save a new recipe. Walkthrough for missing fields in TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--tool <slug>", "MCP tool slug").option("--params <json>", "Default parameters").option("--scope <s>", "tenant | agent | thread", "tenant").option("--agent <id>", "Required if --scope=agent").option("--thread <id>", "Required if --scope=thread").addHelpText(
4329
+ "after",
4330
+ `
4331
+ Examples:
4332
+ $ thinkwork recipe create "Create PagerDuty incident" \\
4333
+ --tool pagerduty.create_incident --params '{"urgency":"high"}'
4334
+ `
4335
+ ).action(() => notYetImplemented("recipe create", 4));
4336
+ recipe.command("update <id>").description("Update a recipe's name, tool, or default params.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--tool <slug>").option("--params <json>").action(() => notYetImplemented("recipe update", 4));
4337
+ recipe.command("delete <id>").description("Delete a recipe.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("recipe delete", 4));
4338
+ }
4339
+
4340
+ // src/commands/artifact.ts
4341
+ function registerArtifactCommand(program2) {
4342
+ const art = program2.command("artifact").alias("artifacts").description("List and fetch agent-produced artifacts (notes, reports, data-views, plans, drafts).");
4343
+ art.command("list").alias("ls").description("List artifacts in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--thread <id>", "Filter by thread").option("--agent <id>", "Filter by producing agent").option(
4344
+ "--type <t>",
4345
+ "DATA_VIEW | NOTE | REPORT | PLAN | DRAFT | DIGEST"
4346
+ ).option("--status <s>", "DRAFT | FINAL | SUPERSEDED").option("--limit <n>", "Max rows", "25").option("--cursor <c>", "Pagination cursor").addHelpText(
4347
+ "after",
4348
+ `
4349
+ Examples:
4350
+ $ thinkwork artifact list --agent agt-editor --type REPORT
4351
+ $ thinkwork artifact list --thread thr-abc --json
4352
+ `
4353
+ ).action(() => notYetImplemented("artifact list", 4));
4354
+ art.command("get <id>").description("Fetch one artifact. Human mode prints a preview; --json returns the full content.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--raw", "Print only the markdown body to stdout (for piping to pandoc / bat / less)").action(() => notYetImplemented("artifact get", 4));
4355
+ }
4356
+
4357
+ // src/commands/cost.ts
4358
+ function registerCostCommand(program2) {
4359
+ const cost = program2.command("cost").description("Spend reports \u2014 totals, per-agent, per-model, and daily series.");
4360
+ cost.command("summary").description("Total spend for the tenant over an optional date range.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from <iso>", "Start date (ISO-8601)").option("--to <iso>", "End date (ISO-8601)").addHelpText(
4361
+ "after",
4362
+ `
4363
+ Examples:
4364
+ # MTD spend
4365
+ $ thinkwork cost summary
4366
+
4367
+ # Specific window
4368
+ $ thinkwork cost summary --from 2026-04-01 --to 2026-04-30 --json
4369
+ `
4370
+ ).action(() => notYetImplemented("cost summary", 5));
4371
+ cost.command("by-agent").description("Spend broken down by agent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from <iso>").option("--to <iso>").option("--sort <field>", "cost | requests (default cost)", "cost").action(() => notYetImplemented("cost by-agent", 5));
4372
+ cost.command("by-model").description("Spend broken down by model ID (tokens + cost).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from <iso>").option("--to <iso>").action(() => notYetImplemented("cost by-model", 5));
4373
+ cost.command("series").description("Daily cost series.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--days <n>", "Days of history", "30").action(() => notYetImplemented("cost series", 5));
4374
+ }
4375
+
4376
+ // src/commands/budget.ts
4377
+ function registerBudgetCommand(program2) {
4378
+ const budget = program2.command("budget").alias("budgets").description("Manage budget policies (tenant-wide or per-agent) and inspect current status.");
4379
+ budget.command("list").alias("ls").description("List budget policies in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("budget list", 5));
4380
+ budget.command("status").description("Show each budget's current spend vs. limit.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("budget status", 5));
4381
+ budget.command("upsert").description("Create or update a budget policy.").option("--limit-usd <amount>", "USD ceiling for the window").option("--scope <s>", "tenant | agent", "tenant").option("--agent <id>", "Required if --scope=agent").option("--window <w>", "daily | weekly | monthly", "monthly").option("--action <a>", "PAUSE | ALERT", "PAUSE").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4382
+ "after",
4383
+ `
4384
+ Examples:
4385
+ # Tenant-wide $5k/month pause
4386
+ $ thinkwork budget upsert --limit-usd 5000 --window monthly --action PAUSE
4387
+
4388
+ # Per-agent alert-only
4389
+ $ thinkwork budget upsert --scope agent --agent agt-ops --limit-usd 500 --action ALERT
4390
+ `
4391
+ ).action(() => notYetImplemented("budget upsert", 5));
4392
+ budget.command("delete <id>").description("Remove a budget policy.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("budget delete", 5));
4393
+ }
4394
+
4395
+ // src/commands/performance.ts
4396
+ function registerPerformanceCommand(program2) {
4397
+ const perf = program2.command("performance").alias("perf").description("Observability: per-agent invocations, errors, p95 latency, and cost.");
4398
+ perf.command("agents").description("Performance summary for every agent in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from <iso>").option("--to <iso>").option("--sort <f>", "cost | errors | latency | requests", "errors").action(() => notYetImplemented("performance agents", 5));
4399
+ perf.command("agent <id>").description("Performance detail for one agent (including daily time-series).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--days <n>", "Time-series history", "14").action(() => notYetImplemented("performance agent", 5));
4400
+ }
4401
+
4402
+ // src/commands/trace.ts
4403
+ function registerTraceCommand(program2) {
4404
+ const trace = program2.command("trace").description("Inspect LLM invocations (traces) for a thread or turn.");
4405
+ trace.command("thread <threadId>").description("All LLM invocations across every turn of one thread.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--since <iso>").action(() => notYetImplemented("trace thread", 5));
4406
+ trace.command("turn <turnId>").description("LLM invocations for a single thread-turn (verbose \u2014 prompt + response + metadata).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--raw", "Print raw prompts + responses as a JSON array (useful for piping)").addHelpText(
4407
+ "after",
4408
+ `
4409
+ Examples:
4410
+ $ thinkwork trace turn ttn-abc --json | jq '.[].model'
4411
+ $ thinkwork trace turn ttn-abc --raw | jq '.[].response'
4412
+ `
4413
+ ).action(() => notYetImplemented("trace turn", 5));
4414
+ }
4415
+
4416
+ // src/commands/dashboard.ts
4417
+ function registerDashboardCommand(program2) {
4418
+ program2.command("dashboard").alias("overview").description("One-screen snapshot of the tenant \u2014 agents, open threads, approvals, spend.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4419
+ "after",
4420
+ `
4421
+ Examples:
4422
+ # Print the dashboard for the default tenant
4423
+ $ thinkwork dashboard
4424
+
4425
+ # Check a specific stage
4426
+ $ thinkwork dashboard --stage prod
4427
+ `
4428
+ ).action(() => notYetImplemented("dashboard", 5));
4429
+ }
4430
+
4431
+ // src/cli.ts
4432
+ var program = new Command();
4433
+ program.name("thinkwork").description(
4434
+ "Thinkwork CLI \u2014 deploy, manage, and interact with your Thinkwork stack"
4435
+ ).version(VERSION, "-v, --version", "Print the CLI version").option(
4436
+ "-p, --profile <name>",
4437
+ "AWS profile to use (sets AWS_PROFILE for Terraform and AWS CLI)"
4438
+ ).option(
4439
+ "--json",
4440
+ "Emit machine-readable JSON on stdout. Warnings/spinners stay on stderr."
4441
+ );
4442
+ program.hook("preAction", (_thisCommand, actionCommand) => {
4443
+ const explicit = actionCommand.opts().profile ?? program.opts().profile;
4444
+ if (explicit) {
4445
+ process.env.AWS_PROFILE = explicit;
4446
+ } else if (!process.env.AWS_PROFILE) {
4447
+ const fallback = loadCliConfig().defaultProfile;
4448
+ if (fallback) process.env.AWS_PROFILE = fallback;
4449
+ }
4450
+ const jsonGlobal = Boolean(program.opts().json);
4451
+ const jsonLocal = Boolean(actionCommand.opts().json);
4452
+ setJsonMode(jsonGlobal || jsonLocal);
2724
4453
  });
2725
4454
  registerLoginCommand(program);
4455
+ registerLogoutCommand(program);
2726
4456
  registerInitCommand(program);
2727
4457
  registerDoctorCommand(program);
4458
+ registerMeCommand(program);
2728
4459
  registerPlanCommand(program);
2729
4460
  registerDeployCommand(program);
2730
4461
  registerBootstrapCommand(program);
@@ -2736,4 +4467,34 @@ registerMcpCommand(program);
2736
4467
  registerToolsCommand(program);
2737
4468
  registerUpdateCommand(program);
2738
4469
  registerUserCommand(program);
4470
+ registerThreadCommand(program);
4471
+ registerMessageCommand(program);
4472
+ registerLabelCommand(program);
4473
+ registerInboxCommand(program);
4474
+ registerAgentCommand(program);
4475
+ registerTemplateCommand(program);
4476
+ registerTenantCommand(program);
4477
+ registerMemberCommand(program);
4478
+ registerTeamCommand(program);
4479
+ registerKbCommand(program);
4480
+ registerRoutineCommand(program);
4481
+ registerScheduledJobCommand(program);
4482
+ registerTurnCommand(program);
4483
+ registerWakeupCommand(program);
4484
+ registerWebhookCommand(program);
4485
+ registerConnectorCommand(program);
4486
+ registerSkillCommand(program);
4487
+ registerMemoryCommand(program);
4488
+ registerRecipeCommand(program);
4489
+ registerArtifactCommand(program);
4490
+ registerCostCommand(program);
4491
+ registerBudgetCommand(program);
4492
+ registerPerformanceCommand(program);
4493
+ registerTraceCommand(program);
4494
+ registerDashboardCommand(program);
4495
+ for (const cmd of program.commands) {
4496
+ if (!cmd.options.some((o) => o.long === "--json")) {
4497
+ cmd.option("--json", "Emit machine-readable JSON on stdout.");
4498
+ }
4499
+ }
2739
4500
  program.parse();