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.
- package/dist/cli.js +310 -72
- 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:
|
|
780
|
-
const secretJson =
|
|
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
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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").
|
|
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").
|
|
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 =
|
|
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
|
|
1864
|
-
|
|
1865
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2344
|
-
"Invite a teammate to a tenant. Creates the Cognito user (Cognito emails a temporary password) and adds them as a tenant member."
|
|
2345
|
-
).
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
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(
|
|
2589
|
+
`/api/tenants/${encodeURIComponent(tenant)}/invites`,
|
|
2372
2590
|
{
|
|
2373
2591
|
method: "POST",
|
|
2374
2592
|
body: JSON.stringify({
|
|
2375
|
-
email
|
|
2376
|
-
name:
|
|
2377
|
-
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
|
-
`${
|
|
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 ${
|
|
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.
|
|
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"
|