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