thinkwork-cli 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +310 -72
  2. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -776,8 +776,8 @@ function registerBootstrapCommand(program2) {
776
776
  bucket = await getTerraformOutput(cwd, "bucket_name");
777
777
  dbEndpoint = await getTerraformOutput(cwd, "db_cluster_endpoint");
778
778
  const secretArn = await getTerraformOutput(cwd, "db_secret_arn");
779
- const { execSync: execSync9 } = await import("child_process");
780
- const secretJson = execSync9(
779
+ const { execSync: execSync10 } = await import("child_process");
780
+ const secretJson = execSync10(
781
781
  `aws secretsmanager get-secret-value --secret-id "${secretArn}" --query SecretString --output text`,
782
782
  { encoding: "utf-8" }
783
783
  ).trim();
@@ -802,6 +802,7 @@ function registerBootstrapCommand(program2) {
802
802
  // src/commands/login.ts
803
803
  import { execSync as execSync4 } from "child_process";
804
804
  import { createInterface as createInterface2 } from "readline";
805
+ import { select, Separator } from "@inquirer/prompts";
805
806
  import chalk5 from "chalk";
806
807
 
807
808
  // src/aws-profiles.ts
@@ -1036,33 +1037,41 @@ function describeType(type) {
1036
1037
  }
1037
1038
  }
1038
1039
  async function pickProfile(profiles) {
1039
- console.log("");
1040
- console.log(chalk5.bold(" Found these profiles in ~/.aws:"));
1041
- profiles.forEach((p, i) => {
1042
- const idx = String(i + 1).padStart(2, " ");
1043
- console.log(
1044
- ` ${chalk5.cyan(idx)}. ${chalk5.bold(p.name)} ${chalk5.dim(`(${describeType(p.type)})`)}`
1040
+ if (!process.stdin.isTTY) {
1041
+ printError(
1042
+ "The profile picker needs an interactive terminal. Re-run with --keys, --sso, or --profile <name>."
1045
1043
  );
1046
- });
1047
- const newIdx = profiles.length + 1;
1048
- const ssoIdx = profiles.length + 2;
1049
- console.log(
1050
- ` ${chalk5.cyan(String(newIdx).padStart(2, " "))}. Enter new access keys`
1051
- );
1052
- console.log(
1053
- ` ${chalk5.cyan(String(ssoIdx).padStart(2, " "))}. Log in via AWS SSO`
1054
- );
1055
- console.log("");
1056
- const answer = await ask(` Pick a profile [1-${ssoIdx}] (Enter to cancel): `);
1057
- if (!answer) return { kind: "cancel" };
1058
- const n = Number.parseInt(answer, 10);
1059
- if (Number.isNaN(n) || n < 1 || n > ssoIdx) {
1060
- printError(`"${answer}" is not a valid option.`);
1061
1044
  return { kind: "cancel" };
1062
1045
  }
1063
- if (n === newIdx) return { kind: "keys" };
1064
- if (n === ssoIdx) return { kind: "sso" };
1065
- return { kind: "existing", name: profiles[n - 1].name };
1046
+ const choices = profiles.map((p) => ({
1047
+ name: `${p.name} ${chalk5.dim(`(${describeType(p.type)})`)}`,
1048
+ value: { kind: "existing", name: p.name }
1049
+ }));
1050
+ choices.push(new Separator());
1051
+ choices.push({
1052
+ name: "Enter new access keys",
1053
+ value: { kind: "keys" },
1054
+ description: "Paste an AWS Access Key ID and Secret Access Key; saved to a new profile."
1055
+ });
1056
+ choices.push({
1057
+ name: "Log in via AWS SSO",
1058
+ value: { kind: "sso" },
1059
+ description: "Run `aws sso login` against the configured SSO profile."
1060
+ });
1061
+ try {
1062
+ const picked = await select({
1063
+ message: "Pick an AWS profile for Thinkwork:",
1064
+ choices,
1065
+ loop: false,
1066
+ pageSize: Math.max(profiles.length + 2, 10)
1067
+ });
1068
+ return picked;
1069
+ } catch (err) {
1070
+ if (err instanceof Error && err.name === "ExitPromptError") {
1071
+ return { kind: "cancel" };
1072
+ }
1073
+ throw err;
1074
+ }
1066
1075
  }
1067
1076
  async function runKeyEntry(targetProfile) {
1068
1077
  console.log("");
@@ -1145,7 +1154,26 @@ function registerLoginCommand(program2) {
1145
1154
  "--profile <name>",
1146
1155
  "AWS profile name to configure (used when entering new keys or SSO)",
1147
1156
  "thinkwork"
1148
- ).option("--sso", "Skip the picker and go straight to SSO login").option("--keys", "Skip the picker and go straight to access-key entry").action(async (opts) => {
1157
+ ).option("--sso", "Skip the picker and go straight to SSO login").option("--keys", "Skip the picker and go straight to access-key entry").addHelpText(
1158
+ "after",
1159
+ `
1160
+ Examples:
1161
+ # Interactive picker \u2014 lists profiles from ~/.aws, verifies the one you pick,
1162
+ # and saves it as your Thinkwork default.
1163
+ $ thinkwork login
1164
+
1165
+ # Skip the picker, enter fresh access keys into a named profile
1166
+ $ thinkwork login --keys --profile thinkwork
1167
+
1168
+ # Skip the picker, log in via AWS SSO
1169
+ $ thinkwork login --sso --profile work-sso
1170
+
1171
+ After login, commands resolve the AWS profile in this order:
1172
+ 1. --profile <name> (per-command override)
1173
+ 2. $AWS_PROFILE env var
1174
+ 3. defaultProfile from ~/.thinkwork/config.json (set by this command)
1175
+ `
1176
+ ).action(async (opts) => {
1149
1177
  printHeader("login", opts.profile);
1150
1178
  const awsOk = await ensureAwsCli();
1151
1179
  if (!awsOk) process.exit(1);
@@ -1744,7 +1772,30 @@ function printStageDetail(info) {
1744
1772
  function registerStatusCommand(program2) {
1745
1773
  program2.command("status").alias("list").alias("ls").description(
1746
1774
  "Show all Thinkwork environments / deployments (AWS + local). Aliases: list, ls"
1747
- ).option("-s, --stage <name>", "Show details for a specific stage").option("--region <region>", "AWS region to scan", "us-east-1").action(async (opts) => {
1775
+ ).option("-s, --stage <name>", "Show details for a specific stage").option("--region <region>", "AWS region to scan", "us-east-1").addHelpText(
1776
+ "after",
1777
+ `
1778
+ Examples:
1779
+ # List every deployment in the current AWS account (us-east-1)
1780
+ $ thinkwork list
1781
+
1782
+ # Same thing, tighter verb
1783
+ $ thinkwork ls
1784
+
1785
+ # Deep-dive on one stage (same info but scoped)
1786
+ $ thinkwork list -s dev
1787
+
1788
+ # Scan a different region
1789
+ $ thinkwork list --region us-west-2
1790
+
1791
+ # Use a specific AWS profile for this call only
1792
+ $ thinkwork --profile work-sso list
1793
+
1794
+ Discovers stages by looking for \`thinkwork-<stage>-api-graphql-http\`
1795
+ Lambdas and fans out to API Gateway, AppSync, S3, RDS, ECS, CloudFront,
1796
+ and AgentCore for per-stage detail.
1797
+ `
1798
+ ).action(async (opts) => {
1748
1799
  const identity = getAwsIdentity();
1749
1800
  printHeader("status", opts.stage || "all", identity);
1750
1801
  if (!identity) {
@@ -1801,7 +1852,46 @@ import chalk8 from "chalk";
1801
1852
 
1802
1853
  // src/api-client.ts
1803
1854
  import { readFileSync as readFileSync5, existsSync as existsSync8 } from "fs";
1855
+ import { execSync as execSync8 } from "child_process";
1856
+
1857
+ // src/aws-discovery.ts
1804
1858
  import { execSync as execSync7 } from "child_process";
1859
+ function runAws2(cmd) {
1860
+ try {
1861
+ return execSync7(`aws ${cmd}`, {
1862
+ encoding: "utf-8",
1863
+ timeout: 15e3,
1864
+ stdio: ["pipe", "pipe", "pipe"]
1865
+ }).trim();
1866
+ } catch {
1867
+ return null;
1868
+ }
1869
+ }
1870
+ function listDeployedStages(region) {
1871
+ const raw = runAws2(
1872
+ `lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
1873
+ );
1874
+ if (!raw) return [];
1875
+ try {
1876
+ const functions = JSON.parse(raw);
1877
+ const stages = /* @__PURE__ */ new Set();
1878
+ for (const fn of functions) {
1879
+ const m = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
1880
+ if (m) stages.add(m[1]);
1881
+ }
1882
+ return [...stages].sort();
1883
+ } catch {
1884
+ return [];
1885
+ }
1886
+ }
1887
+ function getApiAuthSecretFromLambda(stage, region) {
1888
+ const raw = runAws2(
1889
+ `lambda get-function-configuration --function-name thinkwork-${stage}-api-tenants --region ${region} --query "Environment.Variables.API_AUTH_SECRET" --output text`
1890
+ );
1891
+ return raw && raw !== "None" ? raw : null;
1892
+ }
1893
+
1894
+ // src/api-client.ts
1805
1895
  function readTfVar2(tfvarsPath, key) {
1806
1896
  if (!existsSync8(tfvarsPath)) return null;
1807
1897
  const content = readFileSync5(tfvarsPath, "utf-8");
@@ -1820,7 +1910,7 @@ function resolveTfvarsPath2(stage) {
1820
1910
  }
1821
1911
  function getApiEndpoint(stage, region) {
1822
1912
  try {
1823
- const raw = execSync7(
1913
+ const raw = execSync8(
1824
1914
  `aws apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`,
1825
1915
  { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
1826
1916
  ).trim();
@@ -1858,18 +1948,22 @@ async function apiFetchRaw(apiUrl, authSecret, path2, options = {}, extraHeaders
1858
1948
  const body = await res.json().catch(() => ({}));
1859
1949
  return { ok: res.ok, status: res.status, body };
1860
1950
  }
1861
- function resolveApiConfig(stage) {
1951
+ function resolveApiConfig(stage, regionOverride) {
1862
1952
  const tfvarsPath = resolveTfvarsPath2(stage);
1863
- const authSecret = readTfVar2(tfvarsPath, "api_auth_secret");
1864
- if (!authSecret) {
1865
- printError(`Cannot read api_auth_secret from ${tfvarsPath}`);
1866
- return null;
1867
- }
1868
- const region = readTfVar2(tfvarsPath, "region") || "us-east-1";
1953
+ const tfAuthSecret = readTfVar2(tfvarsPath, "api_auth_secret");
1954
+ const tfRegion = readTfVar2(tfvarsPath, "region");
1955
+ const region = regionOverride || tfRegion || "us-east-1";
1869
1956
  const apiUrl = getApiEndpoint(stage, region);
1870
1957
  if (!apiUrl) {
1871
1958
  printError(
1872
- `Cannot discover API endpoint for stage "${stage}". Is the stack deployed?`
1959
+ `Cannot discover API endpoint for stage "${stage}" in ${region}. Is the stack deployed?`
1960
+ );
1961
+ return null;
1962
+ }
1963
+ const authSecret = tfAuthSecret ?? getApiAuthSecretFromLambda(stage, region);
1964
+ if (!authSecret) {
1965
+ printError(
1966
+ `Cannot read api_auth_secret. Tried terraform.tfvars at ${tfvarsPath} and the \`thinkwork-${stage}-api-tenants\` Lambda env. Deploy the stack or set --profile to a role with lambda:GetFunctionConfiguration.`
1873
1967
  );
1874
1968
  return null;
1875
1969
  }
@@ -2206,12 +2300,12 @@ function registerToolsCommand(program2) {
2206
2300
  }
2207
2301
 
2208
2302
  // src/commands/update.ts
2209
- import { execSync as execSync8 } from "child_process";
2303
+ import { execSync as execSync9 } from "child_process";
2210
2304
  import { realpathSync } from "fs";
2211
2305
  import chalk10 from "chalk";
2212
2306
  function getLatestVersion() {
2213
2307
  try {
2214
- return execSync8("npm view thinkwork-cli version", {
2308
+ return execSync9("npm view thinkwork-cli version", {
2215
2309
  encoding: "utf-8",
2216
2310
  timeout: 1e4,
2217
2311
  stdio: ["pipe", "pipe", "pipe"]
@@ -2222,7 +2316,7 @@ function getLatestVersion() {
2222
2316
  }
2223
2317
  function detectInstallMethod() {
2224
2318
  try {
2225
- const which = execSync8("which thinkwork", {
2319
+ const which = execSync9("which thinkwork", {
2226
2320
  encoding: "utf-8",
2227
2321
  timeout: 5e3,
2228
2322
  stdio: ["pipe", "pipe", "pipe"]
@@ -2282,7 +2376,7 @@ function registerUpdateCommand(program2) {
2282
2376
  console.log(chalk10.dim(` $ ${cmd}`));
2283
2377
  console.log("");
2284
2378
  try {
2285
- execSync8(cmd, { stdio: "inherit", timeout: 12e4 });
2379
+ execSync9(cmd, { stdio: "inherit", timeout: 12e4 });
2286
2380
  console.log("");
2287
2381
  console.log(chalk10.green(` \u2713 Upgraded to thinkwork-cli@${latest}`));
2288
2382
  } catch {
@@ -2296,6 +2390,7 @@ function registerUpdateCommand(program2) {
2296
2390
 
2297
2391
  // src/commands/user.ts
2298
2392
  import { spawn as spawn3 } from "child_process";
2393
+ import { input, select as select2 } from "@inquirer/prompts";
2299
2394
  function getTerraformOutput2(cwd, key) {
2300
2395
  return new Promise((resolve3, reject) => {
2301
2396
  const proc = spawn3("terraform", ["output", "-raw", key], {
@@ -2338,43 +2433,166 @@ function runAwsCognitoReset(userPoolId, username, region) {
2338
2433
  proc.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
2339
2434
  });
2340
2435
  }
2436
+ function isCancellation(err) {
2437
+ return err instanceof Error && err.name === "ExitPromptError";
2438
+ }
2439
+ function requireTty(label) {
2440
+ if (!process.stdin.isTTY) {
2441
+ printError(
2442
+ `${label} is required. Pass it as a flag or re-run in an interactive terminal.`
2443
+ );
2444
+ process.exit(1);
2445
+ }
2446
+ }
2447
+ async function promptEmail() {
2448
+ requireTty("Email");
2449
+ return await input({
2450
+ message: "Email address of the person to invite:",
2451
+ validate: (v) => v.trim().includes("@") ? true : "That doesn't look like an email."
2452
+ });
2453
+ }
2454
+ async function promptStage(region) {
2455
+ requireTty("Stage");
2456
+ const stages = listDeployedStages(region);
2457
+ if (stages.length === 0) {
2458
+ printError(
2459
+ `No Thinkwork deployments found in ${region}. Run \`thinkwork list\` or pass --region.`
2460
+ );
2461
+ process.exit(1);
2462
+ }
2463
+ if (stages.length === 1) {
2464
+ console.log(` Using the only deployed stage: ${stages[0]}`);
2465
+ return stages[0];
2466
+ }
2467
+ return await select2({
2468
+ message: "Which stage?",
2469
+ choices: stages.map((s) => ({ name: s, value: s })),
2470
+ loop: false
2471
+ });
2472
+ }
2473
+ async function promptTenant(apiUrl, authSecret) {
2474
+ requireTty("Tenant");
2475
+ const list = await apiFetch(apiUrl, authSecret, "/api/tenants");
2476
+ if (!list || list.length === 0) {
2477
+ printError(
2478
+ "No tenants exist in this stage yet. Create one in the admin UI first."
2479
+ );
2480
+ process.exit(1);
2481
+ }
2482
+ if (list.length === 1) {
2483
+ console.log(` Using the only tenant: ${list[0].name} (${list[0].slug})`);
2484
+ return list[0].slug;
2485
+ }
2486
+ return await select2({
2487
+ message: "Which tenant?",
2488
+ choices: list.map((t) => ({
2489
+ name: `${t.name} (slug: ${t.slug})`,
2490
+ value: t.slug
2491
+ })),
2492
+ loop: false
2493
+ });
2494
+ }
2495
+ async function promptOptionalName() {
2496
+ if (!process.stdin.isTTY) return void 0;
2497
+ const answer = await input({
2498
+ message: "Display name (optional, press Enter to skip):",
2499
+ default: ""
2500
+ });
2501
+ return answer.trim() || void 0;
2502
+ }
2503
+ async function promptRole() {
2504
+ if (!process.stdin.isTTY) return "member";
2505
+ return await select2({
2506
+ message: "Role:",
2507
+ choices: [
2508
+ { name: "member \u2014 regular access", value: "member" },
2509
+ { name: "admin \u2014 can manage members and settings", value: "admin" },
2510
+ { name: "owner \u2014 full control", value: "owner" }
2511
+ ],
2512
+ default: "member",
2513
+ loop: false
2514
+ });
2515
+ }
2341
2516
  function registerUserCommand(program2) {
2342
2517
  const user = program2.command("user").description("User-management utilities for a deployed Thinkwork stack");
2343
- user.command("invite <email>").description(
2344
- "Invite a teammate to a tenant. Creates the Cognito user (Cognito emails a temporary password) and adds them as a tenant member."
2345
- ).requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").option("--name <name>", "Display name for the invited user").option("--role <role>", "Tenant member role", "member").action(
2346
- async (email, opts) => {
2347
- const stageCheck = validateStage(opts.stage);
2348
- if (!stageCheck.valid) {
2349
- printError(stageCheck.error);
2350
- process.exit(1);
2351
- }
2352
- const trimmed = email.trim().toLowerCase();
2353
- if (!trimmed || !trimmed.includes("@")) {
2354
- printError(
2355
- `"${email}" doesn't look like an email address. Pass the user's sign-in email.`
2356
- );
2357
- process.exit(1);
2358
- }
2359
- const api = resolveApiConfig(opts.stage);
2360
- if (!api) process.exit(1);
2361
- printHeader("user invite", opts.stage);
2362
- console.log(` Tenant: ${opts.tenant}`);
2363
- console.log(` Email: ${trimmed}`);
2364
- if (opts.name) console.log(` Name: ${opts.name}`);
2365
- console.log(` Role: ${opts.role}`);
2366
- console.log("");
2518
+ user.command("invite [email]").description(
2519
+ "Invite a teammate to a tenant. Creates the Cognito user (Cognito emails a temporary password) and adds them as a tenant member. Prompts interactively for any missing fields."
2520
+ ).option("-s, --stage <name>", "Deployment stage (e.g. dev, prod)").option(
2521
+ "--tenant <slug>",
2522
+ "Tenant slug (the URL-safe tenant id, e.g. acme)"
2523
+ ).option("--name <name>", "Display name for the invited user").option(
2524
+ "--role <role>",
2525
+ 'Tenant member role: "member", "admin", or "owner"'
2526
+ ).option("--region <region>", "AWS region to scan", "us-east-1").addHelpText(
2527
+ "after",
2528
+ `
2529
+ Examples:
2530
+ # Fully interactive \u2014 prompts for email, stage, tenant, name, role.
2531
+ $ thinkwork user invite
2532
+
2533
+ # Scriptable (no prompts) \u2014 all fields via flags.
2534
+ $ thinkwork user invite alice@example.com --tenant acme -s dev
2535
+
2536
+ # Mix: pass the email, prompt for everything else.
2537
+ $ thinkwork user invite alice@example.com
2538
+
2539
+ # With display name and admin role.
2540
+ $ thinkwork user invite bob@example.com --tenant acme -s dev \\
2541
+ --name "Bob Smith" --role admin
2542
+
2543
+ What happens:
2544
+ 1. A Cognito user is created (or reused if the email already exists).
2545
+ 2. Cognito emails the user a temporary password.
2546
+ 3. The user is added to the tenant with the given role.
2547
+ 4. On first sign-in they're prompted to set a real password.
2548
+
2549
+ Re-inviting someone who's already a member is a no-op (no second email).
2550
+ Agents / scripts that pass all flags stay non-interactive.
2551
+ `
2552
+ ).action(
2553
+ async (emailArg, opts) => {
2367
2554
  try {
2555
+ let email = emailArg ?? "";
2556
+ if (!email) email = await promptEmail();
2557
+ email = email.trim().toLowerCase();
2558
+ if (!email.includes("@")) {
2559
+ printError(
2560
+ `"${emailArg}" doesn't look like an email address. Pass the user's sign-in email.`
2561
+ );
2562
+ process.exit(1);
2563
+ }
2564
+ let stage = opts.stage;
2565
+ if (!stage) stage = await promptStage(opts.region);
2566
+ const stageCheck = validateStage(stage);
2567
+ if (!stageCheck.valid) {
2568
+ printError(stageCheck.error);
2569
+ process.exit(1);
2570
+ }
2571
+ const api = resolveApiConfig(stage, opts.region);
2572
+ if (!api) process.exit(1);
2573
+ let tenant = opts.tenant;
2574
+ if (!tenant) tenant = await promptTenant(api.apiUrl, api.authSecret);
2575
+ let name = opts.name;
2576
+ if (name === void 0 && !emailArg) name = await promptOptionalName();
2577
+ let role = opts.role;
2578
+ if (!role && !emailArg) role = await promptRole();
2579
+ role = role || "member";
2580
+ printHeader("user invite", stage);
2581
+ console.log(` Tenant: ${tenant}`);
2582
+ console.log(` Email: ${email}`);
2583
+ if (name) console.log(` Name: ${name}`);
2584
+ console.log(` Role: ${role}`);
2585
+ console.log("");
2368
2586
  const result = await apiFetchRaw(
2369
2587
  api.apiUrl,
2370
2588
  api.authSecret,
2371
- `/api/tenants/${encodeURIComponent(opts.tenant)}/members`,
2589
+ `/api/tenants/${encodeURIComponent(tenant)}/invites`,
2372
2590
  {
2373
2591
  method: "POST",
2374
2592
  body: JSON.stringify({
2375
- email: trimmed,
2376
- name: opts.name ?? null,
2377
- role: opts.role
2593
+ email,
2594
+ name: name ?? null,
2595
+ role
2378
2596
  })
2379
2597
  }
2380
2598
  );
@@ -2385,14 +2603,19 @@ function registerUserCommand(program2) {
2385
2603
  }
2386
2604
  if (result.body.alreadyMember) {
2387
2605
  printWarning(
2388
- `${trimmed} is already a member of "${opts.tenant}" (role: ${result.body.role}). No email sent.`
2606
+ `${email} is already a member of "${tenant}" (role: ${result.body.role}). No email sent.`
2389
2607
  );
2390
2608
  return;
2391
2609
  }
2392
2610
  printSuccess(
2393
- `Invited ${trimmed} to "${opts.tenant}" (role: ${result.body.role}). Cognito has emailed a temporary password; the user sets a new password on first sign-in.`
2611
+ `Invited ${email} to "${tenant}" (role: ${result.body.role}). Cognito has emailed a temporary password; the user sets a new password on first sign-in.`
2394
2612
  );
2395
2613
  } catch (err) {
2614
+ if (isCancellation(err)) {
2615
+ console.log("");
2616
+ console.log(" Cancelled.");
2617
+ return;
2618
+ }
2396
2619
  printError(
2397
2620
  `Invite failed: ${err instanceof Error ? err.message : String(err)}`
2398
2621
  );
@@ -2402,9 +2625,24 @@ function registerUserCommand(program2) {
2402
2625
  );
2403
2626
  user.command("reset-password <email>").description(
2404
2627
  "Trigger Cognito's forgot-password flow for a user (admin-initiated). Sends them a verification code email."
2405
- ).option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").option(
2628
+ ).option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage (e.g. dev, prod)").option(
2406
2629
  "-r, --region <name>",
2407
2630
  "AWS region (defaults to AWS CLI default / AWS_REGION)"
2631
+ ).addHelpText(
2632
+ "after",
2633
+ `
2634
+ Examples:
2635
+ # Admin-triggered password reset \u2014 works even if the account is locked
2636
+ $ thinkwork user reset-password alice@example.com -s dev
2637
+
2638
+ # Target a specific AWS profile + region
2639
+ $ thinkwork user reset-password alice@example.com -s prod \\
2640
+ --profile thinkwork --region us-east-1
2641
+
2642
+ Cognito emails the user a verification code; they set a new password on
2643
+ next sign-in. Use this instead of \`forgot-password\` when the user is in
2644
+ FORCE_CHANGE_PASSWORD or has been disabled.
2645
+ `
2408
2646
  ).action(
2409
2647
  async (email, opts) => {
2410
2648
  const stageCheck = validateStage(opts.stage);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkwork-cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Thinkwork CLI — deploy, manage, and interact with your Thinkwork stack",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,6 +19,7 @@
19
19
  "prepublishOnly": "npm run build && npm run typecheck"
20
20
  },
21
21
  "dependencies": {
22
+ "@inquirer/prompts": "^8.4.1",
22
23
  "chalk": "^5.6.2",
23
24
  "commander": "^12.0.0",
24
25
  "ora": "^9.3.0"