thinkwork-cli 0.5.4 → 0.6.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/README.md +4 -0
- package/dist/cli.js +645 -287
- package/dist/terraform/examples/greenfield/main.tf +156 -2
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +10 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +33 -0
- package/dist/terraform/modules/app/job-triggers/main.tf +21 -0
- package/dist/terraform/modules/app/lambda-api/.build/placeholder.zip +0 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +66 -16
- package/dist/terraform/modules/app/lambda-api/main.tf +120 -2
- package/dist/terraform/modules/app/lambda-api/outputs.tf +20 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +22 -0
- package/dist/terraform/modules/app/ses-email/main.tf +173 -10
- package/dist/terraform/modules/app/static-site/main.tf +37 -14
- package/dist/terraform/modules/app/www-dns/README.md +39 -0
- package/dist/terraform/modules/app/www-dns/main.tf +245 -0
- package/dist/terraform/modules/app/www-dns/outputs.tf +14 -0
- package/dist/terraform/modules/app/www-dns/variables.tf +43 -0
- package/dist/terraform/modules/thinkwork/main.tf +52 -9
- package/dist/terraform/modules/thinkwork/outputs.tf +32 -0
- package/dist/terraform/modules/thinkwork/variables.tf +57 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,30 @@ var require2 = createRequire(import.meta.url);
|
|
|
9
9
|
var pkg = require2("../package.json");
|
|
10
10
|
var VERSION = pkg.version;
|
|
11
11
|
|
|
12
|
+
// src/cli-config.ts
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { dirname, join } from "path";
|
|
16
|
+
function getCliConfigPath(override) {
|
|
17
|
+
return override ?? join(homedir(), ".thinkwork", "config.json");
|
|
18
|
+
}
|
|
19
|
+
function loadCliConfig(pathOverride) {
|
|
20
|
+
const path2 = getCliConfigPath(pathOverride);
|
|
21
|
+
if (!existsSync(path2)) return {};
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(path2, "utf-8"));
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function saveCliConfig(next, pathOverride) {
|
|
29
|
+
const path2 = getCliConfigPath(pathOverride);
|
|
30
|
+
const dir = dirname(path2);
|
|
31
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
32
|
+
const merged = { ...loadCliConfig(pathOverride), ...next };
|
|
33
|
+
writeFileSync(path2, JSON.stringify(merged, null, 2) + "\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
// src/config.ts
|
|
13
37
|
var VALID_COMPONENTS = ["foundation", "data", "app", "all"];
|
|
14
38
|
var PROD_LIKE_STAGES = ["main", "prod", "production", "staging"];
|
|
@@ -76,23 +100,23 @@ function getAwsIdentity() {
|
|
|
76
100
|
|
|
77
101
|
// src/terraform.ts
|
|
78
102
|
import { spawn } from "child_process";
|
|
79
|
-
import { existsSync } from "fs";
|
|
103
|
+
import { existsSync as existsSync2 } from "fs";
|
|
80
104
|
import path from "path";
|
|
81
105
|
function resolveTierDir(terraformDir, stage, tier) {
|
|
82
106
|
const envDir = path.join(terraformDir, "environments", stage, tier);
|
|
83
|
-
if (
|
|
107
|
+
if (existsSync2(envDir)) {
|
|
84
108
|
return envDir;
|
|
85
109
|
}
|
|
86
110
|
const greenfield = path.join(terraformDir, "examples", "greenfield");
|
|
87
|
-
if (
|
|
111
|
+
if (existsSync2(greenfield)) {
|
|
88
112
|
return greenfield;
|
|
89
113
|
}
|
|
90
114
|
const flat = path.join(terraformDir);
|
|
91
|
-
if (
|
|
115
|
+
if (existsSync2(path.join(flat, "main.tf"))) {
|
|
92
116
|
return flat;
|
|
93
117
|
}
|
|
94
118
|
const cwdTf = path.join(process.cwd(), "terraform");
|
|
95
|
-
if (
|
|
119
|
+
if (existsSync2(path.join(cwdTf, "main.tf"))) {
|
|
96
120
|
return cwdTf;
|
|
97
121
|
}
|
|
98
122
|
return terraformDir;
|
|
@@ -136,7 +160,7 @@ function runTerraform(cwd, args) {
|
|
|
136
160
|
}
|
|
137
161
|
async function ensureInit(cwd) {
|
|
138
162
|
const dotTerraform = path.join(cwd, ".terraform");
|
|
139
|
-
if (!
|
|
163
|
+
if (!existsSync2(dotTerraform)) {
|
|
140
164
|
const code = await runTerraform(cwd, ["init"]);
|
|
141
165
|
if (code !== 0) {
|
|
142
166
|
throw new Error("terraform init failed");
|
|
@@ -485,58 +509,58 @@ function registerOutputsCommand(program2) {
|
|
|
485
509
|
}
|
|
486
510
|
|
|
487
511
|
// src/commands/config.ts
|
|
488
|
-
import { readFileSync as
|
|
512
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
489
513
|
import chalk3 from "chalk";
|
|
490
514
|
|
|
491
515
|
// src/environments.ts
|
|
492
|
-
import { existsSync as
|
|
493
|
-
import { join } from "path";
|
|
494
|
-
import { homedir } from "os";
|
|
495
|
-
var THINKWORK_HOME =
|
|
496
|
-
var ENVIRONMENTS_DIR =
|
|
516
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
517
|
+
import { join as join2 } from "path";
|
|
518
|
+
import { homedir as homedir2 } from "os";
|
|
519
|
+
var THINKWORK_HOME = join2(homedir2(), ".thinkwork");
|
|
520
|
+
var ENVIRONMENTS_DIR = join2(THINKWORK_HOME, "environments");
|
|
497
521
|
function ensureDir(dir) {
|
|
498
|
-
if (!
|
|
522
|
+
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
|
|
499
523
|
}
|
|
500
524
|
function saveEnvironment(config) {
|
|
501
525
|
ensureDir(ENVIRONMENTS_DIR);
|
|
502
|
-
const envDir =
|
|
526
|
+
const envDir = join2(ENVIRONMENTS_DIR, config.stage);
|
|
503
527
|
ensureDir(envDir);
|
|
504
|
-
|
|
505
|
-
|
|
528
|
+
writeFileSync2(
|
|
529
|
+
join2(envDir, "config.json"),
|
|
506
530
|
JSON.stringify(config, null, 2) + "\n"
|
|
507
531
|
);
|
|
508
532
|
}
|
|
509
533
|
function loadEnvironment(stage) {
|
|
510
|
-
const configPath =
|
|
511
|
-
if (!
|
|
512
|
-
return JSON.parse(
|
|
534
|
+
const configPath = join2(ENVIRONMENTS_DIR, stage, "config.json");
|
|
535
|
+
if (!existsSync3(configPath)) return null;
|
|
536
|
+
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
513
537
|
}
|
|
514
538
|
function listEnvironments() {
|
|
515
|
-
if (!
|
|
539
|
+
if (!existsSync3(ENVIRONMENTS_DIR)) return [];
|
|
516
540
|
return readdirSync(ENVIRONMENTS_DIR).filter((name) => {
|
|
517
|
-
return
|
|
541
|
+
return existsSync3(join2(ENVIRONMENTS_DIR, name, "config.json"));
|
|
518
542
|
}).map((name) => {
|
|
519
543
|
return JSON.parse(
|
|
520
|
-
|
|
544
|
+
readFileSync2(join2(ENVIRONMENTS_DIR, name, "config.json"), "utf-8")
|
|
521
545
|
);
|
|
522
546
|
}).sort((a, b) => a.stage.localeCompare(b.stage));
|
|
523
547
|
}
|
|
524
548
|
function resolveTerraformDir(stage) {
|
|
525
549
|
const env = loadEnvironment(stage);
|
|
526
|
-
if (env?.terraformDir &&
|
|
550
|
+
if (env?.terraformDir && existsSync3(env.terraformDir)) {
|
|
527
551
|
return env.terraformDir;
|
|
528
552
|
}
|
|
529
553
|
const envVar = process.env.THINKWORK_TERRAFORM_DIR;
|
|
530
|
-
if (envVar &&
|
|
531
|
-
const cwdTf =
|
|
532
|
-
if (
|
|
554
|
+
if (envVar && existsSync3(envVar)) return envVar;
|
|
555
|
+
const cwdTf = join2(process.cwd(), "terraform");
|
|
556
|
+
if (existsSync3(join2(cwdTf, "main.tf"))) return cwdTf;
|
|
533
557
|
return null;
|
|
534
558
|
}
|
|
535
559
|
|
|
536
560
|
// src/commands/config.ts
|
|
537
561
|
function readTfVar(tfvarsPath, key) {
|
|
538
|
-
if (!
|
|
539
|
-
const content =
|
|
562
|
+
if (!existsSync4(tfvarsPath)) return null;
|
|
563
|
+
const content = readFileSync3(tfvarsPath, "utf-8");
|
|
540
564
|
const quoted = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
|
|
541
565
|
if (quoted) return quoted[1];
|
|
542
566
|
const bare = content.match(new RegExp(`^${key}\\s*=\\s*([^\\s#]+)`, "m"));
|
|
@@ -544,10 +568,10 @@ function readTfVar(tfvarsPath, key) {
|
|
|
544
568
|
}
|
|
545
569
|
var BARE_KEYS = /* @__PURE__ */ new Set(["enable_hindsight"]);
|
|
546
570
|
function setTfVar(tfvarsPath, key, value) {
|
|
547
|
-
if (!
|
|
571
|
+
if (!existsSync4(tfvarsPath)) {
|
|
548
572
|
throw new Error(`terraform.tfvars not found at ${tfvarsPath}`);
|
|
549
573
|
}
|
|
550
|
-
let content =
|
|
574
|
+
let content = readFileSync3(tfvarsPath, "utf-8");
|
|
551
575
|
const bare = BARE_KEYS.has(key);
|
|
552
576
|
const newLine = bare ? `${key} = ${value}` : `${key} = "${value}"`;
|
|
553
577
|
const existingRegex = new RegExp(`^${key}\\s*=\\s*(?:"[^"]*"|[^\\s#]+)`, "m");
|
|
@@ -558,13 +582,13 @@ function setTfVar(tfvarsPath, key, value) {
|
|
|
558
582
|
${newLine}
|
|
559
583
|
`;
|
|
560
584
|
}
|
|
561
|
-
|
|
585
|
+
writeFileSync3(tfvarsPath, content);
|
|
562
586
|
}
|
|
563
587
|
function resolveTfvarsPath(stage) {
|
|
564
588
|
const tfDir = resolveTerraformDir(stage);
|
|
565
589
|
if (tfDir) {
|
|
566
590
|
const direct = `${tfDir}/terraform.tfvars`;
|
|
567
|
-
if (
|
|
591
|
+
if (existsSync4(direct)) return direct;
|
|
568
592
|
}
|
|
569
593
|
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
570
594
|
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
@@ -591,10 +615,10 @@ function registerConfigCommand(program2) {
|
|
|
591
615
|
console.log(` ${chalk3.bold("Updated:")} ${env.updatedAt}`);
|
|
592
616
|
console.log(chalk3.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"));
|
|
593
617
|
const tfvarsPath = `${env.terraformDir}/terraform.tfvars`;
|
|
594
|
-
if (
|
|
618
|
+
if (existsSync4(tfvarsPath)) {
|
|
595
619
|
console.log("");
|
|
596
620
|
console.log(chalk3.dim(" terraform.tfvars:"));
|
|
597
|
-
const content =
|
|
621
|
+
const content = readFileSync3(tfvarsPath, "utf-8");
|
|
598
622
|
for (const line of content.split("\n")) {
|
|
599
623
|
if (line.trim() && !line.trim().startsWith("#")) {
|
|
600
624
|
const masked = line.replace(
|
|
@@ -752,8 +776,8 @@ function registerBootstrapCommand(program2) {
|
|
|
752
776
|
bucket = await getTerraformOutput(cwd, "bucket_name");
|
|
753
777
|
dbEndpoint = await getTerraformOutput(cwd, "db_cluster_endpoint");
|
|
754
778
|
const secretArn = await getTerraformOutput(cwd, "db_secret_arn");
|
|
755
|
-
const { execSync:
|
|
756
|
-
const secretJson =
|
|
779
|
+
const { execSync: execSync9 } = await import("child_process");
|
|
780
|
+
const secretJson = execSync9(
|
|
757
781
|
`aws secretsmanager get-secret-value --secret-id "${secretArn}" --query SecretString --output text`,
|
|
758
782
|
{ encoding: "utf-8" }
|
|
759
783
|
).trim();
|
|
@@ -778,12 +802,89 @@ function registerBootstrapCommand(program2) {
|
|
|
778
802
|
// src/commands/login.ts
|
|
779
803
|
import { execSync as execSync4 } from "child_process";
|
|
780
804
|
import { createInterface as createInterface2 } from "readline";
|
|
805
|
+
import chalk5 from "chalk";
|
|
806
|
+
|
|
807
|
+
// src/aws-profiles.ts
|
|
808
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
809
|
+
import { homedir as homedir3 } from "os";
|
|
810
|
+
import { join as join3 } from "path";
|
|
811
|
+
var CREDENTIALS_PATH = join3(homedir3(), ".aws", "credentials");
|
|
812
|
+
var CONFIG_PATH = join3(homedir3(), ".aws", "config");
|
|
813
|
+
function parseIni(content) {
|
|
814
|
+
const sections = {};
|
|
815
|
+
let current = null;
|
|
816
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
817
|
+
const line = rawLine.trim();
|
|
818
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
819
|
+
const header = line.match(/^\[(.+)\]$/);
|
|
820
|
+
if (header) {
|
|
821
|
+
current = header[1].trim();
|
|
822
|
+
sections[current] ??= {};
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (!current) continue;
|
|
826
|
+
const eq = line.indexOf("=");
|
|
827
|
+
if (eq === -1) continue;
|
|
828
|
+
const key = line.slice(0, eq).trim();
|
|
829
|
+
const value = line.slice(eq + 1).trim();
|
|
830
|
+
sections[current][key] = value;
|
|
831
|
+
}
|
|
832
|
+
return sections;
|
|
833
|
+
}
|
|
834
|
+
function normalizeConfigSection(section) {
|
|
835
|
+
if (section === "default") return "default";
|
|
836
|
+
if (section.startsWith("profile ")) return section.slice("profile ".length).trim();
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
function classify(fields) {
|
|
840
|
+
if (fields.aws_access_key_id) return "keys";
|
|
841
|
+
if (fields.sso_start_url || fields.sso_session || fields.sso_account_id || fields.sso_role_name) {
|
|
842
|
+
return "sso";
|
|
843
|
+
}
|
|
844
|
+
if (fields.role_arn || fields.source_profile || fields.credential_source) {
|
|
845
|
+
return "role";
|
|
846
|
+
}
|
|
847
|
+
return "other";
|
|
848
|
+
}
|
|
849
|
+
function listAwsProfiles() {
|
|
850
|
+
const byName = /* @__PURE__ */ new Map();
|
|
851
|
+
if (existsSync5(CREDENTIALS_PATH)) {
|
|
852
|
+
const sections = parseIni(readFileSync4(CREDENTIALS_PATH, "utf-8"));
|
|
853
|
+
for (const [section, fields] of Object.entries(sections)) {
|
|
854
|
+
byName.set(section, {
|
|
855
|
+
name: section,
|
|
856
|
+
source: "credentials",
|
|
857
|
+
type: classify(fields)
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (existsSync5(CONFIG_PATH)) {
|
|
862
|
+
const sections = parseIni(readFileSync4(CONFIG_PATH, "utf-8"));
|
|
863
|
+
for (const [section, fields] of Object.entries(sections)) {
|
|
864
|
+
const name = normalizeConfigSection(section);
|
|
865
|
+
if (!name) continue;
|
|
866
|
+
const existing = byName.get(name);
|
|
867
|
+
const type = classify(fields);
|
|
868
|
+
if (existing) {
|
|
869
|
+
byName.set(name, {
|
|
870
|
+
...existing,
|
|
871
|
+
source: "both",
|
|
872
|
+
// Prefer the more specific type if one side says "other".
|
|
873
|
+
type: existing.type === "other" ? type : existing.type
|
|
874
|
+
});
|
|
875
|
+
} else {
|
|
876
|
+
byName.set(name, { name, source: "config", type });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
881
|
+
}
|
|
781
882
|
|
|
782
883
|
// src/prerequisites.ts
|
|
783
884
|
import { execSync as execSync3 } from "child_process";
|
|
784
|
-
import { mkdirSync as
|
|
785
|
-
import { join as
|
|
786
|
-
import { homedir as
|
|
885
|
+
import { mkdirSync as mkdirSync3, createWriteStream, chmodSync } from "fs";
|
|
886
|
+
import { join as join4 } from "path";
|
|
887
|
+
import { homedir as homedir4, platform, arch } from "os";
|
|
787
888
|
import chalk4 from "chalk";
|
|
788
889
|
function run(cmd, opts) {
|
|
789
890
|
try {
|
|
@@ -815,14 +916,14 @@ async function ensureAwsCli() {
|
|
|
815
916
|
}
|
|
816
917
|
if (os === "linux") {
|
|
817
918
|
try {
|
|
818
|
-
const tmpDir =
|
|
819
|
-
|
|
820
|
-
const zipPath =
|
|
919
|
+
const tmpDir = join4(homedir4(), ".thinkwork", "tmp");
|
|
920
|
+
mkdirSync3(tmpDir, { recursive: true });
|
|
921
|
+
const zipPath = join4(tmpDir, "awscliv2.zip");
|
|
821
922
|
console.log(" Downloading AWS CLI...");
|
|
822
923
|
run(`curl -sL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "${zipPath}"`);
|
|
823
924
|
run(`cd "${tmpDir}" && unzip -qo "${zipPath}"`);
|
|
824
|
-
run(`"${tmpDir}/aws/install" --install-dir "${
|
|
825
|
-
process.env.PATH = `${
|
|
925
|
+
run(`"${tmpDir}/aws/install" --install-dir "${homedir4()}/.thinkwork/aws-cli" --bin-dir "${homedir4()}/.local/bin" --update`);
|
|
926
|
+
process.env.PATH = `${homedir4()}/.local/bin:${process.env.PATH}`;
|
|
826
927
|
if (isInstalled("aws")) {
|
|
827
928
|
console.log(` ${chalk4.green("\u2713")} AWS CLI installed to ~/.local/bin/aws`);
|
|
828
929
|
return true;
|
|
@@ -832,9 +933,9 @@ async function ensureAwsCli() {
|
|
|
832
933
|
}
|
|
833
934
|
if (os === "darwin") {
|
|
834
935
|
try {
|
|
835
|
-
const tmpDir =
|
|
836
|
-
|
|
837
|
-
const pkgPath =
|
|
936
|
+
const tmpDir = join4(homedir4(), ".thinkwork", "tmp");
|
|
937
|
+
mkdirSync3(tmpDir, { recursive: true });
|
|
938
|
+
const pkgPath = join4(tmpDir, "AWSCLIV2.pkg");
|
|
838
939
|
console.log(" Downloading AWS CLI...");
|
|
839
940
|
run(`curl -sL "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "${pkgPath}"`);
|
|
840
941
|
run(`installer -pkg "${pkgPath}" -target CurrentUserHomeDirectory 2>/dev/null || sudo installer -pkg "${pkgPath}" -target /`);
|
|
@@ -865,15 +966,15 @@ async function ensureTerraform() {
|
|
|
865
966
|
const archName = arch() === "arm64" ? "arm64" : "amd64";
|
|
866
967
|
const url = `https://releases.hashicorp.com/terraform/${tfVersion}/terraform_${tfVersion}_${osName}_${archName}.zip`;
|
|
867
968
|
try {
|
|
868
|
-
const tmpDir =
|
|
869
|
-
const binDir =
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
const zipPath =
|
|
969
|
+
const tmpDir = join4(homedir4(), ".thinkwork", "tmp");
|
|
970
|
+
const binDir = join4(homedir4(), ".local", "bin");
|
|
971
|
+
mkdirSync3(tmpDir, { recursive: true });
|
|
972
|
+
mkdirSync3(binDir, { recursive: true });
|
|
973
|
+
const zipPath = join4(tmpDir, "terraform.zip");
|
|
873
974
|
console.log(` Downloading Terraform ${tfVersion}...`);
|
|
874
975
|
run(`curl -sL "${url}" -o "${zipPath}"`);
|
|
875
976
|
run(`unzip -qo "${zipPath}" -d "${binDir}"`);
|
|
876
|
-
chmodSync(
|
|
977
|
+
chmodSync(join4(binDir, "terraform"), 493);
|
|
877
978
|
if (!process.env.PATH?.includes(binDir)) {
|
|
878
979
|
process.env.PATH = `${binDir}:${process.env.PATH}`;
|
|
879
980
|
}
|
|
@@ -910,93 +1011,212 @@ function ask(prompt2) {
|
|
|
910
1011
|
});
|
|
911
1012
|
});
|
|
912
1013
|
}
|
|
1014
|
+
function verifyProfile(profile) {
|
|
1015
|
+
try {
|
|
1016
|
+
const raw = execSync4(
|
|
1017
|
+
`aws sts get-caller-identity --profile ${profile} --output json`,
|
|
1018
|
+
{ encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
|
|
1019
|
+
);
|
|
1020
|
+
const parsed = JSON.parse(raw);
|
|
1021
|
+
return { account: parsed.Account, arn: parsed.Arn };
|
|
1022
|
+
} catch {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function describeType(type) {
|
|
1027
|
+
switch (type) {
|
|
1028
|
+
case "keys":
|
|
1029
|
+
return "access keys";
|
|
1030
|
+
case "sso":
|
|
1031
|
+
return "SSO";
|
|
1032
|
+
case "role":
|
|
1033
|
+
return "assumed role";
|
|
1034
|
+
default:
|
|
1035
|
+
return "config only";
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
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)})`)}`
|
|
1045
|
+
);
|
|
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
|
+
return { kind: "cancel" };
|
|
1062
|
+
}
|
|
1063
|
+
if (n === newIdx) return { kind: "keys" };
|
|
1064
|
+
if (n === ssoIdx) return { kind: "sso" };
|
|
1065
|
+
return { kind: "existing", name: profiles[n - 1].name };
|
|
1066
|
+
}
|
|
1067
|
+
async function runKeyEntry(targetProfile) {
|
|
1068
|
+
console.log("");
|
|
1069
|
+
console.log(" Enter your AWS credentials. These will be saved to the");
|
|
1070
|
+
console.log(` AWS CLI profile "${targetProfile}".`);
|
|
1071
|
+
console.log("");
|
|
1072
|
+
const accessKeyId = await ask(" AWS Access Key ID: ");
|
|
1073
|
+
if (!accessKeyId) {
|
|
1074
|
+
printError("Access Key ID is required");
|
|
1075
|
+
return false;
|
|
1076
|
+
}
|
|
1077
|
+
const secretAccessKey = await ask(" AWS Secret Access Key: ");
|
|
1078
|
+
if (!secretAccessKey) {
|
|
1079
|
+
printError("Secret Access Key is required");
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
const region = await ask(" Default region [us-east-1]: ");
|
|
1083
|
+
const finalRegion = region || "us-east-1";
|
|
1084
|
+
try {
|
|
1085
|
+
execSync4(
|
|
1086
|
+
`aws configure set aws_access_key_id "${accessKeyId}" --profile ${targetProfile}`,
|
|
1087
|
+
{ stdio: "pipe" }
|
|
1088
|
+
);
|
|
1089
|
+
execSync4(
|
|
1090
|
+
`aws configure set aws_secret_access_key "${secretAccessKey}" --profile ${targetProfile}`,
|
|
1091
|
+
{ stdio: "pipe" }
|
|
1092
|
+
);
|
|
1093
|
+
execSync4(
|
|
1094
|
+
`aws configure set region "${finalRegion}" --profile ${targetProfile}`,
|
|
1095
|
+
{ stdio: "pipe" }
|
|
1096
|
+
);
|
|
1097
|
+
return true;
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
printError(`Failed to save credentials: ${err}`);
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function runSsoLogin(targetProfile) {
|
|
1104
|
+
console.log(" Launching AWS SSO login...");
|
|
1105
|
+
console.log("");
|
|
1106
|
+
try {
|
|
1107
|
+
execSync4(`aws sso login --profile ${targetProfile}`, { stdio: "inherit" });
|
|
1108
|
+
return true;
|
|
1109
|
+
} catch {
|
|
1110
|
+
printError(
|
|
1111
|
+
`SSO login failed. Run \`aws configure sso --profile ${targetProfile}\` first to set up the profile.`
|
|
1112
|
+
);
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function finalize(profile, mode) {
|
|
1117
|
+
const identity = getAwsIdentity();
|
|
1118
|
+
if (!identity) {
|
|
1119
|
+
printError(
|
|
1120
|
+
`Credentials saved but could not verify with profile "${profile}". Try \`aws sts get-caller-identity --profile ${profile}\`.`
|
|
1121
|
+
);
|
|
1122
|
+
process.exit(1);
|
|
1123
|
+
}
|
|
1124
|
+
saveCliConfig({ defaultProfile: profile });
|
|
1125
|
+
printSuccess(
|
|
1126
|
+
`Logged in via ${mode} (account: ${identity.account}, region: ${identity.region})`
|
|
1127
|
+
);
|
|
1128
|
+
console.log("");
|
|
1129
|
+
console.log(
|
|
1130
|
+
` Profile "${profile}" saved as your Thinkwork default. Subsequent commands`
|
|
1131
|
+
);
|
|
1132
|
+
console.log(
|
|
1133
|
+
` (\`thinkwork list\`, \`thinkwork deploy\`, \u2026) will use it automatically.`
|
|
1134
|
+
);
|
|
1135
|
+
console.log(
|
|
1136
|
+
chalk5.dim(
|
|
1137
|
+
` Override per-command with --profile <other>, or unset with \`rm ~/.thinkwork/config.json\`.`
|
|
1138
|
+
)
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
913
1141
|
function registerLoginCommand(program2) {
|
|
914
|
-
program2.command("login").description(
|
|
1142
|
+
program2.command("login").description(
|
|
1143
|
+
"Configure AWS credentials for Thinkwork. Picks from existing ~/.aws profiles by default; falls back to new keys or SSO."
|
|
1144
|
+
).option(
|
|
1145
|
+
"--profile <name>",
|
|
1146
|
+
"AWS profile name to configure (used when entering new keys or SSO)",
|
|
1147
|
+
"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) => {
|
|
915
1149
|
printHeader("login", opts.profile);
|
|
916
1150
|
const awsOk = await ensureAwsCli();
|
|
917
|
-
if (!awsOk)
|
|
918
|
-
|
|
1151
|
+
if (!awsOk) process.exit(1);
|
|
1152
|
+
if (opts.sso) {
|
|
1153
|
+
if (!runSsoLogin(opts.profile)) process.exit(1);
|
|
1154
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
1155
|
+
finalize(opts.profile, "SSO");
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (opts.keys) {
|
|
1159
|
+
if (!await runKeyEntry(opts.profile)) process.exit(1);
|
|
1160
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
1161
|
+
finalize(opts.profile, "access keys");
|
|
1162
|
+
return;
|
|
919
1163
|
}
|
|
920
|
-
const
|
|
921
|
-
if (
|
|
922
|
-
console.log(` Already authenticated:`);
|
|
923
|
-
console.log(` Account: ${existing.account}`);
|
|
924
|
-
console.log(` Region: ${existing.region}`);
|
|
925
|
-
console.log(` ARN: ${existing.arn}`);
|
|
1164
|
+
const profiles = listAwsProfiles();
|
|
1165
|
+
if (profiles.length === 0) {
|
|
926
1166
|
console.log("");
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1167
|
+
console.log(chalk5.dim(" No AWS profiles found in ~/.aws/."));
|
|
1168
|
+
console.log(
|
|
1169
|
+
chalk5.dim(" Falling through to access-key entry for a new profile.")
|
|
1170
|
+
);
|
|
1171
|
+
if (!await runKeyEntry(opts.profile)) process.exit(1);
|
|
1172
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
1173
|
+
finalize(opts.profile, "access keys");
|
|
1174
|
+
return;
|
|
932
1175
|
}
|
|
933
|
-
|
|
934
|
-
|
|
1176
|
+
const choice = await pickProfile(profiles);
|
|
1177
|
+
if (choice.kind === "cancel") {
|
|
935
1178
|
console.log("");
|
|
936
|
-
|
|
937
|
-
execSync4(`aws sso login --profile ${opts.profile}`, {
|
|
938
|
-
stdio: "inherit"
|
|
939
|
-
});
|
|
940
|
-
process.env.AWS_PROFILE = opts.profile;
|
|
941
|
-
const identity2 = getAwsIdentity();
|
|
942
|
-
if (identity2) {
|
|
943
|
-
printSuccess(`Logged in via SSO (account: ${identity2.account}, region: ${identity2.region})`);
|
|
944
|
-
} else {
|
|
945
|
-
printError("SSO login succeeded but could not verify identity");
|
|
946
|
-
}
|
|
947
|
-
} catch {
|
|
948
|
-
printError("SSO login failed. Run `aws configure sso` first to set up your SSO profile.");
|
|
949
|
-
}
|
|
1179
|
+
console.log(chalk5.dim(" Cancelled. No changes made."));
|
|
950
1180
|
return;
|
|
951
1181
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
printError("Access Key ID is required");
|
|
958
|
-
process.exit(1);
|
|
1182
|
+
if (choice.kind === "keys") {
|
|
1183
|
+
if (!await runKeyEntry(opts.profile)) process.exit(1);
|
|
1184
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
1185
|
+
finalize(opts.profile, "access keys");
|
|
1186
|
+
return;
|
|
959
1187
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1188
|
+
if (choice.kind === "sso") {
|
|
1189
|
+
if (!runSsoLogin(opts.profile)) process.exit(1);
|
|
1190
|
+
process.env.AWS_PROFILE = opts.profile;
|
|
1191
|
+
finalize(opts.profile, "SSO");
|
|
1192
|
+
return;
|
|
964
1193
|
}
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1194
|
+
const picked = choice.name;
|
|
1195
|
+
console.log("");
|
|
1196
|
+
console.log(` Verifying "${picked}"...`);
|
|
1197
|
+
const identity = verifyProfile(picked);
|
|
1198
|
+
if (!identity) {
|
|
1199
|
+
printError(
|
|
1200
|
+
`Could not authenticate with profile "${picked}". If it's an SSO profile, try \`aws sso login --profile ${picked}\` first.`
|
|
1201
|
+
);
|
|
973
1202
|
process.exit(1);
|
|
974
1203
|
}
|
|
975
|
-
process.env.AWS_PROFILE =
|
|
976
|
-
|
|
977
|
-
if (identity) {
|
|
978
|
-
printSuccess(`Logged in (account: ${identity.account}, region: ${identity.region})`);
|
|
979
|
-
console.log("");
|
|
980
|
-
console.log(` Profile saved as "${opts.profile}". Use it with:`);
|
|
981
|
-
console.log(` thinkwork deploy -s dev --profile ${opts.profile}`);
|
|
982
|
-
console.log(` export AWS_PROFILE=${opts.profile}`);
|
|
983
|
-
} else {
|
|
984
|
-
printError("Credentials saved but could not verify. Check your Access Key ID and Secret.");
|
|
985
|
-
}
|
|
1204
|
+
process.env.AWS_PROFILE = picked;
|
|
1205
|
+
finalize(picked, "existing profile");
|
|
986
1206
|
});
|
|
987
1207
|
}
|
|
988
1208
|
|
|
989
1209
|
// src/commands/init.ts
|
|
990
|
-
import { existsSync as
|
|
991
|
-
import { resolve as resolve2, join as
|
|
1210
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, cpSync } from "fs";
|
|
1211
|
+
import { resolve as resolve2, join as join5, dirname as dirname2 } from "path";
|
|
992
1212
|
import { execSync as execSync5 } from "child_process";
|
|
993
1213
|
import { fileURLToPath } from "url";
|
|
994
1214
|
import { createInterface as createInterface3 } from "readline";
|
|
995
|
-
import
|
|
996
|
-
var __dirname =
|
|
1215
|
+
import chalk6 from "chalk";
|
|
1216
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
997
1217
|
function ask2(prompt2, defaultVal = "") {
|
|
998
1218
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
999
|
-
const suffix = defaultVal ?
|
|
1219
|
+
const suffix = defaultVal ? chalk6.dim(` [${defaultVal}]`) : "";
|
|
1000
1220
|
return new Promise((resolve3) => {
|
|
1001
1221
|
rl.question(` ${prompt2}${suffix}: `, (answer) => {
|
|
1002
1222
|
rl.close();
|
|
@@ -1005,7 +1225,7 @@ function ask2(prompt2, defaultVal = "") {
|
|
|
1005
1225
|
});
|
|
1006
1226
|
}
|
|
1007
1227
|
function choose(prompt2, options, defaultVal) {
|
|
1008
|
-
const optStr = options.map((o) => o === defaultVal ?
|
|
1228
|
+
const optStr = options.map((o) => o === defaultVal ? chalk6.bold(o) : chalk6.dim(o)).join(" / ");
|
|
1009
1229
|
return ask2(`${prompt2} (${optStr})`, defaultVal);
|
|
1010
1230
|
}
|
|
1011
1231
|
function generateSecret(length = 32) {
|
|
@@ -1018,9 +1238,9 @@ function generateSecret(length = 32) {
|
|
|
1018
1238
|
}
|
|
1019
1239
|
function findBundledTerraform() {
|
|
1020
1240
|
const bundled = resolve2(__dirname, "terraform");
|
|
1021
|
-
if (
|
|
1241
|
+
if (existsSync7(join5(bundled, "modules"))) return bundled;
|
|
1022
1242
|
const repoTf = resolve2(__dirname, "..", "..", "..", "terraform");
|
|
1023
|
-
if (
|
|
1243
|
+
if (existsSync7(join5(repoTf, "modules"))) return repoTf;
|
|
1024
1244
|
throw new Error(
|
|
1025
1245
|
"Terraform modules not found. The CLI package may be incomplete.\nTry reinstalling: npm install -g thinkwork-cli@latest"
|
|
1026
1246
|
);
|
|
@@ -1090,9 +1310,9 @@ function registerInitCommand(program2) {
|
|
|
1090
1310
|
process.exit(1);
|
|
1091
1311
|
}
|
|
1092
1312
|
const targetDir = resolve2(opts.dir);
|
|
1093
|
-
const tfDir =
|
|
1094
|
-
const tfvarsPath =
|
|
1095
|
-
if (
|
|
1313
|
+
const tfDir = join5(targetDir, "terraform");
|
|
1314
|
+
const tfvarsPath = join5(tfDir, "terraform.tfvars");
|
|
1315
|
+
if (existsSync7(tfvarsPath)) {
|
|
1096
1316
|
printWarning(`terraform.tfvars already exists at ${tfvarsPath}`);
|
|
1097
1317
|
const overwrite = await ask2("Overwrite?", "N");
|
|
1098
1318
|
if (overwrite.toLowerCase() !== "y") {
|
|
@@ -1116,21 +1336,21 @@ function registerInitCommand(program2) {
|
|
|
1116
1336
|
config.admin_url = "http://localhost:5174";
|
|
1117
1337
|
config.mobile_scheme = "thinkwork";
|
|
1118
1338
|
} else {
|
|
1119
|
-
console.log(
|
|
1339
|
+
console.log(chalk6.bold(" Configure your Thinkwork environment\n"));
|
|
1120
1340
|
const defaultRegion = identity.region !== "unknown" ? identity.region : "us-east-1";
|
|
1121
1341
|
config.region = await ask2("AWS Region", defaultRegion);
|
|
1122
1342
|
console.log("");
|
|
1123
|
-
console.log(
|
|
1343
|
+
console.log(chalk6.dim(" \u2500\u2500 Database \u2500\u2500"));
|
|
1124
1344
|
config.database_engine = await choose("Database engine", ["aurora-serverless", "rds-postgres"], "aurora-serverless");
|
|
1125
1345
|
console.log("");
|
|
1126
|
-
console.log(
|
|
1127
|
-
console.log(
|
|
1128
|
-
console.log(
|
|
1129
|
-
console.log(
|
|
1346
|
+
console.log(chalk6.dim(" \u2500\u2500 Memory \u2500\u2500"));
|
|
1347
|
+
console.log(chalk6.dim(" AgentCore managed memory is always on (automatic retention)."));
|
|
1348
|
+
console.log(chalk6.dim(" Hindsight is an optional add-on: ECS Fargate service for"));
|
|
1349
|
+
console.log(chalk6.dim(" semantic + entity-graph retrieval (~$75/mo)."));
|
|
1130
1350
|
const hindsightAnswer = await ask2("Enable Hindsight long-term memory add-on? (y/N)", "N");
|
|
1131
1351
|
config.enable_hindsight = hindsightAnswer.toLowerCase() === "y" ? "true" : "false";
|
|
1132
1352
|
console.log("");
|
|
1133
|
-
console.log(
|
|
1353
|
+
console.log(chalk6.dim(" \u2500\u2500 Auth \u2500\u2500"));
|
|
1134
1354
|
const useGoogle = await ask2("Enable Google OAuth login? (y/N)", "N");
|
|
1135
1355
|
if (useGoogle.toLowerCase() === "y") {
|
|
1136
1356
|
config.google_oauth_client_id = await ask2("Google OAuth Client ID");
|
|
@@ -1140,13 +1360,13 @@ function registerInitCommand(program2) {
|
|
|
1140
1360
|
config.google_oauth_client_secret = "";
|
|
1141
1361
|
}
|
|
1142
1362
|
console.log("");
|
|
1143
|
-
console.log(
|
|
1363
|
+
console.log(chalk6.dim(" \u2500\u2500 Frontend URLs \u2500\u2500"));
|
|
1144
1364
|
config.admin_url = await ask2("Admin UI URL", "http://localhost:5174");
|
|
1145
1365
|
config.mobile_scheme = await ask2("Mobile app URL scheme", "thinkwork");
|
|
1146
1366
|
console.log("");
|
|
1147
|
-
console.log(
|
|
1148
|
-
console.log(
|
|
1149
|
-
console.log(
|
|
1367
|
+
console.log(chalk6.dim(" \u2500\u2500 Secrets (auto-generated) \u2500\u2500"));
|
|
1368
|
+
console.log(chalk6.dim(` DB password: ${config.db_password.slice(0, 8)}...`));
|
|
1369
|
+
console.log(chalk6.dim(` API auth secret: ${config.api_auth_secret.slice(0, 16)}...`));
|
|
1150
1370
|
}
|
|
1151
1371
|
console.log("");
|
|
1152
1372
|
console.log(" Scaffolding Terraform modules...");
|
|
@@ -1157,24 +1377,24 @@ function registerInitCommand(program2) {
|
|
|
1157
1377
|
printError(String(err));
|
|
1158
1378
|
process.exit(1);
|
|
1159
1379
|
}
|
|
1160
|
-
|
|
1380
|
+
mkdirSync4(tfDir, { recursive: true });
|
|
1161
1381
|
const copyDirs = ["modules", "examples"];
|
|
1162
1382
|
for (const dir of copyDirs) {
|
|
1163
|
-
const src =
|
|
1164
|
-
const dst =
|
|
1165
|
-
if (
|
|
1383
|
+
const src = join5(bundledTf, dir);
|
|
1384
|
+
const dst = join5(tfDir, dir);
|
|
1385
|
+
if (existsSync7(src) && !existsSync7(dst)) {
|
|
1166
1386
|
cpSync(src, dst, { recursive: true });
|
|
1167
1387
|
}
|
|
1168
1388
|
}
|
|
1169
|
-
const schemaPath =
|
|
1170
|
-
if (
|
|
1171
|
-
cpSync(schemaPath,
|
|
1389
|
+
const schemaPath = join5(bundledTf, "schema.graphql");
|
|
1390
|
+
if (existsSync7(schemaPath) && !existsSync7(join5(tfDir, "schema.graphql"))) {
|
|
1391
|
+
cpSync(schemaPath, join5(tfDir, "schema.graphql"));
|
|
1172
1392
|
}
|
|
1173
1393
|
const tfvars = buildTfvars(config);
|
|
1174
|
-
|
|
1175
|
-
const mainTfPath =
|
|
1176
|
-
if (!
|
|
1177
|
-
|
|
1394
|
+
writeFileSync4(tfvarsPath, tfvars);
|
|
1395
|
+
const mainTfPath = join5(tfDir, "main.tf");
|
|
1396
|
+
if (!existsSync7(mainTfPath)) {
|
|
1397
|
+
writeFileSync4(mainTfPath, `################################################################################
|
|
1178
1398
|
# Thinkwork \u2014 ${config.stage}
|
|
1179
1399
|
# Generated by: thinkwork init -s ${config.stage}
|
|
1180
1400
|
################################################################################
|
|
@@ -1351,17 +1571,17 @@ output "agentcore_memory_id" {
|
|
|
1351
1571
|
}
|
|
1352
1572
|
`);
|
|
1353
1573
|
}
|
|
1354
|
-
console.log(` Wrote ${
|
|
1574
|
+
console.log(` Wrote ${chalk6.cyan(tfDir + "/")}`);
|
|
1355
1575
|
console.log("");
|
|
1356
|
-
console.log(
|
|
1357
|
-
console.log(` ${
|
|
1358
|
-
console.log(` ${
|
|
1359
|
-
console.log(` ${
|
|
1360
|
-
console.log(` ${
|
|
1361
|
-
console.log(` ${
|
|
1362
|
-
console.log(` ${
|
|
1363
|
-
console.log(` ${
|
|
1364
|
-
console.log(
|
|
1576
|
+
console.log(chalk6.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"));
|
|
1577
|
+
console.log(` ${chalk6.bold("Stage:")} ${config.stage}`);
|
|
1578
|
+
console.log(` ${chalk6.bold("Region:")} ${config.region}`);
|
|
1579
|
+
console.log(` ${chalk6.bold("Account:")} ${config.account_id}`);
|
|
1580
|
+
console.log(` ${chalk6.bold("Database:")} ${config.database_engine}`);
|
|
1581
|
+
console.log(` ${chalk6.bold("Memory:")} managed (always on)${config.enable_hindsight === "true" ? " + hindsight" : ""}`);
|
|
1582
|
+
console.log(` ${chalk6.bold("Google OAuth:")} ${config.google_oauth_client_id ? "enabled" : "disabled"}`);
|
|
1583
|
+
console.log(` ${chalk6.bold("Directory:")} ${tfDir}`);
|
|
1584
|
+
console.log(chalk6.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"));
|
|
1365
1585
|
console.log("\n Initializing Terraform...\n");
|
|
1366
1586
|
try {
|
|
1367
1587
|
execSync5("terraform init", { cwd: tfDir, stdio: "inherit" });
|
|
@@ -1383,17 +1603,17 @@ output "agentcore_memory_id" {
|
|
|
1383
1603
|
printSuccess(`Environment "${opts.stage}" initialized`);
|
|
1384
1604
|
console.log("");
|
|
1385
1605
|
console.log(" Next steps:");
|
|
1386
|
-
console.log(` ${
|
|
1387
|
-
console.log(` ${
|
|
1388
|
-
console.log(` ${
|
|
1389
|
-
console.log(` ${
|
|
1606
|
+
console.log(` ${chalk6.cyan("1.")} thinkwork plan -s ${opts.stage} ${chalk6.dim("# Review infrastructure plan")}`);
|
|
1607
|
+
console.log(` ${chalk6.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk6.dim("# Deploy to AWS (~5 min)")}`);
|
|
1608
|
+
console.log(` ${chalk6.cyan("3.")} thinkwork bootstrap -s ${opts.stage} ${chalk6.dim("# Seed workspace files + skills")}`);
|
|
1609
|
+
console.log(` ${chalk6.cyan("4.")} thinkwork outputs -s ${opts.stage} ${chalk6.dim("# Show API URL, Cognito IDs, etc.")}`);
|
|
1390
1610
|
console.log("");
|
|
1391
1611
|
});
|
|
1392
1612
|
}
|
|
1393
1613
|
|
|
1394
1614
|
// src/commands/status.ts
|
|
1395
1615
|
import { execSync as execSync6 } from "child_process";
|
|
1396
|
-
import
|
|
1616
|
+
import chalk7 from "chalk";
|
|
1397
1617
|
function link(url, label) {
|
|
1398
1618
|
const text = label || url;
|
|
1399
1619
|
return `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
|
|
@@ -1491,45 +1711,47 @@ function discoverAwsStages(region) {
|
|
|
1491
1711
|
return stages;
|
|
1492
1712
|
}
|
|
1493
1713
|
function printStageDetail(info) {
|
|
1494
|
-
console.log(
|
|
1495
|
-
console.log(
|
|
1496
|
-
console.log(` ${
|
|
1497
|
-
console.log(` ${
|
|
1498
|
-
console.log(` ${
|
|
1499
|
-
console.log(` ${
|
|
1500
|
-
console.log(` ${
|
|
1501
|
-
console.log(` ${
|
|
1714
|
+
console.log(chalk7.bold.cyan(` \u2B21 ${info.stage}`));
|
|
1715
|
+
console.log(chalk7.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"));
|
|
1716
|
+
console.log(` ${chalk7.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
|
|
1717
|
+
console.log(` ${chalk7.bold("Region:")} ${info.region}`);
|
|
1718
|
+
console.log(` ${chalk7.bold("Account:")} ${info.accountId}`);
|
|
1719
|
+
console.log(` ${chalk7.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
|
|
1720
|
+
console.log(` ${chalk7.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
|
|
1721
|
+
console.log(` ${chalk7.bold("Memory:")} managed (always on)`);
|
|
1502
1722
|
const hindsightLabel = info.hindsightEnabled === void 0 ? "unknown" : info.hindsightEnabled ? info.hindsightHealth || "running" : "disabled";
|
|
1503
|
-
console.log(` ${
|
|
1504
|
-
if (info.bucketName) console.log(` ${
|
|
1505
|
-
if (info.dbEndpoint) console.log(` ${
|
|
1506
|
-
if (info.ecrUrl) console.log(` ${
|
|
1723
|
+
console.log(` ${chalk7.bold("Hindsight:")} ${hindsightLabel}`);
|
|
1724
|
+
if (info.bucketName) console.log(` ${chalk7.bold("S3 bucket:")} ${info.bucketName}`);
|
|
1725
|
+
if (info.dbEndpoint) console.log(` ${chalk7.bold("Database:")} ${info.dbEndpoint}`);
|
|
1726
|
+
if (info.ecrUrl) console.log(` ${chalk7.bold("ECR:")} ${info.ecrUrl}`);
|
|
1507
1727
|
console.log("");
|
|
1508
|
-
console.log(
|
|
1728
|
+
console.log(chalk7.bold(" URLs:"));
|
|
1509
1729
|
if (info.adminUrl) console.log(` Admin: ${link(info.adminUrl)}`);
|
|
1510
1730
|
if (info.docsUrl) console.log(` Docs: ${link(info.docsUrl)}`);
|
|
1511
1731
|
if (info.apiEndpoint) console.log(` API: ${link(info.apiEndpoint)}`);
|
|
1512
1732
|
if (info.appsyncApiUrl) console.log(` AppSync: ${link(info.appsyncApiUrl)}`);
|
|
1513
1733
|
if (info.appsyncUrl) console.log(` WebSocket: ${link(info.appsyncUrl)}`);
|
|
1514
1734
|
if (info.hindsightEndpoint) console.log(` Hindsight: ${link(info.hindsightEndpoint)}`);
|
|
1515
|
-
console.log(
|
|
1735
|
+
console.log(chalk7.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"));
|
|
1516
1736
|
const local = loadEnvironment(info.stage);
|
|
1517
1737
|
if (local) {
|
|
1518
|
-
console.log(
|
|
1738
|
+
console.log(chalk7.dim(` Terraform dir: ${local.terraformDir}`));
|
|
1519
1739
|
} else {
|
|
1520
|
-
console.log(
|
|
1740
|
+
console.log(chalk7.dim(` No local config. Run: thinkwork init -s ${info.stage}`));
|
|
1521
1741
|
}
|
|
1522
1742
|
console.log("");
|
|
1523
1743
|
}
|
|
1524
1744
|
function registerStatusCommand(program2) {
|
|
1525
|
-
program2.command("status").
|
|
1745
|
+
program2.command("status").alias("list").alias("ls").description(
|
|
1746
|
+
"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) => {
|
|
1526
1748
|
const identity = getAwsIdentity();
|
|
1527
1749
|
printHeader("status", opts.stage || "all", identity);
|
|
1528
1750
|
if (!identity) {
|
|
1529
1751
|
printError("AWS credentials not configured. Run `thinkwork login` first.");
|
|
1530
1752
|
process.exit(1);
|
|
1531
1753
|
}
|
|
1532
|
-
console.log(
|
|
1754
|
+
console.log(chalk7.dim(" Scanning AWS account for Thinkwork deployments...\n"));
|
|
1533
1755
|
const awsStages = discoverAwsStages(opts.region);
|
|
1534
1756
|
const localEnvs = listEnvironments();
|
|
1535
1757
|
const merged = /* @__PURE__ */ new Map();
|
|
@@ -1564,7 +1786,7 @@ function registerStatusCommand(program2) {
|
|
|
1564
1786
|
}
|
|
1565
1787
|
if (merged.size === 0) {
|
|
1566
1788
|
console.log(" No Thinkwork environments found.");
|
|
1567
|
-
console.log(` Run ${
|
|
1789
|
+
console.log(` Run ${chalk7.cyan("thinkwork init -s <stage>")} to create one.`);
|
|
1568
1790
|
console.log("");
|
|
1569
1791
|
return;
|
|
1570
1792
|
}
|
|
@@ -1575,12 +1797,14 @@ function registerStatusCommand(program2) {
|
|
|
1575
1797
|
}
|
|
1576
1798
|
|
|
1577
1799
|
// src/commands/mcp.ts
|
|
1578
|
-
import
|
|
1800
|
+
import chalk8 from "chalk";
|
|
1801
|
+
|
|
1802
|
+
// src/api-client.ts
|
|
1803
|
+
import { readFileSync as readFileSync5, existsSync as existsSync8 } from "fs";
|
|
1579
1804
|
import { execSync as execSync7 } from "child_process";
|
|
1580
|
-
import chalk7 from "chalk";
|
|
1581
1805
|
function readTfVar2(tfvarsPath, key) {
|
|
1582
|
-
if (!
|
|
1583
|
-
const content =
|
|
1806
|
+
if (!existsSync8(tfvarsPath)) return null;
|
|
1807
|
+
const content = readFileSync5(tfvarsPath, "utf-8");
|
|
1584
1808
|
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
|
|
1585
1809
|
return match ? match[1] : null;
|
|
1586
1810
|
}
|
|
@@ -1588,7 +1812,7 @@ function resolveTfvarsPath2(stage) {
|
|
|
1588
1812
|
const tfDir = resolveTerraformDir(stage);
|
|
1589
1813
|
if (tfDir) {
|
|
1590
1814
|
const direct = `${tfDir}/terraform.tfvars`;
|
|
1591
|
-
if (
|
|
1815
|
+
if (existsSync8(direct)) return direct;
|
|
1592
1816
|
}
|
|
1593
1817
|
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
1594
1818
|
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
@@ -1621,6 +1845,19 @@ async function apiFetch(apiUrl, authSecret, path2, options = {}, extraHeaders =
|
|
|
1621
1845
|
}
|
|
1622
1846
|
return res.json();
|
|
1623
1847
|
}
|
|
1848
|
+
async function apiFetchRaw(apiUrl, authSecret, path2, options = {}, extraHeaders = {}) {
|
|
1849
|
+
const res = await fetch(`${apiUrl}${path2}`, {
|
|
1850
|
+
...options,
|
|
1851
|
+
headers: {
|
|
1852
|
+
"Content-Type": "application/json",
|
|
1853
|
+
Authorization: `Bearer ${authSecret}`,
|
|
1854
|
+
...extraHeaders,
|
|
1855
|
+
...options.headers
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
const body = await res.json().catch(() => ({}));
|
|
1859
|
+
return { ok: res.ok, status: res.status, body };
|
|
1860
|
+
}
|
|
1624
1861
|
function resolveApiConfig(stage) {
|
|
1625
1862
|
const tfvarsPath = resolveTfvarsPath2(stage);
|
|
1626
1863
|
const authSecret = readTfVar2(tfvarsPath, "api_auth_secret");
|
|
@@ -1631,11 +1868,15 @@ function resolveApiConfig(stage) {
|
|
|
1631
1868
|
const region = readTfVar2(tfvarsPath, "region") || "us-east-1";
|
|
1632
1869
|
const apiUrl = getApiEndpoint(stage, region);
|
|
1633
1870
|
if (!apiUrl) {
|
|
1634
|
-
printError(
|
|
1871
|
+
printError(
|
|
1872
|
+
`Cannot discover API endpoint for stage "${stage}". Is the stack deployed?`
|
|
1873
|
+
);
|
|
1635
1874
|
return null;
|
|
1636
1875
|
}
|
|
1637
1876
|
return { apiUrl, authSecret };
|
|
1638
1877
|
}
|
|
1878
|
+
|
|
1879
|
+
// src/commands/mcp.ts
|
|
1639
1880
|
function registerMcpCommand(program2) {
|
|
1640
1881
|
const mcp = program2.command("mcp").description("Manage MCP servers for your tenant");
|
|
1641
1882
|
mcp.command("list").description("List registered MCP servers").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
|
|
@@ -1650,14 +1891,14 @@ function registerMcpCommand(program2) {
|
|
|
1650
1891
|
try {
|
|
1651
1892
|
const { servers } = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {}, { "x-tenant-slug": opts.tenant });
|
|
1652
1893
|
if (!servers || servers.length === 0) {
|
|
1653
|
-
console.log(
|
|
1894
|
+
console.log(chalk8.dim(" No MCP servers registered."));
|
|
1654
1895
|
return;
|
|
1655
1896
|
}
|
|
1656
1897
|
console.log("");
|
|
1657
1898
|
for (const s of servers) {
|
|
1658
|
-
const status = s.enabled ?
|
|
1899
|
+
const status = s.enabled ? chalk8.green("enabled") : chalk8.dim("disabled");
|
|
1659
1900
|
const authLabel = s.authType === "per_user_oauth" ? `OAuth (${s.oauthProvider})` : s.authType === "tenant_api_key" ? "API Key" : "none";
|
|
1660
|
-
console.log(` ${
|
|
1901
|
+
console.log(` ${chalk8.bold(s.name)} ${chalk8.dim(s.slug)} ${status}`);
|
|
1661
1902
|
console.log(` URL: ${s.url}`);
|
|
1662
1903
|
console.log(` Transport: ${s.transport}`);
|
|
1663
1904
|
console.log(` Auth: ${authLabel}`);
|
|
@@ -1732,11 +1973,11 @@ function registerMcpCommand(program2) {
|
|
|
1732
1973
|
if (result.ok) {
|
|
1733
1974
|
printSuccess("Connection successful.");
|
|
1734
1975
|
if (result.tools?.length) {
|
|
1735
|
-
console.log(
|
|
1976
|
+
console.log(chalk8.bold(`
|
|
1736
1977
|
Discovered tools (${result.tools.length}):
|
|
1737
1978
|
`));
|
|
1738
1979
|
for (const t of result.tools) {
|
|
1739
|
-
console.log(` ${
|
|
1980
|
+
console.log(` ${chalk8.cyan(t.name)}${t.description ? chalk8.dim(` - ${t.description}`) : ""}`);
|
|
1740
1981
|
}
|
|
1741
1982
|
console.log("");
|
|
1742
1983
|
} else {
|
|
@@ -1791,68 +2032,8 @@ function registerMcpCommand(program2) {
|
|
|
1791
2032
|
}
|
|
1792
2033
|
|
|
1793
2034
|
// src/commands/tools.ts
|
|
1794
|
-
import { readFileSync as readFileSync4, existsSync as existsSync7 } from "fs";
|
|
1795
|
-
import { execSync as execSync8 } from "child_process";
|
|
1796
2035
|
import { createInterface as createInterface4 } from "readline";
|
|
1797
|
-
import
|
|
1798
|
-
function readTfVar3(tfvarsPath, key) {
|
|
1799
|
-
if (!existsSync7(tfvarsPath)) return null;
|
|
1800
|
-
const content = readFileSync4(tfvarsPath, "utf-8");
|
|
1801
|
-
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
|
|
1802
|
-
return match ? match[1] : null;
|
|
1803
|
-
}
|
|
1804
|
-
function resolveTfvarsPath3(stage) {
|
|
1805
|
-
const tfDir = resolveTerraformDir(stage);
|
|
1806
|
-
if (tfDir) {
|
|
1807
|
-
const direct = `${tfDir}/terraform.tfvars`;
|
|
1808
|
-
if (existsSync7(direct)) return direct;
|
|
1809
|
-
}
|
|
1810
|
-
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
1811
|
-
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
1812
|
-
return `${cwd}/terraform.tfvars`;
|
|
1813
|
-
}
|
|
1814
|
-
function getApiEndpoint2(stage, region) {
|
|
1815
|
-
try {
|
|
1816
|
-
const raw = execSync8(
|
|
1817
|
-
`aws apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`,
|
|
1818
|
-
{ encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
|
|
1819
|
-
).trim();
|
|
1820
|
-
return raw && raw !== "None" ? raw : null;
|
|
1821
|
-
} catch {
|
|
1822
|
-
return null;
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
async function apiFetch2(apiUrl, authSecret, path2, options = {}, extraHeaders = {}) {
|
|
1826
|
-
const res = await fetch(`${apiUrl}${path2}`, {
|
|
1827
|
-
...options,
|
|
1828
|
-
headers: {
|
|
1829
|
-
"Content-Type": "application/json",
|
|
1830
|
-
Authorization: `Bearer ${authSecret}`,
|
|
1831
|
-
...extraHeaders,
|
|
1832
|
-
...options.headers
|
|
1833
|
-
}
|
|
1834
|
-
});
|
|
1835
|
-
if (!res.ok) {
|
|
1836
|
-
const body = await res.json().catch(() => ({}));
|
|
1837
|
-
throw new Error(body.error || `HTTP ${res.status}`);
|
|
1838
|
-
}
|
|
1839
|
-
return res.json();
|
|
1840
|
-
}
|
|
1841
|
-
function resolveApiConfig2(stage) {
|
|
1842
|
-
const tfvarsPath = resolveTfvarsPath3(stage);
|
|
1843
|
-
const authSecret = readTfVar3(tfvarsPath, "api_auth_secret");
|
|
1844
|
-
if (!authSecret) {
|
|
1845
|
-
printError(`Cannot read api_auth_secret from ${tfvarsPath}`);
|
|
1846
|
-
return null;
|
|
1847
|
-
}
|
|
1848
|
-
const region = readTfVar3(tfvarsPath, "region") || "us-east-1";
|
|
1849
|
-
const apiUrl = getApiEndpoint2(stage, region);
|
|
1850
|
-
if (!apiUrl) {
|
|
1851
|
-
printError(`Cannot discover API endpoint for stage "${stage}". Is the stack deployed?`);
|
|
1852
|
-
return null;
|
|
1853
|
-
}
|
|
1854
|
-
return { apiUrl, authSecret };
|
|
1855
|
-
}
|
|
2036
|
+
import chalk9 from "chalk";
|
|
1856
2037
|
function prompt(question) {
|
|
1857
2038
|
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
1858
2039
|
return new Promise((resolve3) => {
|
|
@@ -1873,11 +2054,11 @@ function registerToolsCommand(program2) {
|
|
|
1873
2054
|
printError(check.error);
|
|
1874
2055
|
process.exit(1);
|
|
1875
2056
|
}
|
|
1876
|
-
const api =
|
|
2057
|
+
const api = resolveApiConfig(opts.stage);
|
|
1877
2058
|
if (!api) process.exit(1);
|
|
1878
2059
|
printHeader("tools list", opts.stage);
|
|
1879
2060
|
try {
|
|
1880
|
-
const { tools: rows } = await
|
|
2061
|
+
const { tools: rows } = await apiFetch(
|
|
1881
2062
|
api.apiUrl,
|
|
1882
2063
|
api.authSecret,
|
|
1883
2064
|
"/api/skills/builtin-tools",
|
|
@@ -1885,16 +2066,16 @@ function registerToolsCommand(program2) {
|
|
|
1885
2066
|
{ "x-tenant-slug": opts.tenant }
|
|
1886
2067
|
);
|
|
1887
2068
|
if (!rows || rows.length === 0) {
|
|
1888
|
-
console.log(
|
|
1889
|
-
console.log(
|
|
2069
|
+
console.log(chalk9.dim(" No built-in tools configured."));
|
|
2070
|
+
console.log(chalk9.dim(" Try: thinkwork tools web-search set --tenant <slug> -s <stage>"));
|
|
1890
2071
|
return;
|
|
1891
2072
|
}
|
|
1892
2073
|
console.log("");
|
|
1893
2074
|
for (const r of rows) {
|
|
1894
|
-
const status = r.enabled ?
|
|
1895
|
-
const key = r.hasSecret ?
|
|
1896
|
-
const provider = r.provider ??
|
|
1897
|
-
console.log(` ${
|
|
2075
|
+
const status = r.enabled ? chalk9.green("enabled") : chalk9.dim("disabled");
|
|
2076
|
+
const key = r.hasSecret ? chalk9.green("yes") : chalk9.red("no");
|
|
2077
|
+
const provider = r.provider ?? chalk9.dim("\u2014");
|
|
2078
|
+
console.log(` ${chalk9.bold(r.toolSlug)} ${status}`);
|
|
1898
2079
|
console.log(` Provider: ${provider}`);
|
|
1899
2080
|
console.log(` Has key: ${key}`);
|
|
1900
2081
|
if (r.lastTestedAt) {
|
|
@@ -1914,7 +2095,7 @@ function registerToolsCommand(program2) {
|
|
|
1914
2095
|
printError(check.error);
|
|
1915
2096
|
process.exit(1);
|
|
1916
2097
|
}
|
|
1917
|
-
const api =
|
|
2098
|
+
const api = resolveApiConfig(opts.stage);
|
|
1918
2099
|
if (!api) process.exit(1);
|
|
1919
2100
|
let provider = opts.provider;
|
|
1920
2101
|
if (!provider) {
|
|
@@ -1933,7 +2114,7 @@ function registerToolsCommand(program2) {
|
|
|
1933
2114
|
process.exit(1);
|
|
1934
2115
|
}
|
|
1935
2116
|
try {
|
|
1936
|
-
await
|
|
2117
|
+
await apiFetch(
|
|
1937
2118
|
api.apiUrl,
|
|
1938
2119
|
api.authSecret,
|
|
1939
2120
|
"/api/skills/builtin-tools/web-search",
|
|
@@ -1956,11 +2137,11 @@ function registerToolsCommand(program2) {
|
|
|
1956
2137
|
printError(check.error);
|
|
1957
2138
|
process.exit(1);
|
|
1958
2139
|
}
|
|
1959
|
-
const api =
|
|
2140
|
+
const api = resolveApiConfig(opts.stage);
|
|
1960
2141
|
if (!api) process.exit(1);
|
|
1961
2142
|
printHeader("tools web-search test", opts.stage);
|
|
1962
2143
|
try {
|
|
1963
|
-
const result = await
|
|
2144
|
+
const result = await apiFetch(
|
|
1964
2145
|
api.apiUrl,
|
|
1965
2146
|
api.authSecret,
|
|
1966
2147
|
"/api/skills/builtin-tools/web-search/test",
|
|
@@ -1984,10 +2165,10 @@ function registerToolsCommand(program2) {
|
|
|
1984
2165
|
printError(check.error);
|
|
1985
2166
|
process.exit(1);
|
|
1986
2167
|
}
|
|
1987
|
-
const api =
|
|
2168
|
+
const api = resolveApiConfig(opts.stage);
|
|
1988
2169
|
if (!api) process.exit(1);
|
|
1989
2170
|
try {
|
|
1990
|
-
await
|
|
2171
|
+
await apiFetch(
|
|
1991
2172
|
api.apiUrl,
|
|
1992
2173
|
api.authSecret,
|
|
1993
2174
|
"/api/skills/builtin-tools/web-search",
|
|
@@ -2006,10 +2187,10 @@ function registerToolsCommand(program2) {
|
|
|
2006
2187
|
printError(check.error);
|
|
2007
2188
|
process.exit(1);
|
|
2008
2189
|
}
|
|
2009
|
-
const api =
|
|
2190
|
+
const api = resolveApiConfig(opts.stage);
|
|
2010
2191
|
if (!api) process.exit(1);
|
|
2011
2192
|
try {
|
|
2012
|
-
await
|
|
2193
|
+
await apiFetch(
|
|
2013
2194
|
api.apiUrl,
|
|
2014
2195
|
api.authSecret,
|
|
2015
2196
|
"/api/skills/builtin-tools/web-search",
|
|
@@ -2025,12 +2206,12 @@ function registerToolsCommand(program2) {
|
|
|
2025
2206
|
}
|
|
2026
2207
|
|
|
2027
2208
|
// src/commands/update.ts
|
|
2028
|
-
import { execSync as
|
|
2209
|
+
import { execSync as execSync8 } from "child_process";
|
|
2029
2210
|
import { realpathSync } from "fs";
|
|
2030
|
-
import
|
|
2211
|
+
import chalk10 from "chalk";
|
|
2031
2212
|
function getLatestVersion() {
|
|
2032
2213
|
try {
|
|
2033
|
-
return
|
|
2214
|
+
return execSync8("npm view thinkwork-cli version", {
|
|
2034
2215
|
encoding: "utf-8",
|
|
2035
2216
|
timeout: 1e4,
|
|
2036
2217
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2041,7 +2222,7 @@ function getLatestVersion() {
|
|
|
2041
2222
|
}
|
|
2042
2223
|
function detectInstallMethod() {
|
|
2043
2224
|
try {
|
|
2044
|
-
const which =
|
|
2225
|
+
const which = execSync8("which thinkwork", {
|
|
2045
2226
|
encoding: "utf-8",
|
|
2046
2227
|
timeout: 5e3,
|
|
2047
2228
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -2069,28 +2250,28 @@ function compareVersions(a, b) {
|
|
|
2069
2250
|
function registerUpdateCommand(program2) {
|
|
2070
2251
|
program2.command("update").description("Check for and install CLI updates").option("--check", "Only check for updates, don't install").action(async (opts) => {
|
|
2071
2252
|
printHeader("update", "", null);
|
|
2072
|
-
console.log(` Current version: ${
|
|
2253
|
+
console.log(` Current version: ${chalk10.bold(VERSION)}`);
|
|
2073
2254
|
const latest = getLatestVersion();
|
|
2074
2255
|
if (!latest) {
|
|
2075
|
-
console.log(
|
|
2256
|
+
console.log(chalk10.yellow(" Could not check npm registry for updates."));
|
|
2076
2257
|
return;
|
|
2077
2258
|
}
|
|
2078
|
-
console.log(` Latest version: ${
|
|
2259
|
+
console.log(` Latest version: ${chalk10.bold(latest)}`);
|
|
2079
2260
|
console.log("");
|
|
2080
2261
|
const cmp = compareVersions(VERSION, latest);
|
|
2081
2262
|
if (cmp >= 0) {
|
|
2082
|
-
console.log(
|
|
2263
|
+
console.log(chalk10.green(" \u2713 You're on the latest version."));
|
|
2083
2264
|
console.log("");
|
|
2084
2265
|
return;
|
|
2085
2266
|
}
|
|
2086
|
-
console.log(
|
|
2267
|
+
console.log(chalk10.cyan(` Update available: ${VERSION} \u2192 ${latest}`));
|
|
2087
2268
|
console.log("");
|
|
2088
2269
|
if (opts.check) {
|
|
2089
2270
|
const method2 = detectInstallMethod();
|
|
2090
2271
|
if (method2 === "homebrew") {
|
|
2091
|
-
console.log(` Run: ${
|
|
2272
|
+
console.log(` Run: ${chalk10.cyan("brew upgrade thinkwork-ai/tap/thinkwork")}`);
|
|
2092
2273
|
} else {
|
|
2093
|
-
console.log(` Run: ${
|
|
2274
|
+
console.log(` Run: ${chalk10.cyan(`npm install -g thinkwork-cli@${latest}`)}`);
|
|
2094
2275
|
}
|
|
2095
2276
|
console.log("");
|
|
2096
2277
|
return;
|
|
@@ -2098,21 +2279,193 @@ function registerUpdateCommand(program2) {
|
|
|
2098
2279
|
const method = detectInstallMethod();
|
|
2099
2280
|
const cmd = method === "homebrew" ? "brew upgrade thinkwork-ai/tap/thinkwork" : `npm install -g thinkwork-cli@${latest}`;
|
|
2100
2281
|
console.log(` Installing via ${method}...`);
|
|
2101
|
-
console.log(
|
|
2282
|
+
console.log(chalk10.dim(` $ ${cmd}`));
|
|
2102
2283
|
console.log("");
|
|
2103
2284
|
try {
|
|
2104
|
-
|
|
2285
|
+
execSync8(cmd, { stdio: "inherit", timeout: 12e4 });
|
|
2105
2286
|
console.log("");
|
|
2106
|
-
console.log(
|
|
2287
|
+
console.log(chalk10.green(` \u2713 Upgraded to thinkwork-cli@${latest}`));
|
|
2107
2288
|
} catch {
|
|
2108
2289
|
console.log("");
|
|
2109
|
-
console.log(
|
|
2110
|
-
console.log(` ${
|
|
2290
|
+
console.log(chalk10.red(` Failed to upgrade. Try manually:`));
|
|
2291
|
+
console.log(` ${chalk10.cyan(cmd)}`);
|
|
2111
2292
|
}
|
|
2112
2293
|
console.log("");
|
|
2113
2294
|
});
|
|
2114
2295
|
}
|
|
2115
2296
|
|
|
2297
|
+
// src/commands/user.ts
|
|
2298
|
+
import { spawn as spawn3 } from "child_process";
|
|
2299
|
+
function getTerraformOutput2(cwd, key) {
|
|
2300
|
+
return new Promise((resolve3, reject) => {
|
|
2301
|
+
const proc = spawn3("terraform", ["output", "-raw", key], {
|
|
2302
|
+
cwd,
|
|
2303
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2304
|
+
});
|
|
2305
|
+
let stdout = "";
|
|
2306
|
+
let stderr = "";
|
|
2307
|
+
proc.stdout.on("data", (d) => stdout += d);
|
|
2308
|
+
proc.stderr.on("data", (d) => stderr += d);
|
|
2309
|
+
proc.on("close", (code) => {
|
|
2310
|
+
if (code === 0) resolve3(stdout.trim());
|
|
2311
|
+
else
|
|
2312
|
+
reject(
|
|
2313
|
+
new Error(
|
|
2314
|
+
`terraform output ${key} failed (exit ${code}): ${stderr.trim() || "no stderr"}`
|
|
2315
|
+
)
|
|
2316
|
+
);
|
|
2317
|
+
});
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
function runAwsCognitoReset(userPoolId, username, region) {
|
|
2321
|
+
return new Promise((resolve3) => {
|
|
2322
|
+
const args = [
|
|
2323
|
+
"cognito-idp",
|
|
2324
|
+
"admin-reset-user-password",
|
|
2325
|
+
"--user-pool-id",
|
|
2326
|
+
userPoolId,
|
|
2327
|
+
"--username",
|
|
2328
|
+
username,
|
|
2329
|
+
"--output",
|
|
2330
|
+
"json"
|
|
2331
|
+
];
|
|
2332
|
+
if (region) args.push("--region", region);
|
|
2333
|
+
const proc = spawn3("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2334
|
+
let stdout = "";
|
|
2335
|
+
let stderr = "";
|
|
2336
|
+
proc.stdout.on("data", (d) => stdout += d);
|
|
2337
|
+
proc.stderr.on("data", (d) => stderr += d);
|
|
2338
|
+
proc.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
function registerUserCommand(program2) {
|
|
2342
|
+
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("");
|
|
2367
|
+
try {
|
|
2368
|
+
const result = await apiFetchRaw(
|
|
2369
|
+
api.apiUrl,
|
|
2370
|
+
api.authSecret,
|
|
2371
|
+
`/api/tenants/${encodeURIComponent(opts.tenant)}/members`,
|
|
2372
|
+
{
|
|
2373
|
+
method: "POST",
|
|
2374
|
+
body: JSON.stringify({
|
|
2375
|
+
email: trimmed,
|
|
2376
|
+
name: opts.name ?? null,
|
|
2377
|
+
role: opts.role
|
|
2378
|
+
})
|
|
2379
|
+
}
|
|
2380
|
+
);
|
|
2381
|
+
if (!result.ok) {
|
|
2382
|
+
const msg = result.body?.error || `HTTP ${result.status}`;
|
|
2383
|
+
printError(`Invite failed: ${msg}`);
|
|
2384
|
+
process.exit(1);
|
|
2385
|
+
}
|
|
2386
|
+
if (result.body.alreadyMember) {
|
|
2387
|
+
printWarning(
|
|
2388
|
+
`${trimmed} is already a member of "${opts.tenant}" (role: ${result.body.role}). No email sent.`
|
|
2389
|
+
);
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
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.`
|
|
2394
|
+
);
|
|
2395
|
+
} catch (err) {
|
|
2396
|
+
printError(
|
|
2397
|
+
`Invite failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2398
|
+
);
|
|
2399
|
+
process.exit(1);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
);
|
|
2403
|
+
user.command("reset-password <email>").description(
|
|
2404
|
+
"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(
|
|
2406
|
+
"-r, --region <name>",
|
|
2407
|
+
"AWS region (defaults to AWS CLI default / AWS_REGION)"
|
|
2408
|
+
).action(
|
|
2409
|
+
async (email, opts) => {
|
|
2410
|
+
const stageCheck = validateStage(opts.stage);
|
|
2411
|
+
if (!stageCheck.valid) {
|
|
2412
|
+
printError(stageCheck.error);
|
|
2413
|
+
process.exit(1);
|
|
2414
|
+
}
|
|
2415
|
+
if (!email || !email.includes("@")) {
|
|
2416
|
+
printError(
|
|
2417
|
+
`"${email}" doesn't look like an email address. Pass the user's sign-in email.`
|
|
2418
|
+
);
|
|
2419
|
+
process.exit(1);
|
|
2420
|
+
}
|
|
2421
|
+
printHeader("user reset-password", opts.stage);
|
|
2422
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
2423
|
+
const cwd = resolveTierDir(terraformDir, opts.stage, "app");
|
|
2424
|
+
await ensureInit(cwd);
|
|
2425
|
+
await ensureWorkspace(cwd, opts.stage);
|
|
2426
|
+
let userPoolId;
|
|
2427
|
+
try {
|
|
2428
|
+
userPoolId = await getTerraformOutput2(cwd, "user_pool_id");
|
|
2429
|
+
} catch (err) {
|
|
2430
|
+
printError(
|
|
2431
|
+
`Failed to read user_pool_id from Terraform outputs. Is the stack deployed? ${err instanceof Error ? err.message : String(err)}`
|
|
2432
|
+
);
|
|
2433
|
+
process.exit(1);
|
|
2434
|
+
}
|
|
2435
|
+
if (!userPoolId) {
|
|
2436
|
+
printError(
|
|
2437
|
+
"user_pool_id output is empty \u2014 the stack may not be fully deployed."
|
|
2438
|
+
);
|
|
2439
|
+
process.exit(1);
|
|
2440
|
+
}
|
|
2441
|
+
console.log(` User pool: ${userPoolId}`);
|
|
2442
|
+
console.log(` Email: ${email}`);
|
|
2443
|
+
console.log("");
|
|
2444
|
+
const result = await runAwsCognitoReset(userPoolId, email, opts.region);
|
|
2445
|
+
if (result.code === 0) {
|
|
2446
|
+
printSuccess(
|
|
2447
|
+
`Reset triggered for ${email}. Cognito has emailed a verification code; the user sets a new password on next sign-in.`
|
|
2448
|
+
);
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (result.stderr.includes("UserNotFoundException")) {
|
|
2452
|
+
printError(
|
|
2453
|
+
`No user found with email ${email} in pool ${userPoolId}. Check the address (case-insensitive) or that they've signed up.`
|
|
2454
|
+
);
|
|
2455
|
+
} else if (result.stderr.includes("NotAuthorizedException")) {
|
|
2456
|
+
printError(
|
|
2457
|
+
"Cognito rejected the call \u2014 your AWS credentials may not have cognito-idp:AdminResetUserPassword on this pool."
|
|
2458
|
+
);
|
|
2459
|
+
} else {
|
|
2460
|
+
printError(
|
|
2461
|
+
`admin-reset-user-password failed (exit ${result.code}): ${result.stderr.trim() || "no stderr"}`
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
process.exit(result.code);
|
|
2465
|
+
}
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2116
2469
|
// src/cli.ts
|
|
2117
2470
|
var program = new Command();
|
|
2118
2471
|
program.name("thinkwork").description(
|
|
@@ -2122,10 +2475,14 @@ program.name("thinkwork").description(
|
|
|
2122
2475
|
"AWS profile to use (sets AWS_PROFILE for Terraform and AWS CLI)"
|
|
2123
2476
|
);
|
|
2124
2477
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
2125
|
-
const
|
|
2126
|
-
if (
|
|
2127
|
-
process.env.AWS_PROFILE =
|
|
2478
|
+
const explicit = actionCommand.opts().profile ?? program.opts().profile;
|
|
2479
|
+
if (explicit) {
|
|
2480
|
+
process.env.AWS_PROFILE = explicit;
|
|
2481
|
+
return;
|
|
2128
2482
|
}
|
|
2483
|
+
if (process.env.AWS_PROFILE) return;
|
|
2484
|
+
const fallback = loadCliConfig().defaultProfile;
|
|
2485
|
+
if (fallback) process.env.AWS_PROFILE = fallback;
|
|
2129
2486
|
});
|
|
2130
2487
|
registerLoginCommand(program);
|
|
2131
2488
|
registerInitCommand(program);
|
|
@@ -2140,4 +2497,5 @@ registerConfigCommand(program);
|
|
|
2140
2497
|
registerMcpCommand(program);
|
|
2141
2498
|
registerToolsCommand(program);
|
|
2142
2499
|
registerUpdateCommand(program);
|
|
2500
|
+
registerUserCommand(program);
|
|
2143
2501
|
program.parse();
|