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.
- package/dist/cli.js +928 -565
- 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,83 +1471,24 @@ function escapeHtml(s) {
|
|
|
1364
1471
|
});
|
|
1365
1472
|
}
|
|
1366
1473
|
|
|
1367
|
-
// src/
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
1373
|
-
timeout: 15e3,
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
|
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,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").
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
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 {
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
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("
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
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
|
-
|
|
2677
|
-
|
|
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
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
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
|
-
|
|
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
|
|
2689
|
-
|
|
2690
|
-
|
|
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
|
|
2699
|
-
|
|
2700
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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);
|
|
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("
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
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
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
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 {
|
|
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").
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
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":
|
|
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
|
|
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
|
-
|
|
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)").
|
|
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
|
-
|
|
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
|
-
|
|
2869
|
-
|
|
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":
|
|
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
|
-
|
|
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").
|
|
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":
|
|
3310
|
+
{ "x-tenant-slug": tenant.slug }
|
|
2905
3311
|
);
|
|
2906
3312
|
printSuccess("web_search disabled.");
|
|
2907
3313
|
} catch (err) {
|
|
2908
|
-
|
|
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)").
|
|
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":
|
|
3327
|
+
{ "x-tenant-slug": tenant.slug }
|
|
2927
3328
|
);
|
|
2928
3329
|
printSuccess("web_search configuration cleared.");
|
|
2929
3330
|
} catch (err) {
|
|
2930
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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).
|
|
3262
|
-
).option("-p, --profile <name>", "AWS profile").
|
|
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
|
-
#
|
|
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
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
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",
|
|
3697
|
+
printHeader("user reset-password", stage);
|
|
3294
3698
|
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
3295
|
-
const cwd = resolveTierDir(terraformDir,
|
|
3699
|
+
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
3296
3700
|
await ensureInit(cwd);
|
|
3297
|
-
await ensureWorkspace(cwd,
|
|
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).").
|
|
3679
|
-
thread.command("delegate <id>").description("Delegate ownership to another agent without the 'escalation' semantics.").
|
|
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.").
|
|
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.").
|
|
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.").
|
|
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.").
|
|
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.").
|
|
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.").
|
|
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.").
|
|
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.").
|
|
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).").
|
|
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.").
|
|
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.").
|
|
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.").
|
|
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).").
|
|
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.").
|
|
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:
|