thinkwork-cli 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +928 -565
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -251,41 +251,141 @@ function printSummary(command, stage, tiers, startTime) {
251
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"));
252
252
  }
253
253
 
254
- // src/commands/plan.ts
255
- function registerPlanCommand(program2) {
256
- 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) => {
257
- const startTime = Date.now();
258
- const stageCheck = validateStage(opts.stage);
259
- if (!stageCheck.valid) {
260
- printError(stageCheck.error);
261
- 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]);
262
281
  }
263
- const compCheck = validateComponent(opts.component);
264
- if (!compCheck.valid) {
265
- 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);
266
331
  process.exit(1);
267
332
  }
268
- const identity = getAwsIdentity();
269
- printHeader("plan", opts.stage, identity);
270
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
271
- const tiers = expandComponent(opts.component);
272
- for (let i = 0; i < tiers.length; i++) {
273
- const tier = tiers[i];
274
- printTierHeader(tier, i, tiers.length);
275
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
276
- await ensureInit(cwd);
277
- await ensureWorkspace(cwd, opts.stage);
278
- const code = await runTerraform(cwd, [
279
- "plan",
280
- `-var=stage=${opts.stage}`
281
- ]);
282
- if (code !== 0) {
283
- printError(`Plan failed for ${tier} (exit ${code})`);
284
- 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
+ }
285
382
  }
383
+ printSuccess("Plan complete");
384
+ printSummary("plan", stage, tiers, startTime);
385
+ } catch (err) {
386
+ if (isCancellation(err)) return;
387
+ throw err;
286
388
  }
287
- printSuccess("Plan complete");
288
- printSummary("plan", opts.stage, tiers, startTime);
289
389
  });
290
390
  }
291
391
 
@@ -306,131 +406,129 @@ async function confirm(message) {
306
406
 
307
407
  // src/commands/deploy.ts
308
408
  function registerDeployCommand(program2) {
309
- 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) => {
310
410
  const startTime = Date.now();
311
- const stageCheck = validateStage(opts.stage);
312
- if (!stageCheck.valid) {
313
- printError(stageCheck.error);
314
- process.exit(1);
315
- }
316
- const compCheck = validateComponent(opts.component);
317
- if (!compCheck.valid) {
318
- printError(compCheck.error);
319
- process.exit(1);
320
- }
321
- const identity = getAwsIdentity();
322
- printHeader("deploy", opts.stage, identity);
323
- if (!identity) {
324
- printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
325
- }
326
- if (isProdLike(opts.stage) && !opts.yes) {
327
- const ok = await confirm(
328
- ` Stage "${opts.stage}" is production-like. Deploy?`
329
- );
330
- if (!ok) {
331
- console.log(" Aborted.");
332
- 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);
333
417
  }
334
- } else if (!opts.yes) {
335
- const ok = await confirm(` Deploy to stage "${opts.stage}"?`);
336
- if (!ok) {
337
- console.log(" Aborted.");
338
- 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?");
339
422
  }
340
- }
341
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
342
- const tiers = expandComponent(opts.component);
343
- for (let i = 0; i < tiers.length; i++) {
344
- const tier = tiers[i];
345
- printTierHeader(tier, i, tiers.length);
346
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
347
- await ensureInit(cwd);
348
- await ensureWorkspace(cwd, opts.stage);
349
- const code = await runTerraform(cwd, [
350
- "apply",
351
- "-auto-approve",
352
- `-var=stage=${opts.stage}`
353
- ]);
354
- if (code !== 0) {
355
- printError(`Deploy failed for ${tier} (exit ${code})`);
356
- 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
+ }
357
453
  }
454
+ printSuccess("Deploy complete");
455
+ printSummary("deploy", stage, tiers, startTime);
456
+ } catch (err) {
457
+ if (isCancellation(err)) return;
458
+ throw err;
358
459
  }
359
- printSuccess("Deploy complete");
360
- printSummary("deploy", opts.stage, tiers, startTime);
361
460
  });
362
461
  }
363
462
 
364
463
  // src/commands/destroy.ts
365
464
  function registerDestroyCommand(program2) {
366
- 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) => {
367
466
  const startTime = Date.now();
368
- const stageCheck = validateStage(opts.stage);
369
- if (!stageCheck.valid) {
370
- printError(stageCheck.error);
371
- process.exit(1);
372
- }
373
- const compCheck = validateComponent(opts.component);
374
- if (!compCheck.valid) {
375
- printError(compCheck.error);
376
- process.exit(1);
377
- }
378
- const identity = getAwsIdentity();
379
- printHeader("destroy", opts.stage, identity);
380
- if (!identity) {
381
- printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
382
- }
383
- if (isProdLike(opts.stage)) {
384
- printWarning(`Stage "${opts.stage}" is production-like.`);
385
- if (!opts.yes) {
386
- const ok = await confirm(
387
- ` Type 'y' to confirm destruction of stage "${opts.stage}":`
388
- );
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}"?`);
389
491
  if (!ok) {
390
492
  console.log(" Aborted.");
391
493
  process.exit(0);
392
494
  }
393
495
  }
394
- console.log(` Proceeding with destroy of "${opts.stage}" (--yes provided).`);
395
- } else if (!opts.yes) {
396
- const ok = await confirm(` Destroy stage "${opts.stage}"?`);
397
- if (!ok) {
398
- console.log(" Aborted.");
399
- process.exit(0);
400
- }
401
- }
402
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
403
- const tiers = expandComponent(opts.component).reverse();
404
- for (let i = 0; i < tiers.length; i++) {
405
- const tier = tiers[i];
406
- printTierHeader(tier, i, tiers.length);
407
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
408
- await ensureInit(cwd);
409
- await ensureWorkspace(cwd, opts.stage);
410
- const code = await runTerraform(cwd, [
411
- "destroy",
412
- "-auto-approve",
413
- `-var=stage=${opts.stage}`
414
- ]);
415
- if (code !== 0) {
416
- printError(`Destroy failed for ${tier} (exit ${code})`);
417
- 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
+ }
418
513
  }
514
+ printSuccess("Destroy complete");
515
+ printSummary("destroy", stage, tiers, startTime);
516
+ } catch (err) {
517
+ if (isCancellation(err)) return;
518
+ throw err;
419
519
  }
420
- printSuccess("Destroy complete");
421
- printSummary("destroy", opts.stage, tiers, startTime);
422
520
  });
423
521
  }
424
522
 
425
523
  // src/commands/doctor.ts
426
524
  import chalk3 from "chalk";
427
- import { execSync as execSync2 } from "child_process";
525
+ import { execSync as execSync3 } from "child_process";
428
526
  function checkAwsCli() {
429
527
  return {
430
528
  name: "AWS CLI installed",
431
529
  run: () => {
432
530
  try {
433
- 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();
434
532
  return { pass: true, detail: v.split(" ")[0] ?? v };
435
533
  } catch {
436
534
  return { pass: false, detail: "aws CLI not found. Install: https://aws.amazon.com/cli/" };
@@ -443,7 +541,7 @@ function checkTerraformCli() {
443
541
  name: "Terraform CLI installed",
444
542
  run: () => {
445
543
  try {
446
- 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"] });
447
545
  const parsed = JSON.parse(v);
448
546
  return { pass: true, detail: `v${parsed.terraform_version}` };
449
547
  } catch {
@@ -469,7 +567,7 @@ function checkBedrockAccess() {
469
567
  name: "Bedrock model access",
470
568
  run: () => {
471
569
  try {
472
- execSync2(
570
+ execSync3(
473
571
  "aws bedrock get-foundation-model --model-identifier anthropic.claude-3-haiku-20240307-v1:0 --output json --region us-east-1",
474
572
  { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
475
573
  );
@@ -484,13 +582,15 @@ function checkBedrockAccess() {
484
582
  };
485
583
  }
486
584
  function registerDoctorCommand(program2) {
487
- 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) => {
488
- const stageCheck = validateStage(opts.stage);
489
- if (!stageCheck.valid) {
490
- printError(stageCheck.error);
491
- 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;
492
592
  }
493
- printHeader("doctor", opts.stage);
593
+ printHeader("doctor", stage);
494
594
  const checks = [
495
595
  checkAwsCli(),
496
596
  checkTerraformCli(),
@@ -518,31 +618,32 @@ function registerDoctorCommand(program2) {
518
618
 
519
619
  // src/commands/outputs.ts
520
620
  function registerOutputsCommand(program2) {
521
- 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) => {
522
- const stageCheck = validateStage(opts.stage);
523
- if (!stageCheck.valid) {
524
- printError(stageCheck.error);
525
- process.exit(1);
526
- }
527
- const compCheck = validateComponent(opts.component);
528
- if (!compCheck.valid) {
529
- printError(compCheck.error);
530
- process.exit(1);
531
- }
532
- printHeader("outputs", opts.stage);
533
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
534
- const tiers = expandComponent(opts.component);
535
- for (let i = 0; i < tiers.length; i++) {
536
- const tier = tiers[i];
537
- printTierHeader(tier, i, tiers.length);
538
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
539
- await ensureInit(cwd);
540
- await ensureWorkspace(cwd, opts.stage);
541
- const code = await runTerraform(cwd, ["output"]);
542
- if (code !== 0) {
543
- printError(`Outputs failed for ${tier} (exit ${code})`);
544
- 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);
545
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
+ }
643
+ }
644
+ } catch (err) {
645
+ if (isCancellation(err)) return;
646
+ throw err;
546
647
  }
547
648
  });
548
649
  }
@@ -701,13 +802,15 @@ function registerConfigCommand(program2) {
701
802
  console.log(` Show details: ${chalk4.cyan("thinkwork config list -s <stage>")}`);
702
803
  console.log("");
703
804
  });
704
- config.command("get <key>").description("Get a configuration value (e.g. enable-hindsight)").requiredOption("-s, --stage <name>", "Deployment stage").action((key, opts) => {
705
- const stageCheck = validateStage(opts.stage);
706
- if (!stageCheck.valid) {
707
- printError(stageCheck.error);
708
- 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;
709
812
  }
710
- const tfvarsPath = resolveTfvarsPath(opts.stage);
813
+ const tfvarsPath = resolveTfvarsPath(stage);
711
814
  const tfKey = key.replace(/-/g, "_");
712
815
  const value = readTfVar(tfvarsPath, tfKey);
713
816
  if (value === null) {
@@ -716,11 +819,13 @@ function registerConfigCommand(program2) {
716
819
  console.log(` ${key} = ${value}`);
717
820
  }
718
821
  });
719
- 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) => {
720
- const stageCheck = validateStage(opts.stage);
721
- if (!stageCheck.valid) {
722
- printError(stageCheck.error);
723
- 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;
724
829
  }
725
830
  let tfKey = key.replace(/-/g, "_");
726
831
  let tfValue = value;
@@ -738,13 +843,13 @@ function registerConfigCommand(program2) {
738
843
  process.exit(1);
739
844
  }
740
845
  const identity = getAwsIdentity();
741
- printHeader("config set", opts.stage, identity);
742
- const tfvarsPath = resolveTfvarsPath(opts.stage);
846
+ printHeader("config set", stage, identity);
847
+ const tfvarsPath = resolveTfvarsPath(stage);
743
848
  const oldValue = readTfVar(tfvarsPath, tfKey);
744
849
  setTfVar(tfvarsPath, tfKey, tfValue);
745
850
  console.log(` ${tfKey}: ${oldValue ?? "(unset)"} \u2192 ${tfValue}`);
746
851
  if (opts.apply) {
747
- const tfDir = resolveTerraformDir(opts.stage);
852
+ const tfDir = resolveTerraformDir(stage);
748
853
  if (!tfDir) {
749
854
  printError("Cannot find terraform directory. Run `thinkwork init` first.");
750
855
  process.exit(1);
@@ -752,11 +857,11 @@ function registerConfigCommand(program2) {
752
857
  console.log("");
753
858
  console.log(" Applying configuration change...");
754
859
  await ensureInit(tfDir);
755
- await ensureWorkspace(tfDir, opts.stage);
860
+ await ensureWorkspace(tfDir, stage);
756
861
  const code = await runTerraform(tfDir, [
757
862
  "apply",
758
863
  "-auto-approve",
759
- `-var=stage=${opts.stage}`
864
+ `-var=stage=${stage}`
760
865
  ]);
761
866
  if (code !== 0) {
762
867
  printError(`Deploy failed (exit ${code})`);
@@ -796,18 +901,20 @@ function runScript(scriptPath, args) {
796
901
  });
797
902
  }
798
903
  function registerBootstrapCommand(program2) {
799
- 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) => {
800
- const stageCheck = validateStage(opts.stage);
801
- if (!stageCheck.valid) {
802
- printError(stageCheck.error);
803
- 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;
804
911
  }
805
912
  const identity = getAwsIdentity();
806
- printHeader("bootstrap", opts.stage, identity);
913
+ printHeader("bootstrap", stage, identity);
807
914
  const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
808
- const cwd = resolveTierDir(terraformDir, opts.stage, "app");
915
+ const cwd = resolveTierDir(terraformDir, stage, "app");
809
916
  await ensureInit(cwd);
810
- await ensureWorkspace(cwd, opts.stage);
917
+ await ensureWorkspace(cwd, stage);
811
918
  let bucket;
812
919
  let dbEndpoint;
813
920
  let dbPassword;
@@ -829,7 +936,7 @@ function registerBootstrapCommand(program2) {
829
936
  const databaseUrl = `postgresql://thinkwork_admin:${encodeURIComponent(dbPassword)}@${dbEndpoint}:5432/thinkwork?sslmode=no-verify`;
830
937
  const repoRoot = resolve(terraformDir);
831
938
  const scriptPath = resolve(repoRoot, "scripts/bootstrap-workspace.sh");
832
- const code = await runScript(scriptPath, [opts.stage, bucket, databaseUrl]);
939
+ const code = await runScript(scriptPath, [stage, bucket, databaseUrl]);
833
940
  if (code !== 0) {
834
941
  printError(`Bootstrap failed (exit ${code})`);
835
942
  process.exit(code);
@@ -841,7 +948,7 @@ function registerBootstrapCommand(program2) {
841
948
  // src/commands/login.ts
842
949
  import { execSync as execSync6 } from "child_process";
843
950
  import { createInterface as createInterface2 } from "readline";
844
- import { select, Separator } from "@inquirer/prompts";
951
+ import { select as select2, Separator } from "@inquirer/prompts";
845
952
  import chalk7 from "chalk";
846
953
 
847
954
  // src/aws-profiles.ts
@@ -921,14 +1028,14 @@ function listAwsProfiles() {
921
1028
  }
922
1029
 
923
1030
  // src/prerequisites.ts
924
- import { execSync as execSync3 } from "child_process";
1031
+ import { execSync as execSync4 } from "child_process";
925
1032
  import { mkdirSync as mkdirSync3, createWriteStream, chmodSync } from "fs";
926
1033
  import { join as join4 } from "path";
927
1034
  import { homedir as homedir4, platform, arch } from "os";
928
1035
  import chalk5 from "chalk";
929
1036
  function run(cmd, opts) {
930
1037
  try {
931
- return execSync3(cmd, {
1038
+ return execSync4(cmd, {
932
1039
  encoding: "utf-8",
933
1040
  timeout: 3e4,
934
1041
  stdio: opts?.silent ? ["pipe", "pipe", "pipe"] : void 0
@@ -1042,10 +1149,10 @@ async function ensurePrerequisites() {
1042
1149
  }
1043
1150
 
1044
1151
  // src/cognito-discovery.ts
1045
- import { execSync as execSync4 } from "child_process";
1046
- function runAws(cmd) {
1152
+ import { execSync as execSync5 } from "child_process";
1153
+ function runAws2(cmd) {
1047
1154
  try {
1048
- return execSync4(`aws ${cmd}`, {
1155
+ return execSync5(`aws ${cmd}`, {
1049
1156
  encoding: "utf-8",
1050
1157
  timeout: 15e3,
1051
1158
  stdio: ["pipe", "pipe", "pipe"]
@@ -1065,7 +1172,7 @@ function tryTerraformOutput(stage) {
1065
1172
  }
1066
1173
  const read = (key) => {
1067
1174
  try {
1068
- return execSync4(`terraform output -raw ${key}`, {
1175
+ return execSync5(`terraform output -raw ${key}`, {
1069
1176
  cwd,
1070
1177
  encoding: "utf-8",
1071
1178
  timeout: 15e3,
@@ -1081,7 +1188,7 @@ function tryTerraformOutput(stage) {
1081
1188
  return { userPoolId, clientId, domain };
1082
1189
  }
1083
1190
  function tryAwsDiscovery(stage, region) {
1084
- const listRaw = runAws(
1191
+ const listRaw = runAws2(
1085
1192
  `cognito-idp list-user-pools --max-results 60 --region ${region} --output json`
1086
1193
  );
1087
1194
  if (!listRaw) return {};
@@ -1093,7 +1200,7 @@ function tryAwsDiscovery(stage, region) {
1093
1200
  )
1094
1201
  );
1095
1202
  if (!pool) return {};
1096
- const clientsRaw = runAws(
1203
+ const clientsRaw = runAws2(
1097
1204
  `cognito-idp list-user-pool-clients --user-pool-id ${pool.Id} --region ${region} --output json`
1098
1205
  );
1099
1206
  let clientId;
@@ -1364,83 +1471,24 @@ function escapeHtml(s) {
1364
1471
  });
1365
1472
  }
1366
1473
 
1367
- // src/aws-discovery.ts
1368
- import { execSync as execSync5 } from "child_process";
1369
- function runAws2(cmd) {
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) {
1370
1485
  try {
1371
- return execSync5(`aws ${cmd}`, {
1372
- encoding: "utf-8",
1373
- timeout: 15e3,
1374
- stdio: ["pipe", "pipe", "pipe"]
1375
- }).trim();
1376
- } catch {
1377
- return null;
1378
- }
1379
- }
1380
- function listDeployedStages(region) {
1381
- const raw = runAws2(
1382
- `lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
1383
- );
1384
- if (!raw) return [];
1385
- try {
1386
- const functions = JSON.parse(raw);
1387
- const stages = /* @__PURE__ */ new Set();
1388
- for (const fn of functions) {
1389
- const m = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
1390
- if (m) stages.add(m[1]);
1391
- }
1392
- return [...stages].sort();
1393
- } catch {
1394
- return [];
1395
- }
1396
- }
1397
- function getApiEndpoint(stage, region) {
1398
- const raw = runAws2(
1399
- `apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
1400
- );
1401
- return raw && raw !== "None" ? raw : null;
1402
- }
1403
- function getApiAuthSecretFromLambda(stage, region) {
1404
- const raw = runAws2(
1405
- `lambda get-function-configuration --function-name thinkwork-${stage}-api-tenants --region ${region} --query "Environment.Variables.API_AUTH_SECRET" --output text`
1406
- );
1407
- return raw && raw !== "None" ? raw : null;
1408
- }
1409
-
1410
- // src/lib/interactive.ts
1411
- function isCancellation(err) {
1412
- return err instanceof Error && err.name === "ExitPromptError";
1413
- }
1414
- function isInteractive() {
1415
- return Boolean(process.stdin.isTTY);
1416
- }
1417
- function requireTty(label) {
1418
- if (!isInteractive()) {
1419
- printError(
1420
- `${label} is required. Pass it as a flag or re-run in an interactive terminal.`
1421
- );
1422
- process.exit(1);
1423
- }
1424
- }
1425
-
1426
- // src/commands/login.ts
1427
- function ask(prompt2) {
1428
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
1429
- return new Promise((resolve3) => {
1430
- rl.question(prompt2, (answer) => {
1431
- rl.close();
1432
- resolve3(answer.trim());
1433
- });
1434
- });
1435
- }
1436
- function verifyProfile(profile) {
1437
- try {
1438
- const raw = execSync6(
1439
- `aws sts get-caller-identity --profile ${profile} --output json`,
1440
- { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
1441
- );
1442
- const parsed = JSON.parse(raw);
1443
- return { account: parsed.Account, arn: parsed.Arn };
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 };
1444
1492
  } catch {
1445
1493
  return null;
1446
1494
  }
@@ -1480,7 +1528,7 @@ async function pickProfile(profiles) {
1480
1528
  description: "Run `aws sso login` against the configured SSO profile."
1481
1529
  });
1482
1530
  try {
1483
- const picked = await select({
1531
+ const picked = await select2({
1484
1532
  message: "Pick an AWS profile for Thinkwork:",
1485
1533
  choices,
1486
1534
  loop: false,
@@ -1842,7 +1890,7 @@ Registered callback URL:
1842
1890
  }
1843
1891
 
1844
1892
  // src/commands/logout.ts
1845
- import { select as select2 } from "@inquirer/prompts";
1893
+ import { select as select3 } from "@inquirer/prompts";
1846
1894
  function registerLogoutCommand(program2) {
1847
1895
  program2.command("logout").description(
1848
1896
  "Forget a stored session. Touches only ~/.thinkwork/config.json; your AWS profile and Cognito pool are untouched."
@@ -1880,7 +1928,7 @@ Examples:
1880
1928
  console.log(` Only one session stored: ${stage}`);
1881
1929
  } else {
1882
1930
  requireTty("Stage");
1883
- stage = await select2({
1931
+ stage = await select3({
1884
1932
  message: "Forget which stage's session?",
1885
1933
  choices: keys.map((s) => ({ name: s, value: s })),
1886
1934
  loop: false
@@ -1915,19 +1963,19 @@ import { fileURLToPath } from "url";
1915
1963
  import { createInterface as createInterface3 } from "readline";
1916
1964
  import chalk8 from "chalk";
1917
1965
  var __dirname = dirname2(fileURLToPath(import.meta.url));
1918
- function ask2(prompt2, defaultVal = "") {
1966
+ function ask2(prompt, defaultVal = "") {
1919
1967
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
1920
1968
  const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
1921
1969
  return new Promise((resolve3) => {
1922
- rl.question(` ${prompt2}${suffix}: `, (answer) => {
1970
+ rl.question(` ${prompt}${suffix}: `, (answer) => {
1923
1971
  rl.close();
1924
1972
  resolve3(answer.trim() || defaultVal);
1925
1973
  });
1926
1974
  });
1927
1975
  }
1928
- function choose(prompt2, options, defaultVal) {
1976
+ function choose(prompt, options, defaultVal) {
1929
1977
  const optStr = options.map((o) => o === defaultVal ? chalk8.bold(o) : chalk8.dim(o)).join(" / ");
1930
- return ask2(`${prompt2} (${optStr})`, defaultVal);
1978
+ return ask2(`${prompt} (${optStr})`, defaultVal);
1931
1979
  }
1932
1980
  function generateSecret(length = 32) {
1933
1981
  const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
@@ -1994,14 +2042,34 @@ function buildTfvars(config) {
1994
2042
  return lines.join("\n");
1995
2043
  }
1996
2044
  function registerInitCommand(program2) {
1997
- 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) => {
1998
- 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);
1999
2067
  if (!stageCheck.valid) {
2000
2068
  printError(stageCheck.error);
2001
2069
  process.exit(1);
2002
2070
  }
2003
2071
  const identity = getAwsIdentity();
2004
- printHeader("init", opts.stage, identity);
2072
+ printHeader("init", stage, identity);
2005
2073
  const prereqsOk = await ensurePrerequisites();
2006
2074
  if (!prereqsOk) {
2007
2075
  process.exit(1);
@@ -2023,10 +2091,10 @@ function registerInitCommand(program2) {
2023
2091
  console.log("");
2024
2092
  }
2025
2093
  const config = {
2026
- stage: opts.stage,
2094
+ stage,
2027
2095
  account_id: identity.account,
2028
2096
  db_password: generateSecret(24),
2029
- api_auth_secret: `tw-${opts.stage}-${generateSecret(16)}`
2097
+ api_auth_secret: `tw-${stage}-${generateSecret(16)}`
2030
2098
  };
2031
2099
  if (opts.defaults) {
2032
2100
  config.region = identity.region !== "unknown" ? identity.region : "us-east-1";
@@ -2287,7 +2355,7 @@ output "agentcore_memory_id" {
2287
2355
  try {
2288
2356
  execSync7("terraform init", { cwd: tfDir, stdio: "inherit" });
2289
2357
  } catch {
2290
- 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.");
2291
2359
  return;
2292
2360
  }
2293
2361
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -2301,13 +2369,13 @@ output "agentcore_memory_id" {
2301
2369
  createdAt: now,
2302
2370
  updatedAt: now
2303
2371
  });
2304
- printSuccess(`Environment "${opts.stage}" initialized`);
2372
+ printSuccess(`Environment "${stage}" initialized`);
2305
2373
  console.log("");
2306
2374
  console.log(" Next steps:");
2307
- console.log(` ${chalk8.cyan("1.")} thinkwork plan -s ${opts.stage} ${chalk8.dim("# Review infrastructure plan")}`);
2308
- console.log(` ${chalk8.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk8.dim("# Deploy to AWS (~5 min)")}`);
2309
- console.log(` ${chalk8.cyan("3.")} thinkwork bootstrap -s ${opts.stage} ${chalk8.dim("# Seed workspace files + skills")}`);
2310
- console.log(` ${chalk8.cyan("4.")} thinkwork outputs -s ${opts.stage} ${chalk8.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.")}`);
2311
2379
  console.log("");
2312
2380
  });
2313
2381
  }
@@ -2604,20 +2672,213 @@ function resolveApiConfig(stage, regionOverride) {
2604
2672
  return { apiUrl, authSecret };
2605
2673
  }
2606
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
+
2744
+ // src/lib/resolve-identifier.ts
2745
+ import { select as select5 } from "@inquirer/prompts";
2746
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2747
+ function isUuid(s) {
2748
+ return UUID_RE.test(s);
2749
+ }
2750
+ async function resolveIdentifier(opts) {
2751
+ const { identifier, list, getId, getAliases, resourceLabel } = opts;
2752
+ if (!identifier) {
2753
+ if (!isInteractive()) {
2754
+ requireTty(`${capitalize(resourceLabel)} identifier`);
2755
+ throw new Error("unreachable: requireTty must exit in non-TTY");
2756
+ }
2757
+ const items2 = await list();
2758
+ if (items2.length === 0) {
2759
+ printError(`No ${resourceLabel}s found. Nothing to pick.`);
2760
+ process.exit(1);
2761
+ }
2762
+ if (items2.length === 1) {
2763
+ console.log(` Using the only ${resourceLabel}: ${defaultLabel(items2[0], opts)}`);
2764
+ return items2[0];
2765
+ }
2766
+ const chosenId = await select5({
2767
+ message: `Which ${resourceLabel}?`,
2768
+ choices: items2.map((it) => ({
2769
+ name: (opts.pickerLabel ?? defaultLabelFor(opts))(it),
2770
+ value: getId(it)
2771
+ })),
2772
+ loop: false
2773
+ });
2774
+ return items2.find((it) => getId(it) === chosenId);
2775
+ }
2776
+ const items = await list();
2777
+ if (isUuid(identifier)) {
2778
+ const hit = items.find((it) => getId(it) === identifier);
2779
+ if (hit) return hit;
2780
+ printError(
2781
+ `${capitalize(resourceLabel)} with ID "${identifier}" not found. Available: ${formatAvailable(items, opts)}`
2782
+ );
2783
+ process.exit(1);
2784
+ }
2785
+ const needle = identifier.toLowerCase();
2786
+ const matches = items.filter(
2787
+ (it) => getAliases(it).some((a) => a != null && String(a).toLowerCase() === needle)
2788
+ );
2789
+ if (matches.length === 0) {
2790
+ printError(
2791
+ `${capitalize(resourceLabel)} "${identifier}" not found. Available: ${formatAvailable(items, opts)}`
2792
+ );
2793
+ process.exit(1);
2794
+ }
2795
+ if (matches.length > 1) {
2796
+ printError(
2797
+ `"${identifier}" matches ${matches.length} ${resourceLabel}s. Pass the UUID instead \u2014 candidates: ${matches.map(getId).join(", ")}`
2798
+ );
2799
+ process.exit(1);
2800
+ }
2801
+ return matches[0];
2802
+ }
2803
+ function capitalize(s) {
2804
+ return s.charAt(0).toUpperCase() + s.slice(1);
2805
+ }
2806
+ function defaultLabelFor(opts) {
2807
+ return (item) => defaultLabel(item, opts);
2808
+ }
2809
+ function defaultLabel(item, opts) {
2810
+ const aliases = opts.getAliases(item).filter(Boolean);
2811
+ const primary = aliases[0] ?? "(no name)";
2812
+ return `${primary} (${opts.getId(item)})`;
2813
+ }
2814
+ function formatAvailable(items, opts) {
2815
+ if (items.length === 0) return "(none)";
2816
+ const names = items.map((it) => opts.getAliases(it)[0]).filter((n) => Boolean(n)).slice(0, 10);
2817
+ const suffix = items.length > names.length ? `, \u2026(${items.length - names.length} more)` : "";
2818
+ return names.join(", ") + suffix;
2819
+ }
2820
+
2607
2821
  // src/commands/mcp.ts
2822
+ async function resolveMcpContext(opts) {
2823
+ const stage = await resolveStage({ flag: opts.stage });
2824
+ const api = resolveApiConfig(stage);
2825
+ if (!api) process.exit(1);
2826
+ const tenant = await resolveTenantRest({
2827
+ flag: opts.tenant,
2828
+ stage,
2829
+ apiUrl: api.apiUrl,
2830
+ authSecret: api.authSecret
2831
+ });
2832
+ return { stage, api, tenant };
2833
+ }
2834
+ async function resolveServer(identifier, api, tenantSlug) {
2835
+ return resolveIdentifier({
2836
+ identifier,
2837
+ list: async () => {
2838
+ const res = await apiFetch(
2839
+ api.apiUrl,
2840
+ api.authSecret,
2841
+ "/api/skills/mcp-servers",
2842
+ {},
2843
+ { "x-tenant-slug": tenantSlug }
2844
+ );
2845
+ return res.servers ?? [];
2846
+ },
2847
+ getId: (s) => s.id,
2848
+ getAliases: (s) => [s.slug, s.name],
2849
+ resourceLabel: "MCP server",
2850
+ pickerLabel: (s) => `${s.name} ${chalk10.dim(`(${s.slug}, ${s.id})`)}`
2851
+ });
2852
+ }
2853
+ function formatAuth(s) {
2854
+ if (s.authType === "per_user_oauth") return `OAuth (${s.oauthProvider})`;
2855
+ if (s.authType === "tenant_api_key") return "API Key";
2856
+ return "none";
2857
+ }
2608
2858
  function registerMcpCommand(program2) {
2609
2859
  const mcp = program2.command("mcp").description("Manage MCP servers for your tenant");
2610
- mcp.command("list").description("List registered MCP servers").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2611
- const check = validateStage(opts.stage);
2612
- if (!check.valid) {
2613
- printError(check.error);
2614
- process.exit(1);
2615
- }
2616
- const api = resolveApiConfig(opts.stage);
2617
- if (!api) process.exit(1);
2618
- printHeader("mcp list", opts.stage);
2860
+ 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(
2861
+ "after",
2862
+ `
2863
+ Examples:
2864
+ # Interactive \u2014 picks stage + tenant from the deployed ones
2865
+ $ thinkwork mcp list
2866
+
2867
+ # Scriptable
2868
+ $ thinkwork mcp list -s dev -t acme
2869
+ $ thinkwork mcp list -s prod -t acme --json | jq '.[].id'
2870
+ `
2871
+ ).action(async (opts) => {
2619
2872
  try {
2620
- const { servers } = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {}, { "x-tenant-slug": opts.tenant });
2873
+ const { stage, api, tenant } = await resolveMcpContext(opts);
2874
+ printHeader("mcp list", stage);
2875
+ const { servers } = await apiFetch(
2876
+ api.apiUrl,
2877
+ api.authSecret,
2878
+ "/api/skills/mcp-servers",
2879
+ {},
2880
+ { "x-tenant-slug": tenant.slug }
2881
+ );
2621
2882
  if (!servers || servers.length === 0) {
2622
2883
  console.log(chalk10.dim(" No MCP servers registered."));
2623
2884
  return;
@@ -2625,87 +2886,194 @@ function registerMcpCommand(program2) {
2625
2886
  console.log("");
2626
2887
  for (const s of servers) {
2627
2888
  const status = s.enabled ? chalk10.green("enabled") : chalk10.dim("disabled");
2628
- const authLabel = s.authType === "per_user_oauth" ? `OAuth (${s.oauthProvider})` : s.authType === "tenant_api_key" ? "API Key" : "none";
2629
2889
  console.log(` ${chalk10.bold(s.name)} ${chalk10.dim(s.slug)} ${status}`);
2890
+ console.log(` ID: ${s.id}`);
2630
2891
  console.log(` URL: ${s.url}`);
2631
2892
  console.log(` Transport: ${s.transport}`);
2632
- console.log(` Auth: ${authLabel}`);
2633
- if (s.tools?.length) {
2634
- console.log(` Tools: ${s.tools.length} cached`);
2635
- }
2893
+ console.log(` Auth: ${formatAuth(s)}`);
2894
+ if (s.tools?.length) console.log(` Tools: ${s.tools.length} cached`);
2636
2895
  console.log("");
2637
2896
  }
2638
2897
  } catch (err) {
2639
- printError(err.message);
2898
+ if (isCancellation(err)) return;
2899
+ printError(err instanceof Error ? err.message : String(err));
2640
2900
  process.exit(1);
2641
2901
  }
2642
2902
  });
2643
- 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) => {
2644
- const check = validateStage(opts.stage);
2645
- if (!check.valid) {
2646
- printError(check.error);
2647
- process.exit(1);
2648
- }
2649
- const api = resolveApiConfig(opts.stage);
2650
- if (!api) process.exit(1);
2651
- const body = {
2652
- name,
2653
- url: opts.url,
2654
- transport: opts.transport
2655
- };
2656
- if (opts.authType !== "none") body.authType = opts.authType;
2657
- if (opts.apiKey) body.apiKey = opts.apiKey;
2658
- if (opts.oauthProvider) body.oauthProvider = opts.oauthProvider;
2659
- try {
2660
- const result = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {
2661
- method: "POST",
2662
- body: JSON.stringify(body)
2663
- }, { "x-tenant-slug": opts.tenant });
2664
- printSuccess(`MCP server "${name}" ${result.created ? "registered" : "updated"} (slug: ${result.slug})`);
2665
- } catch (err) {
2666
- printError(err.message);
2667
- process.exit(1);
2903
+ 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(
2904
+ "after",
2905
+ `
2906
+ Examples:
2907
+ # Fully interactive \u2014 prompts for name, URL, and auth
2908
+ $ thinkwork mcp add
2909
+
2910
+ # Scripted \u2014 API-key auth
2911
+ $ thinkwork mcp add my-tools --url https://mcp.example.com/crm \\
2912
+ --auth-type tenant_api_key --api-key sk-abc -s dev -t acme
2913
+
2914
+ # OAuth connector (users connect from the mobile app)
2915
+ $ thinkwork mcp add lastmile --url https://mcp-dev.lastmile-tei.com/crm \\
2916
+ --auth-type per_user_oauth --oauth-provider lastmile -s dev -t acme
2917
+ `
2918
+ ).action(
2919
+ async (nameArg, opts) => {
2920
+ try {
2921
+ const { input: input3 } = await import("@inquirer/prompts");
2922
+ const { stage, api, tenant } = await resolveMcpContext(opts);
2923
+ let name = nameArg;
2924
+ if (!name) {
2925
+ if (!process.stdin.isTTY) {
2926
+ printError("Name is required. Pass it as a positional arg.");
2927
+ process.exit(1);
2928
+ }
2929
+ name = await input3({ message: "Server name:" });
2930
+ }
2931
+ let url = opts.url;
2932
+ if (!url) {
2933
+ if (!process.stdin.isTTY) {
2934
+ printError("--url is required. Pass it as a flag.");
2935
+ process.exit(1);
2936
+ }
2937
+ url = await input3({
2938
+ message: "MCP server URL:",
2939
+ validate: (v) => v.startsWith("http://") || v.startsWith("https://") ? true : "URL must start with http:// or https://"
2940
+ });
2941
+ }
2942
+ const body = { name, url, transport: opts.transport };
2943
+ if (opts.authType !== "none") body.authType = opts.authType;
2944
+ if (opts.apiKey) body.apiKey = opts.apiKey;
2945
+ if (opts.oauthProvider) body.oauthProvider = opts.oauthProvider;
2946
+ printHeader("mcp add", stage);
2947
+ const result = await apiFetch(
2948
+ api.apiUrl,
2949
+ api.authSecret,
2950
+ "/api/skills/mcp-servers",
2951
+ { method: "POST", body: JSON.stringify(body) },
2952
+ { "x-tenant-slug": tenant.slug }
2953
+ );
2954
+ printSuccess(
2955
+ `MCP server "${name}" ${result.created ? "registered" : "updated"} (slug: ${result.slug})`
2956
+ );
2957
+ } catch (err) {
2958
+ if (isCancellation(err)) return;
2959
+ printError(err instanceof Error ? err.message : String(err));
2960
+ process.exit(1);
2961
+ }
2668
2962
  }
2669
- });
2670
- mcp.command("remove <id>").description("Remove an MCP server").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (id, opts) => {
2671
- const check = validateStage(opts.stage);
2672
- if (!check.valid) {
2673
- printError(check.error);
2674
- process.exit(1);
2963
+ );
2964
+ mcp.command("update [id]").description(
2965
+ "Update an MCP server's URL / transport / auth / enabled state. Accepts UUID, slug, or name; prompts in a TTY when the positional is omitted. Preserves agent assignments (unlike remove + re-add)."
2966
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--url <url>", "New URL").option("--transport <type>", "streamable-http | sse").option("--auth-type <type>", "none | tenant_api_key | per_user_oauth").option("--api-key <token>", "API key (for tenant_api_key auth)").option("--oauth-provider <name>", "OAuth provider name (for per_user_oauth auth)").option("--name <n>", "Rename").option("--enable", "Enable the server").option("--disable", "Disable the server (doesn't delete)").addHelpText(
2967
+ "after",
2968
+ `
2969
+ Examples:
2970
+ # Change URL in place (preserves agent assignments, unlike remove + re-add)
2971
+ $ thinkwork mcp update lastmile-routing --url https://dev-mcp.lastmile-tei.com/routing
2972
+
2973
+ # Disable without deleting
2974
+ $ thinkwork mcp update lastmile-routing --disable
2975
+
2976
+ # Rename + change transport
2977
+ $ thinkwork mcp update 629dcee1-1e14-4b83-9907-cb529e6035f6 --name "LastMile Routing" --transport sse
2978
+
2979
+ # Interactive \u2014 pick the server from a list
2980
+ $ thinkwork mcp update
2981
+ `
2982
+ ).action(
2983
+ async (idArg, opts) => {
2984
+ try {
2985
+ const { stage, api, tenant } = await resolveMcpContext(opts);
2986
+ const server = await resolveServer(idArg, api, tenant.slug);
2987
+ const body = {};
2988
+ if (opts.url !== void 0) body.url = opts.url;
2989
+ if (opts.transport !== void 0) body.transport = opts.transport;
2990
+ if (opts.authType !== void 0) body.authType = opts.authType;
2991
+ if (opts.apiKey !== void 0) body.apiKey = opts.apiKey;
2992
+ if (opts.oauthProvider !== void 0) body.oauthProvider = opts.oauthProvider;
2993
+ if (opts.name !== void 0) body.name = opts.name;
2994
+ if (opts.enable) body.enabled = true;
2995
+ if (opts.disable) body.enabled = false;
2996
+ if (Object.keys(body).length === 0) {
2997
+ printError(
2998
+ "Nothing to update. Pass at least one of: --url, --transport, --auth-type, --api-key, --oauth-provider, --name, --enable, --disable."
2999
+ );
3000
+ process.exit(1);
3001
+ }
3002
+ printHeader("mcp update", stage);
3003
+ await apiFetch(
3004
+ api.apiUrl,
3005
+ api.authSecret,
3006
+ `/api/skills/mcp-servers/${server.id}`,
3007
+ { method: "PUT", body: JSON.stringify(body) },
3008
+ { "x-tenant-slug": tenant.slug }
3009
+ );
3010
+ printSuccess(
3011
+ `Updated ${server.name} (${server.slug}) \u2014 changed ${Object.keys(body).join(", ")}.`
3012
+ );
3013
+ } catch (err) {
3014
+ if (isCancellation(err)) return;
3015
+ printError(err instanceof Error ? err.message : String(err));
3016
+ process.exit(1);
3017
+ }
2675
3018
  }
2676
- const api = resolveApiConfig(opts.stage);
2677
- if (!api) process.exit(1);
3019
+ );
3020
+ mcp.command("remove [id]").alias("rm").description(
3021
+ "Remove an MCP server. Accepts UUID, slug, or name; prompts from a list when omitted in a TTY."
3022
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
3023
+ "after",
3024
+ `
3025
+ Examples:
3026
+ # Interactive picker
3027
+ $ thinkwork mcp remove
3028
+
3029
+ # By slug (case-insensitive)
3030
+ $ thinkwork mcp remove lastmile-routing
3031
+
3032
+ # By UUID (from \`mcp list\` or --json)
3033
+ $ thinkwork mcp remove 629dcee1-1e14-4b83-9907-cb529e6035f6
3034
+ `
3035
+ ).action(async (idArg, opts) => {
2678
3036
  try {
2679
- await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}`, {
2680
- method: "DELETE"
2681
- }, { "x-tenant-slug": opts.tenant });
2682
- printSuccess(`MCP server removed.`);
3037
+ const { api, tenant } = await resolveMcpContext(opts);
3038
+ const server = await resolveServer(idArg, api, tenant.slug);
3039
+ await apiFetch(
3040
+ api.apiUrl,
3041
+ api.authSecret,
3042
+ `/api/skills/mcp-servers/${server.id}`,
3043
+ { method: "DELETE" },
3044
+ { "x-tenant-slug": tenant.slug }
3045
+ );
3046
+ printSuccess(`MCP server removed: ${server.name} (${server.slug}).`);
2683
3047
  } catch (err) {
2684
- printError(err.message);
3048
+ if (isCancellation(err)) return;
3049
+ printError(err instanceof Error ? err.message : String(err));
2685
3050
  process.exit(1);
2686
3051
  }
2687
3052
  });
2688
- 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) => {
2689
- const check = validateStage(opts.stage);
2690
- if (!check.valid) {
2691
- printError(check.error);
2692
- process.exit(1);
2693
- }
2694
- const api = resolveApiConfig(opts.stage);
2695
- if (!api) process.exit(1);
2696
- printHeader("mcp test", opts.stage);
3053
+ mcp.command("test [id]").description(
3054
+ "Test connection and discover tools. Accepts UUID, slug, or name; prompts from a list when omitted in a TTY."
3055
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (idArg, opts) => {
2697
3056
  try {
2698
- const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}/test`, {
2699
- method: "POST"
2700
- }, { "x-tenant-slug": opts.tenant });
3057
+ const { stage, api, tenant } = await resolveMcpContext(opts);
3058
+ const server = await resolveServer(idArg, api, tenant.slug);
3059
+ printHeader("mcp test", stage);
3060
+ const result = await apiFetch(
3061
+ api.apiUrl,
3062
+ api.authSecret,
3063
+ `/api/skills/mcp-servers/${server.id}/test`,
3064
+ { method: "POST" },
3065
+ { "x-tenant-slug": tenant.slug }
3066
+ );
2701
3067
  if (result.ok) {
2702
- printSuccess("Connection successful.");
3068
+ printSuccess(`Connection successful: ${server.name}.`);
2703
3069
  if (result.tools?.length) {
2704
3070
  console.log(chalk10.bold(`
2705
3071
  Discovered tools (${result.tools.length}):
2706
3072
  `));
2707
3073
  for (const t of result.tools) {
2708
- console.log(` ${chalk10.cyan(t.name)}${t.description ? chalk10.dim(` - ${t.description}`) : ""}`);
3074
+ console.log(
3075
+ ` ${chalk10.cyan(t.name)}${t.description ? chalk10.dim(` - ${t.description}`) : ""}`
3076
+ );
2709
3077
  }
2710
3078
  console.log("");
2711
3079
  } else {
@@ -2716,86 +3084,120 @@ function registerMcpCommand(program2) {
2716
3084
  process.exit(1);
2717
3085
  }
2718
3086
  } catch (err) {
2719
- printError(err.message);
2720
- process.exit(1);
2721
- }
2722
- });
2723
- 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) => {
2724
- const check = validateStage(opts.stage);
2725
- if (!check.valid) {
2726
- printError(check.error);
2727
- process.exit(1);
2728
- }
2729
- const api = resolveApiConfig(opts.stage);
2730
- if (!api) process.exit(1);
2731
- try {
2732
- const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agents/${opts.agent}/mcp-servers`, {
2733
- method: "POST",
2734
- body: JSON.stringify({ mcpServerId })
2735
- });
2736
- printSuccess(`MCP server assigned to agent. (${result.created ? "new" : "updated"})`);
2737
- } catch (err) {
2738
- printError(err.message);
3087
+ if (isCancellation(err)) return;
3088
+ printError(err instanceof Error ? err.message : String(err));
2739
3089
  process.exit(1);
2740
3090
  }
2741
3091
  });
2742
- 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) => {
2743
- const check = validateStage(opts.stage);
2744
- if (!check.valid) {
2745
- printError(check.error);
2746
- process.exit(1);
3092
+ mcp.command("assign [mcpServer]").description(
3093
+ "Assign an MCP server to an agent. Accepts UUID, slug, or name for the server; prompts from a list when omitted in a TTY."
3094
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
3095
+ async (mcpServerArg, opts) => {
3096
+ try {
3097
+ const { input: input3 } = await import("@inquirer/prompts");
3098
+ const { api, tenant } = await resolveMcpContext(opts);
3099
+ const server = await resolveServer(mcpServerArg, api, tenant.slug);
3100
+ let agent = opts.agent;
3101
+ if (!agent) {
3102
+ if (!process.stdin.isTTY) {
3103
+ printError("--agent is required. Pass it as a flag.");
3104
+ process.exit(1);
3105
+ }
3106
+ agent = await input3({ message: "Agent ID:" });
3107
+ }
3108
+ const result = await apiFetch(
3109
+ api.apiUrl,
3110
+ api.authSecret,
3111
+ `/api/skills/agents/${agent}/mcp-servers`,
3112
+ { method: "POST", body: JSON.stringify({ mcpServerId: server.id }) }
3113
+ );
3114
+ printSuccess(
3115
+ `MCP server assigned to agent. (${result.created ? "new" : "updated"}) \u2014 ${server.name} \u2192 ${agent}`
3116
+ );
3117
+ } catch (err) {
3118
+ if (isCancellation(err)) return;
3119
+ printError(err instanceof Error ? err.message : String(err));
3120
+ process.exit(1);
3121
+ }
2747
3122
  }
2748
- const api = resolveApiConfig(opts.stage);
2749
- if (!api) process.exit(1);
2750
- try {
2751
- await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agents/${opts.agent}/mcp-servers/${mcpServerId}`, {
2752
- method: "DELETE"
2753
- });
2754
- printSuccess("MCP server unassigned from agent.");
2755
- } catch (err) {
2756
- printError(err.message);
2757
- process.exit(1);
3123
+ );
3124
+ mcp.command("unassign [mcpServer]").description(
3125
+ "Remove an MCP server from an agent. Accepts UUID, slug, or name for the server; prompts from a list when omitted in a TTY."
3126
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
3127
+ async (mcpServerArg, opts) => {
3128
+ try {
3129
+ const { input: input3 } = await import("@inquirer/prompts");
3130
+ const { api, tenant } = await resolveMcpContext(opts);
3131
+ const server = await resolveServer(mcpServerArg, api, tenant.slug);
3132
+ let agent = opts.agent;
3133
+ if (!agent) {
3134
+ if (!process.stdin.isTTY) {
3135
+ printError("--agent is required. Pass it as a flag.");
3136
+ process.exit(1);
3137
+ }
3138
+ agent = await input3({ message: "Agent ID:" });
3139
+ }
3140
+ await apiFetch(
3141
+ api.apiUrl,
3142
+ api.authSecret,
3143
+ `/api/skills/agents/${agent}/mcp-servers/${server.id}`,
3144
+ { method: "DELETE" }
3145
+ );
3146
+ printSuccess(`MCP server unassigned from agent: ${server.name} \u219B ${agent}`);
3147
+ } catch (err) {
3148
+ if (isCancellation(err)) return;
3149
+ printError(err instanceof Error ? err.message : String(err));
3150
+ process.exit(1);
3151
+ }
2758
3152
  }
2759
- });
3153
+ );
2760
3154
  }
2761
3155
 
2762
3156
  // src/commands/tools.ts
2763
- import { createInterface as createInterface4 } from "readline";
3157
+ import { select as select6, password } from "@inquirer/prompts";
2764
3158
  import chalk11 from "chalk";
2765
- function prompt(question) {
2766
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
2767
- return new Promise((resolve3) => {
2768
- rl.question(question, (answer) => {
2769
- rl.close();
2770
- resolve3(answer.trim());
2771
- });
2772
- });
2773
- }
2774
3159
  var TOOL_PROVIDERS = {
2775
3160
  "web-search": ["exa", "serpapi"]
2776
3161
  };
3162
+ async function resolveCtx(opts) {
3163
+ const stage = await resolveStage({ flag: opts.stage });
3164
+ const api = resolveApiConfig(stage);
3165
+ if (!api) process.exit(1);
3166
+ const tenant = await resolveTenantRest({
3167
+ flag: opts.tenant,
3168
+ stage,
3169
+ apiUrl: api.apiUrl,
3170
+ authSecret: api.authSecret
3171
+ });
3172
+ return { stage, api, tenant };
3173
+ }
2777
3174
  function registerToolsCommand(program2) {
2778
3175
  const tools = program2.command("tools").description("Configure built-in agent tools (web_search, \u2026) for your tenant");
2779
- tools.command("list").description("List configured built-in tools").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2780
- const check = validateStage(opts.stage);
2781
- if (!check.valid) {
2782
- printError(check.error);
2783
- process.exit(1);
2784
- }
2785
- const api = resolveApiConfig(opts.stage);
2786
- if (!api) process.exit(1);
2787
- printHeader("tools list", opts.stage);
3176
+ 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(
3177
+ "after",
3178
+ `
3179
+ Examples:
3180
+ # Interactive \u2014 picks stage + tenant
3181
+ $ thinkwork tools list
3182
+
3183
+ # Scripted
3184
+ $ thinkwork tools list -s dev -t acme
3185
+ $ thinkwork tools list --json | jq '.[] | .toolSlug'
3186
+ `
3187
+ ).action(async (opts) => {
2788
3188
  try {
3189
+ const { stage, api, tenant } = await resolveCtx(opts);
3190
+ printHeader("tools list", stage);
2789
3191
  const { tools: rows } = await apiFetch(
2790
3192
  api.apiUrl,
2791
3193
  api.authSecret,
2792
3194
  "/api/skills/builtin-tools",
2793
3195
  {},
2794
- { "x-tenant-slug": opts.tenant }
3196
+ { "x-tenant-slug": tenant.slug }
2795
3197
  );
2796
3198
  if (!rows || rows.length === 0) {
2797
3199
  console.log(chalk11.dim(" No built-in tools configured."));
2798
- console.log(chalk11.dim(" Try: thinkwork tools web-search set --tenant <slug> -s <stage>"));
3200
+ console.log(chalk11.dim(" Try: thinkwork tools web-search set"));
2799
3201
  return;
2800
3202
  }
2801
3203
  console.log("");
@@ -2806,75 +3208,84 @@ function registerToolsCommand(program2) {
2806
3208
  console.log(` ${chalk11.bold(r.toolSlug)} ${status}`);
2807
3209
  console.log(` Provider: ${provider}`);
2808
3210
  console.log(` Has key: ${key}`);
2809
- if (r.lastTestedAt) {
2810
- console.log(` Tested: ${new Date(r.lastTestedAt).toLocaleString()}`);
2811
- }
3211
+ if (r.lastTestedAt) console.log(` Tested: ${new Date(r.lastTestedAt).toLocaleString()}`);
2812
3212
  console.log("");
2813
3213
  }
2814
3214
  } catch (err) {
2815
- printError(err.message);
3215
+ if (isCancellation(err)) return;
3216
+ printError(err instanceof Error ? err.message : String(err));
2816
3217
  process.exit(1);
2817
3218
  }
2818
3219
  });
2819
3220
  const webSearch = tools.command("web-search").description("Configure the web_search built-in tool");
2820
- 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) => {
2821
- const check = validateStage(opts.stage);
2822
- if (!check.valid) {
2823
- printError(check.error);
2824
- process.exit(1);
2825
- }
2826
- const api = resolveApiConfig(opts.stage);
2827
- if (!api) process.exit(1);
2828
- let provider = opts.provider;
2829
- if (!provider) {
2830
- provider = (await prompt(`Provider [${TOOL_PROVIDERS["web-search"].join("/")}]: `)).toLowerCase();
2831
- }
2832
- if (!TOOL_PROVIDERS["web-search"].includes(provider)) {
2833
- printError(`provider must be one of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
2834
- process.exit(1);
2835
- }
2836
- let apiKey = opts.key;
2837
- if (!apiKey) {
2838
- apiKey = await prompt(`${provider} API key: `);
2839
- }
2840
- if (!apiKey) {
2841
- printError("API key is required");
2842
- process.exit(1);
2843
- }
2844
- try {
2845
- await apiFetch(
2846
- api.apiUrl,
2847
- api.authSecret,
2848
- "/api/skills/builtin-tools/web-search",
2849
- {
2850
- method: "PUT",
2851
- body: JSON.stringify({ provider, apiKey, enabled: true })
2852
- },
2853
- { "x-tenant-slug": opts.tenant }
2854
- );
2855
- printSuccess(`web_search configured with provider=${provider}, enabled=true`);
2856
- printWarning("Run `thinkwork tools web-search test` to verify connectivity.");
2857
- } catch (err) {
2858
- printError(err.message);
2859
- process.exit(1);
2860
- }
2861
- });
2862
- 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) => {
2863
- const check = validateStage(opts.stage);
2864
- if (!check.valid) {
2865
- printError(check.error);
2866
- process.exit(1);
3221
+ 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(
3222
+ "after",
3223
+ `
3224
+ Examples:
3225
+ # Fully interactive \u2014 stage/tenant picker, provider picker, hidden key prompt
3226
+ $ thinkwork tools web-search set
3227
+
3228
+ # Scripted (secret from env)
3229
+ $ thinkwork tools web-search set -s dev -t acme --provider exa --key "$EXA_KEY"
3230
+ `
3231
+ ).action(
3232
+ async (opts) => {
3233
+ try {
3234
+ const { api, tenant } = await resolveCtx(opts);
3235
+ let provider = opts.provider;
3236
+ if (!provider) {
3237
+ if (!process.stdin.isTTY) {
3238
+ printError(`--provider is required. One of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
3239
+ process.exit(1);
3240
+ }
3241
+ provider = await select6({
3242
+ message: "Provider:",
3243
+ choices: TOOL_PROVIDERS["web-search"].map((p) => ({ name: p, value: p })),
3244
+ loop: false
3245
+ });
3246
+ }
3247
+ if (!TOOL_PROVIDERS["web-search"].includes(provider)) {
3248
+ printError(`provider must be one of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
3249
+ process.exit(1);
3250
+ }
3251
+ let apiKey = opts.key;
3252
+ if (!apiKey) {
3253
+ if (!process.stdin.isTTY) {
3254
+ printError("--key is required. Pass it as a flag or pipe via env.");
3255
+ process.exit(1);
3256
+ }
3257
+ apiKey = await password({ message: `${provider} API key:`, mask: "*" });
3258
+ }
3259
+ if (!apiKey) {
3260
+ printError("API key is required");
3261
+ process.exit(1);
3262
+ }
3263
+ await apiFetch(
3264
+ api.apiUrl,
3265
+ api.authSecret,
3266
+ "/api/skills/builtin-tools/web-search",
3267
+ { method: "PUT", body: JSON.stringify({ provider, apiKey, enabled: true }) },
3268
+ { "x-tenant-slug": tenant.slug }
3269
+ );
3270
+ printSuccess(`web_search configured with provider=${provider}, enabled=true`);
3271
+ printWarning("Run `thinkwork tools web-search test` to verify connectivity.");
3272
+ } catch (err) {
3273
+ if (isCancellation(err)) return;
3274
+ printError(err instanceof Error ? err.message : String(err));
3275
+ process.exit(1);
3276
+ }
2867
3277
  }
2868
- const api = resolveApiConfig(opts.stage);
2869
- if (!api) process.exit(1);
2870
- printHeader("tools web-search test", opts.stage);
3278
+ );
3279
+ 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) => {
2871
3280
  try {
3281
+ const { stage, api, tenant } = await resolveCtx(opts);
3282
+ printHeader("tools web-search test", stage);
2872
3283
  const result = await apiFetch(
2873
3284
  api.apiUrl,
2874
3285
  api.authSecret,
2875
3286
  "/api/skills/builtin-tools/web-search/test",
2876
3287
  { method: "POST", body: "{}" },
2877
- { "x-tenant-slug": opts.tenant }
3288
+ { "x-tenant-slug": tenant.slug }
2878
3289
  );
2879
3290
  if (result.ok) {
2880
3291
  printSuccess(`${result.provider}: ${result.resultCount} result(s) returned.`);
@@ -2883,51 +3294,42 @@ function registerToolsCommand(program2) {
2883
3294
  process.exit(1);
2884
3295
  }
2885
3296
  } catch (err) {
2886
- printError(err.message);
3297
+ if (isCancellation(err)) return;
3298
+ printError(err instanceof Error ? err.message : String(err));
2887
3299
  process.exit(1);
2888
3300
  }
2889
3301
  });
2890
- 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) => {
2891
- const check = validateStage(opts.stage);
2892
- if (!check.valid) {
2893
- printError(check.error);
2894
- process.exit(1);
2895
- }
2896
- const api = resolveApiConfig(opts.stage);
2897
- if (!api) process.exit(1);
3302
+ 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) => {
2898
3303
  try {
3304
+ const { api, tenant } = await resolveCtx(opts);
2899
3305
  await apiFetch(
2900
3306
  api.apiUrl,
2901
3307
  api.authSecret,
2902
3308
  "/api/skills/builtin-tools/web-search",
2903
3309
  { method: "PUT", body: JSON.stringify({ enabled: false }) },
2904
- { "x-tenant-slug": opts.tenant }
3310
+ { "x-tenant-slug": tenant.slug }
2905
3311
  );
2906
3312
  printSuccess("web_search disabled.");
2907
3313
  } catch (err) {
2908
- printError(err.message);
3314
+ if (isCancellation(err)) return;
3315
+ printError(err instanceof Error ? err.message : String(err));
2909
3316
  process.exit(1);
2910
3317
  }
2911
3318
  });
2912
- 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) => {
2913
- const check = validateStage(opts.stage);
2914
- if (!check.valid) {
2915
- printError(check.error);
2916
- process.exit(1);
2917
- }
2918
- const api = resolveApiConfig(opts.stage);
2919
- if (!api) process.exit(1);
3319
+ 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) => {
2920
3320
  try {
3321
+ const { api, tenant } = await resolveCtx(opts);
2921
3322
  await apiFetch(
2922
3323
  api.apiUrl,
2923
3324
  api.authSecret,
2924
3325
  "/api/skills/builtin-tools/web-search",
2925
3326
  { method: "DELETE" },
2926
- { "x-tenant-slug": opts.tenant }
3327
+ { "x-tenant-slug": tenant.slug }
2927
3328
  );
2928
3329
  printSuccess("web_search configuration cleared.");
2929
3330
  } catch (err) {
2930
- printError(err.message);
3331
+ if (isCancellation(err)) return;
3332
+ printError(err instanceof Error ? err.message : String(err));
2931
3333
  process.exit(1);
2932
3334
  }
2933
3335
  });
@@ -3024,7 +3426,7 @@ function registerUpdateCommand(program2) {
3024
3426
 
3025
3427
  // src/commands/user.ts
3026
3428
  import { spawn as spawn4 } from "child_process";
3027
- import { input, select as select3 } from "@inquirer/prompts";
3429
+ import { input as input2, select as select7 } from "@inquirer/prompts";
3028
3430
  function getTerraformOutput2(cwd, key) {
3029
3431
  return new Promise((resolve3, reject) => {
3030
3432
  const proc = spawn4("terraform", ["output", "-raw", key], {
@@ -3067,9 +3469,6 @@ function runAwsCognitoReset(userPoolId, username, region) {
3067
3469
  proc.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
3068
3470
  });
3069
3471
  }
3070
- function isCancellation2(err) {
3071
- return err instanceof Error && err.name === "ExitPromptError";
3072
- }
3073
3472
  function requireTty2(label) {
3074
3473
  if (!process.stdin.isTTY) {
3075
3474
  printError(
@@ -3080,7 +3479,7 @@ function requireTty2(label) {
3080
3479
  }
3081
3480
  async function promptEmail() {
3082
3481
  requireTty2("Email");
3083
- return await input({
3482
+ return await input2({
3084
3483
  message: "Email address of the person to invite:",
3085
3484
  validate: (v) => v.trim().includes("@") ? true : "That doesn't look like an email."
3086
3485
  });
@@ -3098,7 +3497,7 @@ async function promptStage(region) {
3098
3497
  console.log(` Using the only deployed stage: ${stages[0]}`);
3099
3498
  return stages[0];
3100
3499
  }
3101
- return await select3({
3500
+ return await select7({
3102
3501
  message: "Which stage?",
3103
3502
  choices: stages.map((s) => ({ name: s, value: s })),
3104
3503
  loop: false
@@ -3117,7 +3516,7 @@ async function promptTenant(apiUrl, authSecret) {
3117
3516
  console.log(` Using the only tenant: ${list[0].name} (${list[0].slug})`);
3118
3517
  return list[0].slug;
3119
3518
  }
3120
- return await select3({
3519
+ return await select7({
3121
3520
  message: "Which tenant?",
3122
3521
  choices: list.map((t) => ({
3123
3522
  name: `${t.name} (slug: ${t.slug})`,
@@ -3128,7 +3527,7 @@ async function promptTenant(apiUrl, authSecret) {
3128
3527
  }
3129
3528
  async function promptOptionalName() {
3130
3529
  if (!process.stdin.isTTY) return void 0;
3131
- const answer = await input({
3530
+ const answer = await input2({
3132
3531
  message: "Display name (optional, press Enter to skip):",
3133
3532
  default: ""
3134
3533
  });
@@ -3136,7 +3535,7 @@ async function promptOptionalName() {
3136
3535
  }
3137
3536
  async function promptRole() {
3138
3537
  if (!process.stdin.isTTY) return "member";
3139
- return await select3({
3538
+ return await select7({
3140
3539
  message: "Role:",
3141
3540
  choices: [
3142
3541
  { name: "member \u2014 regular access", value: "member" },
@@ -3245,7 +3644,7 @@ Agents / scripts that pass all flags stay non-interactive.
3245
3644
  `Invited ${email} to "${tenant}" (role: ${result.body.role}). Cognito has emailed a temporary password; the user sets a new password on first sign-in.`
3246
3645
  );
3247
3646
  } catch (err) {
3248
- if (isCancellation2(err)) {
3647
+ if (isCancellation(err)) {
3249
3648
  console.log("");
3250
3649
  console.log(" Cancelled.");
3251
3650
  return;
@@ -3258,15 +3657,18 @@ Agents / scripts that pass all flags stay non-interactive.
3258
3657
  }
3259
3658
  );
3260
3659
  user.command("reset-password <email>").description(
3261
- "Trigger Cognito's forgot-password flow for a user (admin-initiated). Sends them a verification code email."
3262
- ).option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage (e.g. dev, prod)").option(
3660
+ "Trigger Cognito's forgot-password flow for a user (admin-initiated). Prompts for stage in a TTY when omitted."
3661
+ ).option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage (e.g. dev, prod)").option(
3263
3662
  "-r, --region <name>",
3264
3663
  "AWS region (defaults to AWS CLI default / AWS_REGION)"
3265
3664
  ).addHelpText(
3266
3665
  "after",
3267
3666
  `
3268
3667
  Examples:
3269
- # Admin-triggered password reset \u2014 works even if the account is locked
3668
+ # Fully interactive \u2014 picks stage from the deployed ones
3669
+ $ thinkwork user reset-password alice@example.com
3670
+
3671
+ # Scripted
3270
3672
  $ thinkwork user reset-password alice@example.com -s dev
3271
3673
 
3272
3674
  # Target a specific AWS profile + region
@@ -3279,10 +3681,12 @@ FORCE_CHANGE_PASSWORD or has been disabled.
3279
3681
  `
3280
3682
  ).action(
3281
3683
  async (email, opts) => {
3282
- const stageCheck = validateStage(opts.stage);
3283
- if (!stageCheck.valid) {
3284
- printError(stageCheck.error);
3285
- process.exit(1);
3684
+ let stage;
3685
+ try {
3686
+ stage = await resolveStage({ flag: opts.stage, region: opts.region });
3687
+ } catch (err) {
3688
+ if (isCancellation(err)) return;
3689
+ throw err;
3286
3690
  }
3287
3691
  if (!email || !email.includes("@")) {
3288
3692
  printError(
@@ -3290,11 +3694,11 @@ FORCE_CHANGE_PASSWORD or has been disabled.
3290
3694
  );
3291
3695
  process.exit(1);
3292
3696
  }
3293
- printHeader("user reset-password", opts.stage);
3697
+ printHeader("user reset-password", stage);
3294
3698
  const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
3295
- const cwd = resolveTierDir(terraformDir, opts.stage, "app");
3699
+ const cwd = resolveTierDir(terraformDir, stage, "app");
3296
3700
  await ensureInit(cwd);
3297
- await ensureWorkspace(cwd, opts.stage);
3701
+ await ensureWorkspace(cwd, stage);
3298
3702
  let userPoolId;
3299
3703
  try {
3300
3704
  userPoolId = await getTerraformOutput2(cwd, "user_pool_id");
@@ -3341,47 +3745,6 @@ FORCE_CHANGE_PASSWORD or has been disabled.
3341
3745
  // src/commands/me.ts
3342
3746
  import { gql } from "@urql/core";
3343
3747
 
3344
- // src/lib/resolve-stage.ts
3345
- import { select as select4 } from "@inquirer/prompts";
3346
- async function resolveStage(opts = {}) {
3347
- const region = opts.region ?? "us-east-1";
3348
- const validate = opts.validate ?? true;
3349
- const raw = opts.flag ?? process.env.THINKWORK_STAGE ?? loadCliConfig().defaultStage ?? await pickStage(region);
3350
- if (!raw) {
3351
- printError(
3352
- "No stage specified. Pass `--stage <name>`, set THINKWORK_STAGE, or run `thinkwork login --stage <name>`."
3353
- );
3354
- process.exit(1);
3355
- }
3356
- if (validate) {
3357
- const check = validateStage(raw);
3358
- if (!check.valid) {
3359
- printError(check.error);
3360
- process.exit(1);
3361
- }
3362
- }
3363
- return raw;
3364
- }
3365
- async function pickStage(region) {
3366
- const stages = listDeployedStages(region);
3367
- if (stages.length === 0) {
3368
- printError(
3369
- `No Thinkwork deployments found in ${region}. Run \`thinkwork list\` or pass --region.`
3370
- );
3371
- process.exit(1);
3372
- }
3373
- if (stages.length === 1) {
3374
- console.log(` Using the only deployed stage: ${stages[0]}`);
3375
- return stages[0];
3376
- }
3377
- requireTty("Stage");
3378
- return await select4({
3379
- message: "Which stage?",
3380
- choices: stages.map((s) => ({ name: s, value: s })),
3381
- loop: false
3382
- });
3383
- }
3384
-
3385
3748
  // src/lib/gql-client.ts
3386
3749
  import {
3387
3750
  Client,
@@ -3675,8 +4038,8 @@ Examples:
3675
4038
  $ thinkwork thread label remove thr-abc lbl-ops
3676
4039
  `
3677
4040
  ).action(() => notYetImplemented("thread label", 1));
3678
- thread.command("escalate <id>").description("Escalate a thread to another agent (carries context, records actor).").requiredOption("--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));
3679
- thread.command("delegate <id>").description("Delegate ownership to another agent without the 'escalation' semantics.").requiredOption("--to-agent <id>", "Agent to delegate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread delegate", 1));
4041
+ 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));
4042
+ 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));
3680
4043
  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(
3681
4044
  "after",
3682
4045
  `
@@ -3759,7 +4122,7 @@ Examples:
3759
4122
  `
3760
4123
  ).action(() => notYetImplemented("inbox approve", 1));
3761
4124
  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));
3762
- inbox.command("request-revision <id>").description("Ask for changes \u2014 the agent gets the item back with your notes.").requiredOption("--notes <text>", "What needs to change").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox request-revision", 1));
4125
+ 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));
3763
4126
  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));
3764
4127
  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));
3765
4128
  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));
@@ -3811,15 +4174,15 @@ Examples:
3811
4174
  ).action(() => notYetImplemented("agent status", 2));
3812
4175
  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));
3813
4176
  const capabilities = agent.command("capabilities").alias("cap").description("Toggle built-in capabilities (email inbox, web search, etc.).");
3814
- capabilities.command("set <agentId>").description("Enable/disable capabilities on an agent.").requiredOption("--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));
4177
+ 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));
3815
4178
  const skills = agent.command("skills").description("Attach or configure MCP-backed skills on an agent.");
3816
- skills.command("set <agentId>").description("Enable/disable/configure a skill for an agent.").requiredOption("--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));
4179
+ 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));
3817
4180
  const budget = agent.command("budget").description("Per-agent spend caps \u2014 pause or alert when exceeded.");
3818
- budget.command("set <agentId>").description("Set or update an agent's budget policy.").requiredOption("--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));
4181
+ 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));
3819
4182
  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));
3820
4183
  const apiKey = agent.command("api-key").description("Agent API keys \u2014 service-to-service credentials tied to one agent.");
3821
4184
  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));
3822
- apiKey.command("create <agentId>").description("Generate a new API key. The plaintext is printed once \u2014 save it.").requiredOption("--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(
4185
+ 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(
3823
4186
  "after",
3824
4187
  `
3825
4188
  Examples:
@@ -3956,7 +4319,7 @@ Examples:
3956
4319
  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));
3957
4320
  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));
3958
4321
  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));
3959
- kb.command("attach <kbId>").description("Attach a knowledge base to an agent.").requiredOption("--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(
4322
+ 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(
3960
4323
  "after",
3961
4324
  `
3962
4325
  Examples:
@@ -3964,7 +4327,7 @@ Examples:
3964
4327
  $ thinkwork kb attach kb-runbooks --agent agt-oncall --config '{"topK":5}'
3965
4328
  `
3966
4329
  ).action(() => notYetImplemented("kb attach", 2));
3967
- kb.command("detach <kbId>").description("Detach a knowledge base from an agent.").requiredOption("--agent <id>", "Agent ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("kb detach", 2));
4330
+ 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));
3968
4331
  }
3969
4332
 
3970
4333
  // src/commands/routine.ts
@@ -3987,7 +4350,7 @@ Examples:
3987
4350
  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));
3988
4351
  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));
3989
4352
  const trigger = routine.command("trigger-config").description("Manage a routine's triggers (cron, webhook, event).");
3990
- trigger.command("set <routineId>").description("Set or replace a trigger for a routine.").requiredOption("--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(
4353
+ 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(
3991
4354
  "after",
3992
4355
  `
3993
4356
  Examples:
@@ -4040,7 +4403,7 @@ Examples:
4040
4403
  function registerWakeupCommand(program2) {
4041
4404
  const wake = program2.command("wakeup").alias("wakeups").description("View and create agent wakeup requests (deferred/enqueued invocations).");
4042
4405
  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));
4043
- wake.command("create").description("Queue a wakeup for an agent (ad-hoc or deferred).").requiredOption("--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(
4406
+ 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(
4044
4407
  "after",
4045
4408
  `
4046
4409
  Examples:
@@ -4122,12 +4485,12 @@ Examples:
4122
4485
  // src/commands/memory.ts
4123
4486
  function registerMemoryCommand(program2) {
4124
4487
  const memory = program2.command("memory").description("Inspect, search, and edit an agent's memory records and graph.");
4125
- memory.command("list").alias("ls").description("List memory records for an agent in a namespace.").requiredOption("--agent <id>", "Agent (assistant) ID").option(
4488
+ memory.command("list").alias("ls").description("List memory records for an agent in a namespace.").option("--agent <id>", "Agent (assistant) ID").option(
4126
4489
  "--namespace <ns>",
4127
4490
  "Memory namespace (semantic | preferences | episodes | reflections)",
4128
4491
  "semantic"
4129
4492
  ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory list", 4));
4130
- memory.command("search").description("Search an agent's memory by query string.").requiredOption("--agent <id>", "Agent (assistant) ID").requiredOption("--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(
4493
+ 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(
4131
4494
  "after",
4132
4495
  `
4133
4496
  Examples:
@@ -4136,9 +4499,9 @@ Examples:
4136
4499
  `
4137
4500
  ).action(() => notYetImplemented("memory search", 4));
4138
4501
  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));
4139
- memory.command("update <recordId>").description("Replace a memory record's content.").requiredOption("--content <text>", "New content").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory update", 4));
4502
+ 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));
4140
4503
  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));
4141
- memory.command("graph").description("Print the agent's memory graph (summary in human mode; full JSON with --json).").requiredOption("--agent <id>", "Agent (assistant) ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory graph", 4));
4504
+ 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));
4142
4505
  }
4143
4506
 
4144
4507
  // src/commands/recipe.ts
@@ -4199,7 +4562,7 @@ function registerBudgetCommand(program2) {
4199
4562
  const budget = program2.command("budget").alias("budgets").description("Manage budget policies (tenant-wide or per-agent) and inspect current status.");
4200
4563
  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));
4201
4564
  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));
4202
- budget.command("upsert").description("Create or update a budget policy.").requiredOption("--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(
4565
+ 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(
4203
4566
  "after",
4204
4567
  `
4205
4568
  Examples: