thinkwork-cli 0.6.2 → 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 +197 -56
- package/package.json +1 -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();
|
|
@@ -1852,7 +1852,46 @@ import chalk8 from "chalk";
|
|
|
1852
1852
|
|
|
1853
1853
|
// src/api-client.ts
|
|
1854
1854
|
import { readFileSync as readFileSync5, existsSync as existsSync8 } from "fs";
|
|
1855
|
+
import { execSync as execSync8 } from "child_process";
|
|
1856
|
+
|
|
1857
|
+
// src/aws-discovery.ts
|
|
1855
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
|
|
1856
1895
|
function readTfVar2(tfvarsPath, key) {
|
|
1857
1896
|
if (!existsSync8(tfvarsPath)) return null;
|
|
1858
1897
|
const content = readFileSync5(tfvarsPath, "utf-8");
|
|
@@ -1871,7 +1910,7 @@ function resolveTfvarsPath2(stage) {
|
|
|
1871
1910
|
}
|
|
1872
1911
|
function getApiEndpoint(stage, region) {
|
|
1873
1912
|
try {
|
|
1874
|
-
const raw =
|
|
1913
|
+
const raw = execSync8(
|
|
1875
1914
|
`aws apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`,
|
|
1876
1915
|
{ encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
|
|
1877
1916
|
).trim();
|
|
@@ -1909,18 +1948,22 @@ async function apiFetchRaw(apiUrl, authSecret, path2, options = {}, extraHeaders
|
|
|
1909
1948
|
const body = await res.json().catch(() => ({}));
|
|
1910
1949
|
return { ok: res.ok, status: res.status, body };
|
|
1911
1950
|
}
|
|
1912
|
-
function resolveApiConfig(stage) {
|
|
1951
|
+
function resolveApiConfig(stage, regionOverride) {
|
|
1913
1952
|
const tfvarsPath = resolveTfvarsPath2(stage);
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
return null;
|
|
1918
|
-
}
|
|
1919
|
-
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";
|
|
1920
1956
|
const apiUrl = getApiEndpoint(stage, region);
|
|
1921
1957
|
if (!apiUrl) {
|
|
1922
1958
|
printError(
|
|
1923
|
-
`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.`
|
|
1924
1967
|
);
|
|
1925
1968
|
return null;
|
|
1926
1969
|
}
|
|
@@ -2257,12 +2300,12 @@ function registerToolsCommand(program2) {
|
|
|
2257
2300
|
}
|
|
2258
2301
|
|
|
2259
2302
|
// src/commands/update.ts
|
|
2260
|
-
import { execSync as
|
|
2303
|
+
import { execSync as execSync9 } from "child_process";
|
|
2261
2304
|
import { realpathSync } from "fs";
|
|
2262
2305
|
import chalk10 from "chalk";
|
|
2263
2306
|
function getLatestVersion() {
|
|
2264
2307
|
try {
|
|
2265
|
-
return
|
|
2308
|
+
return execSync9("npm view thinkwork-cli version", {
|
|
2266
2309
|
encoding: "utf-8",
|
|
2267
2310
|
timeout: 1e4,
|
|
2268
2311
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2273,7 +2316,7 @@ function getLatestVersion() {
|
|
|
2273
2316
|
}
|
|
2274
2317
|
function detectInstallMethod() {
|
|
2275
2318
|
try {
|
|
2276
|
-
const which =
|
|
2319
|
+
const which = execSync9("which thinkwork", {
|
|
2277
2320
|
encoding: "utf-8",
|
|
2278
2321
|
timeout: 5e3,
|
|
2279
2322
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2333,7 +2376,7 @@ function registerUpdateCommand(program2) {
|
|
|
2333
2376
|
console.log(chalk10.dim(` $ ${cmd}`));
|
|
2334
2377
|
console.log("");
|
|
2335
2378
|
try {
|
|
2336
|
-
|
|
2379
|
+
execSync9(cmd, { stdio: "inherit", timeout: 12e4 });
|
|
2337
2380
|
console.log("");
|
|
2338
2381
|
console.log(chalk10.green(` \u2713 Upgraded to thinkwork-cli@${latest}`));
|
|
2339
2382
|
} catch {
|
|
@@ -2347,6 +2390,7 @@ function registerUpdateCommand(program2) {
|
|
|
2347
2390
|
|
|
2348
2391
|
// src/commands/user.ts
|
|
2349
2392
|
import { spawn as spawn3 } from "child_process";
|
|
2393
|
+
import { input, select as select2 } from "@inquirer/prompts";
|
|
2350
2394
|
function getTerraformOutput2(cwd, key) {
|
|
2351
2395
|
return new Promise((resolve3, reject) => {
|
|
2352
2396
|
const proc = spawn3("terraform", ["output", "-raw", key], {
|
|
@@ -2389,74 +2433,166 @@ function runAwsCognitoReset(userPoolId, username, region) {
|
|
|
2389
2433
|
proc.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
|
|
2390
2434
|
});
|
|
2391
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
|
+
}
|
|
2392
2516
|
function registerUserCommand(program2) {
|
|
2393
2517
|
const user = program2.command("user").description("User-management utilities for a deployed Thinkwork stack");
|
|
2394
|
-
user.command("invite
|
|
2395
|
-
"Invite a teammate to a tenant. Creates the Cognito user (Cognito emails a temporary password) and adds them as a tenant member."
|
|
2396
|
-
).
|
|
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(
|
|
2397
2521
|
"--tenant <slug>",
|
|
2398
2522
|
"Tenant slug (the URL-safe tenant id, e.g. acme)"
|
|
2399
2523
|
).option("--name <name>", "Display name for the invited user").option(
|
|
2400
2524
|
"--role <role>",
|
|
2401
|
-
'Tenant member role: "member", "admin", or "owner"'
|
|
2402
|
-
|
|
2403
|
-
).addHelpText(
|
|
2525
|
+
'Tenant member role: "member", "admin", or "owner"'
|
|
2526
|
+
).option("--region <region>", "AWS region to scan", "us-east-1").addHelpText(
|
|
2404
2527
|
"after",
|
|
2405
2528
|
`
|
|
2406
2529
|
Examples:
|
|
2407
|
-
#
|
|
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.
|
|
2408
2534
|
$ thinkwork user invite alice@example.com --tenant acme -s dev
|
|
2409
2535
|
|
|
2410
|
-
#
|
|
2536
|
+
# Mix: pass the email, prompt for everything else.
|
|
2537
|
+
$ thinkwork user invite alice@example.com
|
|
2538
|
+
|
|
2539
|
+
# With display name and admin role.
|
|
2411
2540
|
$ thinkwork user invite bob@example.com --tenant acme -s dev \\
|
|
2412
2541
|
--name "Bob Smith" --role admin
|
|
2413
2542
|
|
|
2414
|
-
# Re-inviting someone who's already a member is a no-op (no second email)
|
|
2415
|
-
$ thinkwork user invite alice@example.com --tenant acme -s dev
|
|
2416
|
-
\u26A0 alice@example.com is already a member of "acme" (role: member). No email sent.
|
|
2417
|
-
|
|
2418
2543
|
What happens:
|
|
2419
2544
|
1. A Cognito user is created (or reused if the email already exists).
|
|
2420
2545
|
2. Cognito emails the user a temporary password.
|
|
2421
2546
|
3. The user is added to the tenant with the given role.
|
|
2422
2547
|
4. On first sign-in they're prompted to set a real password.
|
|
2423
2548
|
|
|
2424
|
-
|
|
2425
|
-
|
|
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.
|
|
2426
2551
|
`
|
|
2427
2552
|
).action(
|
|
2428
|
-
async (
|
|
2429
|
-
const stageCheck = validateStage(opts.stage);
|
|
2430
|
-
if (!stageCheck.valid) {
|
|
2431
|
-
printError(stageCheck.error);
|
|
2432
|
-
process.exit(1);
|
|
2433
|
-
}
|
|
2434
|
-
const trimmed = email.trim().toLowerCase();
|
|
2435
|
-
if (!trimmed || !trimmed.includes("@")) {
|
|
2436
|
-
printError(
|
|
2437
|
-
`"${email}" doesn't look like an email address. Pass the user's sign-in email.`
|
|
2438
|
-
);
|
|
2439
|
-
process.exit(1);
|
|
2440
|
-
}
|
|
2441
|
-
const api = resolveApiConfig(opts.stage);
|
|
2442
|
-
if (!api) process.exit(1);
|
|
2443
|
-
printHeader("user invite", opts.stage);
|
|
2444
|
-
console.log(` Tenant: ${opts.tenant}`);
|
|
2445
|
-
console.log(` Email: ${trimmed}`);
|
|
2446
|
-
if (opts.name) console.log(` Name: ${opts.name}`);
|
|
2447
|
-
console.log(` Role: ${opts.role}`);
|
|
2448
|
-
console.log("");
|
|
2553
|
+
async (emailArg, opts) => {
|
|
2449
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("");
|
|
2450
2586
|
const result = await apiFetchRaw(
|
|
2451
2587
|
api.apiUrl,
|
|
2452
2588
|
api.authSecret,
|
|
2453
|
-
`/api/tenants/${encodeURIComponent(
|
|
2589
|
+
`/api/tenants/${encodeURIComponent(tenant)}/invites`,
|
|
2454
2590
|
{
|
|
2455
2591
|
method: "POST",
|
|
2456
2592
|
body: JSON.stringify({
|
|
2457
|
-
email
|
|
2458
|
-
name:
|
|
2459
|
-
role
|
|
2593
|
+
email,
|
|
2594
|
+
name: name ?? null,
|
|
2595
|
+
role
|
|
2460
2596
|
})
|
|
2461
2597
|
}
|
|
2462
2598
|
);
|
|
@@ -2467,14 +2603,19 @@ and reads api_auth_secret from terraform.tfvars for the stage).
|
|
|
2467
2603
|
}
|
|
2468
2604
|
if (result.body.alreadyMember) {
|
|
2469
2605
|
printWarning(
|
|
2470
|
-
`${
|
|
2606
|
+
`${email} is already a member of "${tenant}" (role: ${result.body.role}). No email sent.`
|
|
2471
2607
|
);
|
|
2472
2608
|
return;
|
|
2473
2609
|
}
|
|
2474
2610
|
printSuccess(
|
|
2475
|
-
`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.`
|
|
2476
2612
|
);
|
|
2477
2613
|
} catch (err) {
|
|
2614
|
+
if (isCancellation(err)) {
|
|
2615
|
+
console.log("");
|
|
2616
|
+
console.log(" Cancelled.");
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2478
2619
|
printError(
|
|
2479
2620
|
`Invite failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2480
2621
|
);
|