thinkwork-cli 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +727 -548
- 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/
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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").
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
if (!
|
|
337
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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").
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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").
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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",
|
|
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").
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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)").
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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(
|
|
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").
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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",
|
|
742
|
-
const tfvarsPath = resolveTfvarsPath(
|
|
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(
|
|
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,
|
|
860
|
+
await ensureWorkspace(tfDir, stage);
|
|
756
861
|
const code = await runTerraform(tfDir, [
|
|
757
862
|
"apply",
|
|
758
863
|
"-auto-approve",
|
|
759
|
-
`-var=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").
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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",
|
|
913
|
+
printHeader("bootstrap", stage, identity);
|
|
807
914
|
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
808
|
-
const cwd = resolveTierDir(terraformDir,
|
|
915
|
+
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
809
916
|
await ensureInit(cwd);
|
|
810
|
-
await ensureWorkspace(cwd,
|
|
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, [
|
|
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
|
|
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
|
|
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
|
|
1046
|
-
function
|
|
1152
|
+
import { execSync as execSync5 } from "child_process";
|
|
1153
|
+
function runAws2(cmd) {
|
|
1047
1154
|
try {
|
|
1048
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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,70 +1471,11 @@ 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) {
|
|
1370
|
-
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
1474
|
// src/commands/login.ts
|
|
1427
|
-
function ask(
|
|
1475
|
+
function ask(prompt) {
|
|
1428
1476
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1429
1477
|
return new Promise((resolve3) => {
|
|
1430
|
-
rl.question(
|
|
1478
|
+
rl.question(prompt, (answer) => {
|
|
1431
1479
|
rl.close();
|
|
1432
1480
|
resolve3(answer.trim());
|
|
1433
1481
|
});
|
|
@@ -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
|
|
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
|
|
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
|
|
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(
|
|
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(` ${
|
|
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(
|
|
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(`${
|
|
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").
|
|
1998
|
-
|
|
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",
|
|
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
|
|
2094
|
+
stage,
|
|
2027
2095
|
account_id: identity.account,
|
|
2028
2096
|
db_password: generateSecret(24),
|
|
2029
|
-
api_auth_secret: `tw-${
|
|
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 " +
|
|
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 "${
|
|
2372
|
+
printSuccess(`Environment "${stage}" initialized`);
|
|
2305
2373
|
console.log("");
|
|
2306
2374
|
console.log(" Next steps:");
|
|
2307
|
-
console.log(` ${chalk8.cyan("1.")} thinkwork plan -s ${
|
|
2308
|
-
console.log(` ${chalk8.cyan("2.")} thinkwork deploy -s ${
|
|
2309
|
-
console.log(` ${chalk8.cyan("3.")} thinkwork bootstrap -s ${
|
|
2310
|
-
console.log(` ${chalk8.cyan("4.")} thinkwork outputs -s ${
|
|
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,112 @@ 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
|
+
|
|
2607
2744
|
// src/commands/mcp.ts
|
|
2745
|
+
async function resolveMcpContext(opts) {
|
|
2746
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
2747
|
+
const api = resolveApiConfig(stage);
|
|
2748
|
+
if (!api) process.exit(1);
|
|
2749
|
+
const tenant = await resolveTenantRest({
|
|
2750
|
+
flag: opts.tenant,
|
|
2751
|
+
stage,
|
|
2752
|
+
apiUrl: api.apiUrl,
|
|
2753
|
+
authSecret: api.authSecret
|
|
2754
|
+
});
|
|
2755
|
+
return { stage, api, tenant };
|
|
2756
|
+
}
|
|
2608
2757
|
function registerMcpCommand(program2) {
|
|
2609
2758
|
const mcp = program2.command("mcp").description("Manage MCP servers for your tenant");
|
|
2610
|
-
mcp.command("list").description("List registered MCP servers").
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2759
|
+
mcp.command("list").alias("ls").description("List registered MCP servers. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
2760
|
+
"after",
|
|
2761
|
+
`
|
|
2762
|
+
Examples:
|
|
2763
|
+
# Interactive \u2014 picks stage + tenant from the deployed ones
|
|
2764
|
+
$ thinkwork mcp list
|
|
2765
|
+
|
|
2766
|
+
# Scriptable
|
|
2767
|
+
$ thinkwork mcp list -s dev -t acme
|
|
2768
|
+
$ thinkwork mcp list -s prod -t acme --json | jq '.[].slug'
|
|
2769
|
+
`
|
|
2770
|
+
).action(async (opts) => {
|
|
2619
2771
|
try {
|
|
2620
|
-
const {
|
|
2772
|
+
const { stage, api, tenant } = await resolveMcpContext(opts);
|
|
2773
|
+
printHeader("mcp list", stage);
|
|
2774
|
+
const { servers } = await apiFetch(
|
|
2775
|
+
api.apiUrl,
|
|
2776
|
+
api.authSecret,
|
|
2777
|
+
"/api/skills/mcp-servers",
|
|
2778
|
+
{},
|
|
2779
|
+
{ "x-tenant-slug": tenant.slug }
|
|
2780
|
+
);
|
|
2621
2781
|
if (!servers || servers.length === 0) {
|
|
2622
2782
|
console.log(chalk10.dim(" No MCP servers registered."));
|
|
2623
2783
|
return;
|
|
@@ -2630,74 +2790,104 @@ function registerMcpCommand(program2) {
|
|
|
2630
2790
|
console.log(` URL: ${s.url}`);
|
|
2631
2791
|
console.log(` Transport: ${s.transport}`);
|
|
2632
2792
|
console.log(` Auth: ${authLabel}`);
|
|
2633
|
-
if (s.tools?.length) {
|
|
2634
|
-
console.log(` Tools: ${s.tools.length} cached`);
|
|
2635
|
-
}
|
|
2793
|
+
if (s.tools?.length) console.log(` Tools: ${s.tools.length} cached`);
|
|
2636
2794
|
console.log("");
|
|
2637
2795
|
}
|
|
2638
2796
|
} catch (err) {
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
}
|
|
2642
|
-
});
|
|
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);
|
|
2797
|
+
if (isCancellation(err)) return;
|
|
2798
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2667
2799
|
process.exit(1);
|
|
2668
2800
|
}
|
|
2669
2801
|
});
|
|
2670
|
-
mcp.command("
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2802
|
+
mcp.command("add [name]").description("Register an MCP server. Prompts for missing fields in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--url <url>", "MCP server URL").option("--transport <type>", "Transport type (streamable-http|sse)", "streamable-http").option("--auth-type <type>", "Auth type (none|tenant_api_key|per_user_oauth)", "none").option("--api-key <token>", "API key (for tenant_api_key auth)").option("--oauth-provider <name>", "OAuth provider name (for per_user_oauth auth)").addHelpText(
|
|
2803
|
+
"after",
|
|
2804
|
+
`
|
|
2805
|
+
Examples:
|
|
2806
|
+
# Fully interactive \u2014 prompts for name, URL, and auth
|
|
2807
|
+
$ thinkwork mcp add
|
|
2808
|
+
|
|
2809
|
+
# Scripted \u2014 API-key auth
|
|
2810
|
+
$ thinkwork mcp add my-tools --url https://mcp.example.com/crm \\
|
|
2811
|
+
--auth-type tenant_api_key --api-key sk-abc -s dev -t acme
|
|
2812
|
+
|
|
2813
|
+
# OAuth connector (users connect from the mobile app)
|
|
2814
|
+
$ thinkwork mcp add lastmile --url https://mcp-dev.lastmile-tei.com/crm \\
|
|
2815
|
+
--auth-type per_user_oauth --oauth-provider lastmile -s dev -t acme
|
|
2816
|
+
`
|
|
2817
|
+
).action(
|
|
2818
|
+
async (nameArg, opts) => {
|
|
2819
|
+
try {
|
|
2820
|
+
const { input: input3 } = await import("@inquirer/prompts");
|
|
2821
|
+
const { stage, api, tenant } = await resolveMcpContext(opts);
|
|
2822
|
+
let name = nameArg;
|
|
2823
|
+
if (!name) {
|
|
2824
|
+
if (!process.stdin.isTTY) {
|
|
2825
|
+
printError("Name is required. Pass it as a positional arg.");
|
|
2826
|
+
process.exit(1);
|
|
2827
|
+
}
|
|
2828
|
+
name = await input3({ message: "Server name:" });
|
|
2829
|
+
}
|
|
2830
|
+
let url = opts.url;
|
|
2831
|
+
if (!url) {
|
|
2832
|
+
if (!process.stdin.isTTY) {
|
|
2833
|
+
printError("--url is required. Pass it as a flag.");
|
|
2834
|
+
process.exit(1);
|
|
2835
|
+
}
|
|
2836
|
+
url = await input3({
|
|
2837
|
+
message: "MCP server URL:",
|
|
2838
|
+
validate: (v) => v.startsWith("http://") || v.startsWith("https://") ? true : "URL must start with http:// or https://"
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
const body = { name, url, transport: opts.transport };
|
|
2842
|
+
if (opts.authType !== "none") body.authType = opts.authType;
|
|
2843
|
+
if (opts.apiKey) body.apiKey = opts.apiKey;
|
|
2844
|
+
if (opts.oauthProvider) body.oauthProvider = opts.oauthProvider;
|
|
2845
|
+
printHeader("mcp add", stage);
|
|
2846
|
+
const result = await apiFetch(
|
|
2847
|
+
api.apiUrl,
|
|
2848
|
+
api.authSecret,
|
|
2849
|
+
"/api/skills/mcp-servers",
|
|
2850
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
2851
|
+
{ "x-tenant-slug": tenant.slug }
|
|
2852
|
+
);
|
|
2853
|
+
printSuccess(
|
|
2854
|
+
`MCP server "${name}" ${result.created ? "registered" : "updated"} (slug: ${result.slug})`
|
|
2855
|
+
);
|
|
2856
|
+
} catch (err) {
|
|
2857
|
+
if (isCancellation(err)) return;
|
|
2858
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2859
|
+
process.exit(1);
|
|
2860
|
+
}
|
|
2675
2861
|
}
|
|
2676
|
-
|
|
2677
|
-
|
|
2862
|
+
);
|
|
2863
|
+
mcp.command("remove <id>").alias("rm").description("Remove an MCP server. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (id, opts) => {
|
|
2678
2864
|
try {
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2865
|
+
const { api, tenant } = await resolveMcpContext(opts);
|
|
2866
|
+
await apiFetch(
|
|
2867
|
+
api.apiUrl,
|
|
2868
|
+
api.authSecret,
|
|
2869
|
+
`/api/skills/mcp-servers/${id}`,
|
|
2870
|
+
{ method: "DELETE" },
|
|
2871
|
+
{ "x-tenant-slug": tenant.slug }
|
|
2872
|
+
);
|
|
2873
|
+
printSuccess("MCP server removed.");
|
|
2683
2874
|
} catch (err) {
|
|
2684
|
-
|
|
2875
|
+
if (isCancellation(err)) return;
|
|
2876
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2685
2877
|
process.exit(1);
|
|
2686
2878
|
}
|
|
2687
2879
|
});
|
|
2688
|
-
mcp.command("test <id>").description("Test connection and discover tools").
|
|
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);
|
|
2880
|
+
mcp.command("test <id>").description("Test connection and discover tools. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (id, opts) => {
|
|
2697
2881
|
try {
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2700
|
-
|
|
2882
|
+
const { stage, api, tenant } = await resolveMcpContext(opts);
|
|
2883
|
+
printHeader("mcp test", stage);
|
|
2884
|
+
const result = await apiFetch(
|
|
2885
|
+
api.apiUrl,
|
|
2886
|
+
api.authSecret,
|
|
2887
|
+
`/api/skills/mcp-servers/${id}/test`,
|
|
2888
|
+
{ method: "POST" },
|
|
2889
|
+
{ "x-tenant-slug": tenant.slug }
|
|
2890
|
+
);
|
|
2701
2891
|
if (result.ok) {
|
|
2702
2892
|
printSuccess("Connection successful.");
|
|
2703
2893
|
if (result.tools?.length) {
|
|
@@ -2705,7 +2895,9 @@ function registerMcpCommand(program2) {
|
|
|
2705
2895
|
Discovered tools (${result.tools.length}):
|
|
2706
2896
|
`));
|
|
2707
2897
|
for (const t of result.tools) {
|
|
2708
|
-
console.log(
|
|
2898
|
+
console.log(
|
|
2899
|
+
` ${chalk10.cyan(t.name)}${t.description ? chalk10.dim(` - ${t.description}`) : ""}`
|
|
2900
|
+
);
|
|
2709
2901
|
}
|
|
2710
2902
|
console.log("");
|
|
2711
2903
|
} else {
|
|
@@ -2716,86 +2908,112 @@ function registerMcpCommand(program2) {
|
|
|
2716
2908
|
process.exit(1);
|
|
2717
2909
|
}
|
|
2718
2910
|
} catch (err) {
|
|
2719
|
-
|
|
2720
|
-
|
|
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);
|
|
2911
|
+
if (isCancellation(err)) return;
|
|
2912
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2739
2913
|
process.exit(1);
|
|
2740
2914
|
}
|
|
2741
2915
|
});
|
|
2742
|
-
mcp.command("
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2916
|
+
mcp.command("assign <mcpServerId>").description("Assign an MCP server to an agent. Prompts for stage/agent when omitted in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
|
|
2917
|
+
async (mcpServerId, opts) => {
|
|
2918
|
+
try {
|
|
2919
|
+
const { input: input3 } = await import("@inquirer/prompts");
|
|
2920
|
+
const { api } = await resolveMcpContext(opts);
|
|
2921
|
+
let agent = opts.agent;
|
|
2922
|
+
if (!agent) {
|
|
2923
|
+
if (!process.stdin.isTTY) {
|
|
2924
|
+
printError("--agent is required. Pass it as a flag.");
|
|
2925
|
+
process.exit(1);
|
|
2926
|
+
}
|
|
2927
|
+
agent = await input3({ message: "Agent ID:" });
|
|
2928
|
+
}
|
|
2929
|
+
const result = await apiFetch(
|
|
2930
|
+
api.apiUrl,
|
|
2931
|
+
api.authSecret,
|
|
2932
|
+
`/api/skills/agents/${agent}/mcp-servers`,
|
|
2933
|
+
{ method: "POST", body: JSON.stringify({ mcpServerId }) }
|
|
2934
|
+
);
|
|
2935
|
+
printSuccess(`MCP server assigned to agent. (${result.created ? "new" : "updated"})`);
|
|
2936
|
+
} catch (err) {
|
|
2937
|
+
if (isCancellation(err)) return;
|
|
2938
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2939
|
+
process.exit(1);
|
|
2940
|
+
}
|
|
2747
2941
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2942
|
+
);
|
|
2943
|
+
mcp.command("unassign <mcpServerId>").description("Remove an MCP server from an agent. Prompts for stage/agent when omitted in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
|
|
2944
|
+
async (mcpServerId, opts) => {
|
|
2945
|
+
try {
|
|
2946
|
+
const { input: input3 } = await import("@inquirer/prompts");
|
|
2947
|
+
const { api } = await resolveMcpContext(opts);
|
|
2948
|
+
let agent = opts.agent;
|
|
2949
|
+
if (!agent) {
|
|
2950
|
+
if (!process.stdin.isTTY) {
|
|
2951
|
+
printError("--agent is required. Pass it as a flag.");
|
|
2952
|
+
process.exit(1);
|
|
2953
|
+
}
|
|
2954
|
+
agent = await input3({ message: "Agent ID:" });
|
|
2955
|
+
}
|
|
2956
|
+
await apiFetch(
|
|
2957
|
+
api.apiUrl,
|
|
2958
|
+
api.authSecret,
|
|
2959
|
+
`/api/skills/agents/${agent}/mcp-servers/${mcpServerId}`,
|
|
2960
|
+
{ method: "DELETE" }
|
|
2961
|
+
);
|
|
2962
|
+
printSuccess("MCP server unassigned from agent.");
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
if (isCancellation(err)) return;
|
|
2965
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2966
|
+
process.exit(1);
|
|
2967
|
+
}
|
|
2758
2968
|
}
|
|
2759
|
-
|
|
2969
|
+
);
|
|
2760
2970
|
}
|
|
2761
2971
|
|
|
2762
2972
|
// src/commands/tools.ts
|
|
2763
|
-
import {
|
|
2973
|
+
import { select as select5, password } from "@inquirer/prompts";
|
|
2764
2974
|
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
2975
|
var TOOL_PROVIDERS = {
|
|
2775
2976
|
"web-search": ["exa", "serpapi"]
|
|
2776
2977
|
};
|
|
2978
|
+
async function resolveCtx(opts) {
|
|
2979
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
2980
|
+
const api = resolveApiConfig(stage);
|
|
2981
|
+
if (!api) process.exit(1);
|
|
2982
|
+
const tenant = await resolveTenantRest({
|
|
2983
|
+
flag: opts.tenant,
|
|
2984
|
+
stage,
|
|
2985
|
+
apiUrl: api.apiUrl,
|
|
2986
|
+
authSecret: api.authSecret
|
|
2987
|
+
});
|
|
2988
|
+
return { stage, api, tenant };
|
|
2989
|
+
}
|
|
2777
2990
|
function registerToolsCommand(program2) {
|
|
2778
2991
|
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").
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2992
|
+
tools.command("list").alias("ls").description("List configured built-in tools. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
2993
|
+
"after",
|
|
2994
|
+
`
|
|
2995
|
+
Examples:
|
|
2996
|
+
# Interactive \u2014 picks stage + tenant
|
|
2997
|
+
$ thinkwork tools list
|
|
2998
|
+
|
|
2999
|
+
# Scripted
|
|
3000
|
+
$ thinkwork tools list -s dev -t acme
|
|
3001
|
+
$ thinkwork tools list --json | jq '.[] | .toolSlug'
|
|
3002
|
+
`
|
|
3003
|
+
).action(async (opts) => {
|
|
2788
3004
|
try {
|
|
3005
|
+
const { stage, api, tenant } = await resolveCtx(opts);
|
|
3006
|
+
printHeader("tools list", stage);
|
|
2789
3007
|
const { tools: rows } = await apiFetch(
|
|
2790
3008
|
api.apiUrl,
|
|
2791
3009
|
api.authSecret,
|
|
2792
3010
|
"/api/skills/builtin-tools",
|
|
2793
3011
|
{},
|
|
2794
|
-
{ "x-tenant-slug":
|
|
3012
|
+
{ "x-tenant-slug": tenant.slug }
|
|
2795
3013
|
);
|
|
2796
3014
|
if (!rows || rows.length === 0) {
|
|
2797
3015
|
console.log(chalk11.dim(" No built-in tools configured."));
|
|
2798
|
-
console.log(chalk11.dim(" Try: thinkwork tools web-search set
|
|
3016
|
+
console.log(chalk11.dim(" Try: thinkwork tools web-search set"));
|
|
2799
3017
|
return;
|
|
2800
3018
|
}
|
|
2801
3019
|
console.log("");
|
|
@@ -2806,75 +3024,84 @@ function registerToolsCommand(program2) {
|
|
|
2806
3024
|
console.log(` ${chalk11.bold(r.toolSlug)} ${status}`);
|
|
2807
3025
|
console.log(` Provider: ${provider}`);
|
|
2808
3026
|
console.log(` Has key: ${key}`);
|
|
2809
|
-
if (r.lastTestedAt) {
|
|
2810
|
-
console.log(` Tested: ${new Date(r.lastTestedAt).toLocaleString()}`);
|
|
2811
|
-
}
|
|
3027
|
+
if (r.lastTestedAt) console.log(` Tested: ${new Date(r.lastTestedAt).toLocaleString()}`);
|
|
2812
3028
|
console.log("");
|
|
2813
3029
|
}
|
|
2814
3030
|
} catch (err) {
|
|
2815
|
-
|
|
3031
|
+
if (isCancellation(err)) return;
|
|
3032
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2816
3033
|
process.exit(1);
|
|
2817
3034
|
}
|
|
2818
3035
|
});
|
|
2819
3036
|
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)").
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
3037
|
+
webSearch.command("set").description("Set or update web_search provider + API key (enables the tool). Prompts when flags are missing in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--provider <name>", `Provider (${TOOL_PROVIDERS["web-search"].join("|")})`).option("--key <key>", "API key (will prompt hidden if omitted)").addHelpText(
|
|
3038
|
+
"after",
|
|
3039
|
+
`
|
|
3040
|
+
Examples:
|
|
3041
|
+
# Fully interactive \u2014 stage/tenant picker, provider picker, hidden key prompt
|
|
3042
|
+
$ thinkwork tools web-search set
|
|
3043
|
+
|
|
3044
|
+
# Scripted (secret from env)
|
|
3045
|
+
$ thinkwork tools web-search set -s dev -t acme --provider exa --key "$EXA_KEY"
|
|
3046
|
+
`
|
|
3047
|
+
).action(
|
|
3048
|
+
async (opts) => {
|
|
3049
|
+
try {
|
|
3050
|
+
const { api, tenant } = await resolveCtx(opts);
|
|
3051
|
+
let provider = opts.provider;
|
|
3052
|
+
if (!provider) {
|
|
3053
|
+
if (!process.stdin.isTTY) {
|
|
3054
|
+
printError(`--provider is required. One of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
|
|
3055
|
+
process.exit(1);
|
|
3056
|
+
}
|
|
3057
|
+
provider = await select5({
|
|
3058
|
+
message: "Provider:",
|
|
3059
|
+
choices: TOOL_PROVIDERS["web-search"].map((p) => ({ name: p, value: p })),
|
|
3060
|
+
loop: false
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
if (!TOOL_PROVIDERS["web-search"].includes(provider)) {
|
|
3064
|
+
printError(`provider must be one of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
|
|
3065
|
+
process.exit(1);
|
|
3066
|
+
}
|
|
3067
|
+
let apiKey = opts.key;
|
|
3068
|
+
if (!apiKey) {
|
|
3069
|
+
if (!process.stdin.isTTY) {
|
|
3070
|
+
printError("--key is required. Pass it as a flag or pipe via env.");
|
|
3071
|
+
process.exit(1);
|
|
3072
|
+
}
|
|
3073
|
+
apiKey = await password({ message: `${provider} API key:`, mask: "*" });
|
|
3074
|
+
}
|
|
3075
|
+
if (!apiKey) {
|
|
3076
|
+
printError("API key is required");
|
|
3077
|
+
process.exit(1);
|
|
3078
|
+
}
|
|
3079
|
+
await apiFetch(
|
|
3080
|
+
api.apiUrl,
|
|
3081
|
+
api.authSecret,
|
|
3082
|
+
"/api/skills/builtin-tools/web-search",
|
|
3083
|
+
{ method: "PUT", body: JSON.stringify({ provider, apiKey, enabled: true }) },
|
|
3084
|
+
{ "x-tenant-slug": tenant.slug }
|
|
3085
|
+
);
|
|
3086
|
+
printSuccess(`web_search configured with provider=${provider}, enabled=true`);
|
|
3087
|
+
printWarning("Run `thinkwork tools web-search test` to verify connectivity.");
|
|
3088
|
+
} catch (err) {
|
|
3089
|
+
if (isCancellation(err)) return;
|
|
3090
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
3091
|
+
process.exit(1);
|
|
3092
|
+
}
|
|
2867
3093
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
printHeader("tools web-search test", opts.stage);
|
|
3094
|
+
);
|
|
3095
|
+
webSearch.command("test").description("Test the stored web_search provider + key against the provider API.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (opts) => {
|
|
2871
3096
|
try {
|
|
3097
|
+
const { stage, api, tenant } = await resolveCtx(opts);
|
|
3098
|
+
printHeader("tools web-search test", stage);
|
|
2872
3099
|
const result = await apiFetch(
|
|
2873
3100
|
api.apiUrl,
|
|
2874
3101
|
api.authSecret,
|
|
2875
3102
|
"/api/skills/builtin-tools/web-search/test",
|
|
2876
3103
|
{ method: "POST", body: "{}" },
|
|
2877
|
-
{ "x-tenant-slug":
|
|
3104
|
+
{ "x-tenant-slug": tenant.slug }
|
|
2878
3105
|
);
|
|
2879
3106
|
if (result.ok) {
|
|
2880
3107
|
printSuccess(`${result.provider}: ${result.resultCount} result(s) returned.`);
|
|
@@ -2883,51 +3110,42 @@ function registerToolsCommand(program2) {
|
|
|
2883
3110
|
process.exit(1);
|
|
2884
3111
|
}
|
|
2885
3112
|
} catch (err) {
|
|
2886
|
-
|
|
3113
|
+
if (isCancellation(err)) return;
|
|
3114
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2887
3115
|
process.exit(1);
|
|
2888
3116
|
}
|
|
2889
3117
|
});
|
|
2890
|
-
webSearch.command("disable").description("Disable web_search without deleting the stored key").
|
|
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);
|
|
3118
|
+
webSearch.command("disable").description("Disable web_search without deleting the stored key.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (opts) => {
|
|
2898
3119
|
try {
|
|
3120
|
+
const { api, tenant } = await resolveCtx(opts);
|
|
2899
3121
|
await apiFetch(
|
|
2900
3122
|
api.apiUrl,
|
|
2901
3123
|
api.authSecret,
|
|
2902
3124
|
"/api/skills/builtin-tools/web-search",
|
|
2903
3125
|
{ method: "PUT", body: JSON.stringify({ enabled: false }) },
|
|
2904
|
-
{ "x-tenant-slug":
|
|
3126
|
+
{ "x-tenant-slug": tenant.slug }
|
|
2905
3127
|
);
|
|
2906
3128
|
printSuccess("web_search disabled.");
|
|
2907
3129
|
} catch (err) {
|
|
2908
|
-
|
|
3130
|
+
if (isCancellation(err)) return;
|
|
3131
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2909
3132
|
process.exit(1);
|
|
2910
3133
|
}
|
|
2911
3134
|
});
|
|
2912
|
-
webSearch.command("clear").description("Remove web_search config entirely (deletes stored key)").
|
|
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);
|
|
3135
|
+
webSearch.command("clear").description("Remove web_search config entirely (deletes stored key).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (opts) => {
|
|
2920
3136
|
try {
|
|
3137
|
+
const { api, tenant } = await resolveCtx(opts);
|
|
2921
3138
|
await apiFetch(
|
|
2922
3139
|
api.apiUrl,
|
|
2923
3140
|
api.authSecret,
|
|
2924
3141
|
"/api/skills/builtin-tools/web-search",
|
|
2925
3142
|
{ method: "DELETE" },
|
|
2926
|
-
{ "x-tenant-slug":
|
|
3143
|
+
{ "x-tenant-slug": tenant.slug }
|
|
2927
3144
|
);
|
|
2928
3145
|
printSuccess("web_search configuration cleared.");
|
|
2929
3146
|
} catch (err) {
|
|
2930
|
-
|
|
3147
|
+
if (isCancellation(err)) return;
|
|
3148
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2931
3149
|
process.exit(1);
|
|
2932
3150
|
}
|
|
2933
3151
|
});
|
|
@@ -3024,7 +3242,7 @@ function registerUpdateCommand(program2) {
|
|
|
3024
3242
|
|
|
3025
3243
|
// src/commands/user.ts
|
|
3026
3244
|
import { spawn as spawn4 } from "child_process";
|
|
3027
|
-
import { input, select as
|
|
3245
|
+
import { input as input2, select as select6 } from "@inquirer/prompts";
|
|
3028
3246
|
function getTerraformOutput2(cwd, key) {
|
|
3029
3247
|
return new Promise((resolve3, reject) => {
|
|
3030
3248
|
const proc = spawn4("terraform", ["output", "-raw", key], {
|
|
@@ -3067,9 +3285,6 @@ function runAwsCognitoReset(userPoolId, username, region) {
|
|
|
3067
3285
|
proc.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
|
|
3068
3286
|
});
|
|
3069
3287
|
}
|
|
3070
|
-
function isCancellation2(err) {
|
|
3071
|
-
return err instanceof Error && err.name === "ExitPromptError";
|
|
3072
|
-
}
|
|
3073
3288
|
function requireTty2(label) {
|
|
3074
3289
|
if (!process.stdin.isTTY) {
|
|
3075
3290
|
printError(
|
|
@@ -3080,7 +3295,7 @@ function requireTty2(label) {
|
|
|
3080
3295
|
}
|
|
3081
3296
|
async function promptEmail() {
|
|
3082
3297
|
requireTty2("Email");
|
|
3083
|
-
return await
|
|
3298
|
+
return await input2({
|
|
3084
3299
|
message: "Email address of the person to invite:",
|
|
3085
3300
|
validate: (v) => v.trim().includes("@") ? true : "That doesn't look like an email."
|
|
3086
3301
|
});
|
|
@@ -3098,7 +3313,7 @@ async function promptStage(region) {
|
|
|
3098
3313
|
console.log(` Using the only deployed stage: ${stages[0]}`);
|
|
3099
3314
|
return stages[0];
|
|
3100
3315
|
}
|
|
3101
|
-
return await
|
|
3316
|
+
return await select6({
|
|
3102
3317
|
message: "Which stage?",
|
|
3103
3318
|
choices: stages.map((s) => ({ name: s, value: s })),
|
|
3104
3319
|
loop: false
|
|
@@ -3117,7 +3332,7 @@ async function promptTenant(apiUrl, authSecret) {
|
|
|
3117
3332
|
console.log(` Using the only tenant: ${list[0].name} (${list[0].slug})`);
|
|
3118
3333
|
return list[0].slug;
|
|
3119
3334
|
}
|
|
3120
|
-
return await
|
|
3335
|
+
return await select6({
|
|
3121
3336
|
message: "Which tenant?",
|
|
3122
3337
|
choices: list.map((t) => ({
|
|
3123
3338
|
name: `${t.name} (slug: ${t.slug})`,
|
|
@@ -3128,7 +3343,7 @@ async function promptTenant(apiUrl, authSecret) {
|
|
|
3128
3343
|
}
|
|
3129
3344
|
async function promptOptionalName() {
|
|
3130
3345
|
if (!process.stdin.isTTY) return void 0;
|
|
3131
|
-
const answer = await
|
|
3346
|
+
const answer = await input2({
|
|
3132
3347
|
message: "Display name (optional, press Enter to skip):",
|
|
3133
3348
|
default: ""
|
|
3134
3349
|
});
|
|
@@ -3136,7 +3351,7 @@ async function promptOptionalName() {
|
|
|
3136
3351
|
}
|
|
3137
3352
|
async function promptRole() {
|
|
3138
3353
|
if (!process.stdin.isTTY) return "member";
|
|
3139
|
-
return await
|
|
3354
|
+
return await select6({
|
|
3140
3355
|
message: "Role:",
|
|
3141
3356
|
choices: [
|
|
3142
3357
|
{ name: "member \u2014 regular access", value: "member" },
|
|
@@ -3245,7 +3460,7 @@ Agents / scripts that pass all flags stay non-interactive.
|
|
|
3245
3460
|
`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
3461
|
);
|
|
3247
3462
|
} catch (err) {
|
|
3248
|
-
if (
|
|
3463
|
+
if (isCancellation(err)) {
|
|
3249
3464
|
console.log("");
|
|
3250
3465
|
console.log(" Cancelled.");
|
|
3251
3466
|
return;
|
|
@@ -3258,15 +3473,18 @@ Agents / scripts that pass all flags stay non-interactive.
|
|
|
3258
3473
|
}
|
|
3259
3474
|
);
|
|
3260
3475
|
user.command("reset-password <email>").description(
|
|
3261
|
-
"Trigger Cognito's forgot-password flow for a user (admin-initiated).
|
|
3262
|
-
).option("-p, --profile <name>", "AWS profile").
|
|
3476
|
+
"Trigger Cognito's forgot-password flow for a user (admin-initiated). Prompts for stage in a TTY when omitted."
|
|
3477
|
+
).option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage (e.g. dev, prod)").option(
|
|
3263
3478
|
"-r, --region <name>",
|
|
3264
3479
|
"AWS region (defaults to AWS CLI default / AWS_REGION)"
|
|
3265
3480
|
).addHelpText(
|
|
3266
3481
|
"after",
|
|
3267
3482
|
`
|
|
3268
3483
|
Examples:
|
|
3269
|
-
#
|
|
3484
|
+
# Fully interactive \u2014 picks stage from the deployed ones
|
|
3485
|
+
$ thinkwork user reset-password alice@example.com
|
|
3486
|
+
|
|
3487
|
+
# Scripted
|
|
3270
3488
|
$ thinkwork user reset-password alice@example.com -s dev
|
|
3271
3489
|
|
|
3272
3490
|
# Target a specific AWS profile + region
|
|
@@ -3279,10 +3497,12 @@ FORCE_CHANGE_PASSWORD or has been disabled.
|
|
|
3279
3497
|
`
|
|
3280
3498
|
).action(
|
|
3281
3499
|
async (email, opts) => {
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3500
|
+
let stage;
|
|
3501
|
+
try {
|
|
3502
|
+
stage = await resolveStage({ flag: opts.stage, region: opts.region });
|
|
3503
|
+
} catch (err) {
|
|
3504
|
+
if (isCancellation(err)) return;
|
|
3505
|
+
throw err;
|
|
3286
3506
|
}
|
|
3287
3507
|
if (!email || !email.includes("@")) {
|
|
3288
3508
|
printError(
|
|
@@ -3290,11 +3510,11 @@ FORCE_CHANGE_PASSWORD or has been disabled.
|
|
|
3290
3510
|
);
|
|
3291
3511
|
process.exit(1);
|
|
3292
3512
|
}
|
|
3293
|
-
printHeader("user reset-password",
|
|
3513
|
+
printHeader("user reset-password", stage);
|
|
3294
3514
|
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
3295
|
-
const cwd = resolveTierDir(terraformDir,
|
|
3515
|
+
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
3296
3516
|
await ensureInit(cwd);
|
|
3297
|
-
await ensureWorkspace(cwd,
|
|
3517
|
+
await ensureWorkspace(cwd, stage);
|
|
3298
3518
|
let userPoolId;
|
|
3299
3519
|
try {
|
|
3300
3520
|
userPoolId = await getTerraformOutput2(cwd, "user_pool_id");
|
|
@@ -3341,47 +3561,6 @@ FORCE_CHANGE_PASSWORD or has been disabled.
|
|
|
3341
3561
|
// src/commands/me.ts
|
|
3342
3562
|
import { gql } from "@urql/core";
|
|
3343
3563
|
|
|
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
3564
|
// src/lib/gql-client.ts
|
|
3386
3565
|
import {
|
|
3387
3566
|
Client,
|
|
@@ -3675,8 +3854,8 @@ Examples:
|
|
|
3675
3854
|
$ thinkwork thread label remove thr-abc lbl-ops
|
|
3676
3855
|
`
|
|
3677
3856
|
).action(() => notYetImplemented("thread label", 1));
|
|
3678
|
-
thread.command("escalate <id>").description("Escalate a thread to another agent (carries context, records actor).").
|
|
3679
|
-
thread.command("delegate <id>").description("Delegate ownership to another agent without the 'escalation' semantics.").
|
|
3857
|
+
thread.command("escalate <id>").description("Escalate a thread to another agent (carries context, records actor).").option("--to-agent <id>", "Agent to escalate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--reason <text>", "Reason note (appears in activity log)").action(() => notYetImplemented("thread escalate", 1));
|
|
3858
|
+
thread.command("delegate <id>").description("Delegate ownership to another agent without the 'escalation' semantics.").option("--to-agent <id>", "Agent to delegate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread delegate", 1));
|
|
3680
3859
|
thread.command("delete <id>").description("Permanently delete a thread (not just close). Requires confirmation.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip the confirmation prompt").addHelpText(
|
|
3681
3860
|
"after",
|
|
3682
3861
|
`
|
|
@@ -3759,7 +3938,7 @@ Examples:
|
|
|
3759
3938
|
`
|
|
3760
3939
|
).action(() => notYetImplemented("inbox approve", 1));
|
|
3761
3940
|
inbox.command("reject <id>").description("Reject an inbox item. Downstream agents stop.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--notes <text>", "Rejection reason").action(() => notYetImplemented("inbox reject", 1));
|
|
3762
|
-
inbox.command("request-revision <id>").description("Ask for changes \u2014 the agent gets the item back with your notes.").
|
|
3941
|
+
inbox.command("request-revision <id>").description("Ask for changes \u2014 the agent gets the item back with your notes.").option("--notes <text>", "What needs to change").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox request-revision", 1));
|
|
3763
3942
|
inbox.command("resubmit <id>").description("Resubmit a revised inbox item for approval.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--notes <text>", "What changed").action(() => notYetImplemented("inbox resubmit", 1));
|
|
3764
3943
|
inbox.command("cancel <id>").description("Cancel a pending approval request (originator or admin).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox cancel", 1));
|
|
3765
3944
|
inbox.command("comment <id> [content]").description("Add a comment to an inbox item without deciding on it yet.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--file <path>", "Read comment body from a file").action(() => notYetImplemented("inbox comment", 1));
|
|
@@ -3811,15 +3990,15 @@ Examples:
|
|
|
3811
3990
|
).action(() => notYetImplemented("agent status", 2));
|
|
3812
3991
|
agent.command("unpause <id>").description("Resume an agent paused by a budget policy trigger.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent unpause", 2));
|
|
3813
3992
|
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.").
|
|
3993
|
+
capabilities.command("set <agentId>").description("Enable/disable capabilities on an agent.").option("--capability <name>", "Capability name (email, web-search, file-upload, \u2026)").option("--enabled", "Enable (default if flag present)").option("--disabled", "Disable").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent capabilities set", 2));
|
|
3815
3994
|
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.").
|
|
3995
|
+
skills.command("set <agentId>").description("Enable/disable/configure a skill for an agent.").option("--skill <id>", "Skill ID (see `thinkwork skill list`)").option("--enabled", "Enable").option("--disabled", "Disable").option("--config <json>", "Inline JSON config for the skill").option("--rate-limit <rps>", "Rate limit in requests/sec").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent skills set", 2));
|
|
3817
3996
|
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.").
|
|
3997
|
+
budget.command("set <agentId>").description("Set or update an agent's budget policy.").option("--limit-usd <amount>", "USD ceiling for the window").option("--window <w>", "daily | weekly | monthly", "monthly").option("--action <a>", "PAUSE | ALERT", "PAUSE").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent budget set", 2));
|
|
3819
3998
|
budget.command("clear <agentId>").description("Remove an agent's budget policy (falls back to tenant-wide).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent budget clear", 2));
|
|
3820
3999
|
const apiKey = agent.command("api-key").description("Agent API keys \u2014 service-to-service credentials tied to one agent.");
|
|
3821
4000
|
apiKey.command("list <agentId>").description("List API keys for an agent (metadata only; plaintext shown on create).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent api-key list", 2));
|
|
3822
|
-
apiKey.command("create <agentId>").description("Generate a new API key. The plaintext is printed once \u2014 save it.").
|
|
4001
|
+
apiKey.command("create <agentId>").description("Generate a new API key. The plaintext is printed once \u2014 save it.").option("--name <n>", "Human label for the key (e.g. 'GitHub Actions')").option("--expires <iso>", "Expiration (ISO-8601). Omit for no expiry.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
3823
4002
|
"after",
|
|
3824
4003
|
`
|
|
3825
4004
|
Examples:
|
|
@@ -3956,7 +4135,7 @@ Examples:
|
|
|
3956
4135
|
kb.command("update <id>").description("Update knowledge base metadata (name, description). Source changes need re-create.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").action(() => notYetImplemented("kb update", 2));
|
|
3957
4136
|
kb.command("delete <id>").description("Delete a knowledge base. Embeddings + index are destroyed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("kb delete", 2));
|
|
3958
4137
|
kb.command("sync <id>").description("Re-embed from S3. Idempotent; safe to re-run.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--wait", "Block until the sync finishes").action(() => notYetImplemented("kb sync", 2));
|
|
3959
|
-
kb.command("attach <kbId>").description("Attach a knowledge base to an agent.").
|
|
4138
|
+
kb.command("attach <kbId>").description("Attach a knowledge base to an agent.").option("--agent <id>", "Agent ID").option("--config <json>", "Retrieval config (topK, score threshold, \u2026)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
3960
4139
|
"after",
|
|
3961
4140
|
`
|
|
3962
4141
|
Examples:
|
|
@@ -3964,7 +4143,7 @@ Examples:
|
|
|
3964
4143
|
$ thinkwork kb attach kb-runbooks --agent agt-oncall --config '{"topK":5}'
|
|
3965
4144
|
`
|
|
3966
4145
|
).action(() => notYetImplemented("kb attach", 2));
|
|
3967
|
-
kb.command("detach <kbId>").description("Detach a knowledge base from an agent.").
|
|
4146
|
+
kb.command("detach <kbId>").description("Detach a knowledge base from an agent.").option("--agent <id>", "Agent ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("kb detach", 2));
|
|
3968
4147
|
}
|
|
3969
4148
|
|
|
3970
4149
|
// src/commands/routine.ts
|
|
@@ -3987,7 +4166,7 @@ Examples:
|
|
|
3987
4166
|
run2.command("list <routineId>").alias("ls").description("List recent runs of a routine.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max rows", "25").option("--cursor <c>", "Pagination cursor").action(() => notYetImplemented("routine run list", 3));
|
|
3988
4167
|
run2.command("get <runId>").description("Fetch one run with its step outputs.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("routine run get", 3));
|
|
3989
4168
|
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.").
|
|
4169
|
+
trigger.command("set <routineId>").description("Set or replace a trigger for a routine.").option("--type <t>", "CRON | WEBHOOK | EVENT").option("--schedule <cron>", "Cron expression (for CRON triggers)").option("--event <name>", "Event name (for EVENT triggers)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
3991
4170
|
"after",
|
|
3992
4171
|
`
|
|
3993
4172
|
Examples:
|
|
@@ -4040,7 +4219,7 @@ Examples:
|
|
|
4040
4219
|
function registerWakeupCommand(program2) {
|
|
4041
4220
|
const wake = program2.command("wakeup").alias("wakeups").description("View and create agent wakeup requests (deferred/enqueued invocations).");
|
|
4042
4221
|
wake.command("list").alias("ls").description("List queued wakeups in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("wakeup list", 3));
|
|
4043
|
-
wake.command("create").description("Queue a wakeup for an agent (ad-hoc or deferred).").
|
|
4222
|
+
wake.command("create").description("Queue a wakeup for an agent (ad-hoc or deferred).").option("--agent <id>", "Target agent").option("--thread <id>", "Thread to operate on (optional)").option("--delay-seconds <n>", "Wait N seconds before firing", "0").option("--payload <json>", "Optional input payload").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
4044
4223
|
"after",
|
|
4045
4224
|
`
|
|
4046
4225
|
Examples:
|
|
@@ -4122,12 +4301,12 @@ Examples:
|
|
|
4122
4301
|
// src/commands/memory.ts
|
|
4123
4302
|
function registerMemoryCommand(program2) {
|
|
4124
4303
|
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.").
|
|
4304
|
+
memory.command("list").alias("ls").description("List memory records for an agent in a namespace.").option("--agent <id>", "Agent (assistant) ID").option(
|
|
4126
4305
|
"--namespace <ns>",
|
|
4127
4306
|
"Memory namespace (semantic | preferences | episodes | reflections)",
|
|
4128
4307
|
"semantic"
|
|
4129
4308
|
).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.").
|
|
4309
|
+
memory.command("search").description("Search an agent's memory by query string.").option("--agent <id>", "Agent (assistant) ID").option("--query <q>", "Search query").option("--strategy <s>", "Retrieval strategy (semantic | keyword | hybrid)", "semantic").option("--limit <n>", "Max results", "10").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
4131
4310
|
"after",
|
|
4132
4311
|
`
|
|
4133
4312
|
Examples:
|
|
@@ -4136,9 +4315,9 @@ Examples:
|
|
|
4136
4315
|
`
|
|
4137
4316
|
).action(() => notYetImplemented("memory search", 4));
|
|
4138
4317
|
memory.command("get <recordId>").description("Fetch one memory record in full.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory get", 4));
|
|
4139
|
-
memory.command("update <recordId>").description("Replace a memory record's content.").
|
|
4318
|
+
memory.command("update <recordId>").description("Replace a memory record's content.").option("--content <text>", "New content").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory update", 4));
|
|
4140
4319
|
memory.command("delete <recordId>").description("Remove a memory record.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("memory delete", 4));
|
|
4141
|
-
memory.command("graph").description("Print the agent's memory graph (summary in human mode; full JSON with --json).").
|
|
4320
|
+
memory.command("graph").description("Print the agent's memory graph (summary in human mode; full JSON with --json).").option("--agent <id>", "Agent (assistant) ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory graph", 4));
|
|
4142
4321
|
}
|
|
4143
4322
|
|
|
4144
4323
|
// src/commands/recipe.ts
|
|
@@ -4199,7 +4378,7 @@ function registerBudgetCommand(program2) {
|
|
|
4199
4378
|
const budget = program2.command("budget").alias("budgets").description("Manage budget policies (tenant-wide or per-agent) and inspect current status.");
|
|
4200
4379
|
budget.command("list").alias("ls").description("List budget policies in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("budget list", 5));
|
|
4201
4380
|
budget.command("status").description("Show each budget's current spend vs. limit.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("budget status", 5));
|
|
4202
|
-
budget.command("upsert").description("Create or update a budget policy.").
|
|
4381
|
+
budget.command("upsert").description("Create or update a budget policy.").option("--limit-usd <amount>", "USD ceiling for the window").option("--scope <s>", "tenant | agent", "tenant").option("--agent <id>", "Required if --scope=agent").option("--window <w>", "daily | weekly | monthly", "monthly").option("--action <a>", "PAUSE | ALERT", "PAUSE").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
4203
4382
|
"after",
|
|
4204
4383
|
`
|
|
4205
4384
|
Examples:
|