thinkwork-cli 0.12.6 → 0.12.8
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.
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
apiFetch,
|
|
3
|
+
apiFetchRaw,
|
|
4
|
+
getApiEndpoint2 as getApiEndpoint,
|
|
5
|
+
readTfVar,
|
|
6
|
+
resolveApiConfig,
|
|
7
|
+
resolveTfvarsPath
|
|
8
|
+
} from "./chunk-34NMB5AK.js";
|
|
9
|
+
export {
|
|
10
|
+
apiFetch,
|
|
11
|
+
apiFetchRaw,
|
|
12
|
+
getApiEndpoint,
|
|
13
|
+
readTfVar,
|
|
14
|
+
resolveApiConfig,
|
|
15
|
+
resolveTfvarsPath
|
|
16
|
+
};
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/api-client.ts
|
|
8
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
9
|
+
import { execSync as execSync2 } from "child_process";
|
|
10
|
+
|
|
11
|
+
// src/environments.ts
|
|
12
|
+
import {
|
|
13
|
+
existsSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
readdirSync
|
|
18
|
+
} from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
var THINKWORK_HOME = join(homedir(), ".thinkwork");
|
|
22
|
+
var ENVIRONMENTS_DIR = join(THINKWORK_HOME, "environments");
|
|
23
|
+
var ENTERPRISE_DEPLOYMENTS_DIR = join(
|
|
24
|
+
THINKWORK_HOME,
|
|
25
|
+
"enterprise-deployments"
|
|
26
|
+
);
|
|
27
|
+
function ensureDir(dir) {
|
|
28
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
function saveEnvironment(config) {
|
|
31
|
+
ensureDir(ENVIRONMENTS_DIR);
|
|
32
|
+
const envDir = join(ENVIRONMENTS_DIR, config.stage);
|
|
33
|
+
ensureDir(envDir);
|
|
34
|
+
writeFileSync(
|
|
35
|
+
join(envDir, "config.json"),
|
|
36
|
+
JSON.stringify(config, null, 2) + "\n"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
function saveEnterpriseDeployment(config) {
|
|
40
|
+
ensureDir(ENTERPRISE_DEPLOYMENTS_DIR);
|
|
41
|
+
writeFileSync(
|
|
42
|
+
join(ENTERPRISE_DEPLOYMENTS_DIR, `${config.customerSlug}.json`),
|
|
43
|
+
JSON.stringify(config, null, 2) + "\n"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
function loadEnvironment(stage) {
|
|
47
|
+
const configPath = join(ENVIRONMENTS_DIR, stage, "config.json");
|
|
48
|
+
if (!existsSync(configPath)) return null;
|
|
49
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
50
|
+
}
|
|
51
|
+
function listEnvironments() {
|
|
52
|
+
if (!existsSync(ENVIRONMENTS_DIR)) return [];
|
|
53
|
+
return readdirSync(ENVIRONMENTS_DIR).filter((name) => {
|
|
54
|
+
return existsSync(join(ENVIRONMENTS_DIR, name, "config.json"));
|
|
55
|
+
}).map((name) => {
|
|
56
|
+
return JSON.parse(
|
|
57
|
+
readFileSync(join(ENVIRONMENTS_DIR, name, "config.json"), "utf-8")
|
|
58
|
+
);
|
|
59
|
+
}).sort((a, b) => a.stage.localeCompare(b.stage));
|
|
60
|
+
}
|
|
61
|
+
function resolveTerraformDir(stage) {
|
|
62
|
+
const env = loadEnvironment(stage);
|
|
63
|
+
if (env?.terraformDir && existsSync(env.terraformDir)) {
|
|
64
|
+
return env.terraformDir;
|
|
65
|
+
}
|
|
66
|
+
const envVar = process.env.THINKWORK_TERRAFORM_DIR;
|
|
67
|
+
if (envVar && existsSync(envVar)) return envVar;
|
|
68
|
+
const cwdTf = join(process.cwd(), "terraform");
|
|
69
|
+
if (existsSync(join(cwdTf, "main.tf"))) return cwdTf;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/terraform.ts
|
|
74
|
+
import { spawn } from "child_process";
|
|
75
|
+
import { existsSync as existsSync2 } from "fs";
|
|
76
|
+
import path from "path";
|
|
77
|
+
function resolveTierDir(terraformDir, stage, tier) {
|
|
78
|
+
const envDir = path.join(terraformDir, "environments", stage, tier);
|
|
79
|
+
if (existsSync2(envDir)) {
|
|
80
|
+
return envDir;
|
|
81
|
+
}
|
|
82
|
+
const greenfield = path.join(terraformDir, "examples", "greenfield");
|
|
83
|
+
if (existsSync2(greenfield)) {
|
|
84
|
+
return greenfield;
|
|
85
|
+
}
|
|
86
|
+
const flat = path.join(terraformDir);
|
|
87
|
+
if (existsSync2(path.join(flat, "main.tf"))) {
|
|
88
|
+
return flat;
|
|
89
|
+
}
|
|
90
|
+
const cwdTf = path.join(process.cwd(), "terraform");
|
|
91
|
+
if (existsSync2(path.join(cwdTf, "main.tf"))) {
|
|
92
|
+
return cwdTf;
|
|
93
|
+
}
|
|
94
|
+
return terraformDir;
|
|
95
|
+
}
|
|
96
|
+
async function ensureWorkspace(cwd, stage) {
|
|
97
|
+
const list = await runTerraformRaw(cwd, ["workspace", "list"]);
|
|
98
|
+
const workspaces = list.split("\n").map((l) => l.replace("*", "").trim()).filter(Boolean);
|
|
99
|
+
if (!workspaces.includes(stage)) {
|
|
100
|
+
await runTerraformRaw(cwd, ["workspace", "new", stage]);
|
|
101
|
+
} else {
|
|
102
|
+
await runTerraformRaw(cwd, ["workspace", "select", stage]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function runTerraformRaw(cwd, args) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const proc = spawn("terraform", args, {
|
|
108
|
+
cwd,
|
|
109
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
110
|
+
});
|
|
111
|
+
let stdout = "";
|
|
112
|
+
let stderr = "";
|
|
113
|
+
proc.stdout.on("data", (d) => stdout += d);
|
|
114
|
+
proc.stderr.on("data", (d) => stderr += d);
|
|
115
|
+
proc.on("close", (code) => {
|
|
116
|
+
if (code === 0) resolve(stdout);
|
|
117
|
+
else reject(new Error(`terraform ${args.join(" ")} failed (exit ${code}): ${stderr}`));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function runTerraform(cwd, args) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
console.log(`
|
|
124
|
+
\u2192 terraform ${args.join(" ")}
|
|
125
|
+
`);
|
|
126
|
+
const proc = spawn("terraform", args, {
|
|
127
|
+
cwd,
|
|
128
|
+
stdio: "inherit"
|
|
129
|
+
});
|
|
130
|
+
proc.on("close", (code) => resolve(code ?? 1));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function ensureInit(cwd) {
|
|
134
|
+
const dotTerraform = path.join(cwd, ".terraform");
|
|
135
|
+
if (!existsSync2(dotTerraform)) {
|
|
136
|
+
const code = await runTerraform(cwd, ["init"]);
|
|
137
|
+
if (code !== 0) {
|
|
138
|
+
throw new Error("terraform init failed");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/aws-discovery.ts
|
|
144
|
+
import { execSync } from "child_process";
|
|
145
|
+
function runAws(cmd) {
|
|
146
|
+
try {
|
|
147
|
+
return execSync(`aws ${cmd}`, {
|
|
148
|
+
encoding: "utf-8",
|
|
149
|
+
timeout: 15e3,
|
|
150
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
151
|
+
}).trim();
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function listDeployedStages(region) {
|
|
157
|
+
const raw = runAws(
|
|
158
|
+
`lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
|
|
159
|
+
);
|
|
160
|
+
if (!raw) return [];
|
|
161
|
+
try {
|
|
162
|
+
const functions = JSON.parse(raw);
|
|
163
|
+
const stages = /* @__PURE__ */ new Set();
|
|
164
|
+
for (const fn of functions) {
|
|
165
|
+
const m = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
|
|
166
|
+
if (m) stages.add(m[1]);
|
|
167
|
+
}
|
|
168
|
+
return [...stages].sort();
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function getApiEndpoint(stage, region) {
|
|
174
|
+
const raw = runAws(
|
|
175
|
+
`apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
|
|
176
|
+
);
|
|
177
|
+
return raw && raw !== "None" ? raw : null;
|
|
178
|
+
}
|
|
179
|
+
function getApiAuthSecretFromLambda(stage, region) {
|
|
180
|
+
const raw = runAws(
|
|
181
|
+
`lambda get-function-configuration --function-name thinkwork-${stage}-api-tenants --region ${region} --query "Environment.Variables.API_AUTH_SECRET" --output text`
|
|
182
|
+
);
|
|
183
|
+
return raw && raw !== "None" ? raw : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/ui.ts
|
|
187
|
+
import chalk from "chalk";
|
|
188
|
+
import ora from "ora";
|
|
189
|
+
var TIER_LABELS = {
|
|
190
|
+
foundation: "Foundation",
|
|
191
|
+
data: "Data",
|
|
192
|
+
app: "App"
|
|
193
|
+
};
|
|
194
|
+
function printHeader(command, stage, identity) {
|
|
195
|
+
console.log("");
|
|
196
|
+
console.log(chalk.bold.cyan(" \u2B21 Thinkwork") + chalk.dim(` \u2014 ${command}`));
|
|
197
|
+
console.log(chalk.dim(` Stage: ${chalk.white(stage)}`));
|
|
198
|
+
if (identity) {
|
|
199
|
+
console.log(chalk.dim(` AWS: ${chalk.white(identity.account)} / ${chalk.white(identity.region)}`));
|
|
200
|
+
}
|
|
201
|
+
console.log("");
|
|
202
|
+
}
|
|
203
|
+
function printTierHeader(tier, index, total) {
|
|
204
|
+
const label = TIER_LABELS[tier] ?? tier;
|
|
205
|
+
const progress = chalk.dim(`[${index + 1}/${total}]`);
|
|
206
|
+
console.log(` ${progress} ${chalk.bold(label)}`);
|
|
207
|
+
}
|
|
208
|
+
function printSuccess(message) {
|
|
209
|
+
console.log(`
|
|
210
|
+
${chalk.green("\u2713")} ${chalk.bold(message)}`);
|
|
211
|
+
}
|
|
212
|
+
function printError(message) {
|
|
213
|
+
console.log(`
|
|
214
|
+
${chalk.red("\u2717")} ${chalk.bold.red(message)}`);
|
|
215
|
+
}
|
|
216
|
+
function printMissingApiSessionError(stage, hasSession) {
|
|
217
|
+
if (!hasSession) {
|
|
218
|
+
printError(`No API session for stage "${stage}".`);
|
|
219
|
+
console.log("");
|
|
220
|
+
console.log(` ${chalk.bold("To fix:")} thinkwork login --stage ${stage}`);
|
|
221
|
+
console.log(
|
|
222
|
+
chalk.dim(
|
|
223
|
+
` (the deploy-side \`thinkwork login\` only configures an AWS profile \u2014
|
|
224
|
+
it does NOT open an API session.)`
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
console.log("");
|
|
228
|
+
} else {
|
|
229
|
+
printError(`Session for stage "${stage}" has no tenant cached.`);
|
|
230
|
+
console.log("");
|
|
231
|
+
console.log(` ${chalk.bold("To fix:")} thinkwork login --stage ${stage}`);
|
|
232
|
+
console.log(
|
|
233
|
+
chalk.dim(
|
|
234
|
+
` Or pass --tenant <slug>, or set THINKWORK_TENANT.`
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
console.log("");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function printWarning(message) {
|
|
241
|
+
console.log(` ${chalk.yellow("\u26A0")} ${message}`);
|
|
242
|
+
}
|
|
243
|
+
function printSummary(command, stage, tiers, startTime) {
|
|
244
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
245
|
+
console.log("");
|
|
246
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
247
|
+
console.log(` ${chalk.bold("Command:")} ${command}`);
|
|
248
|
+
console.log(` ${chalk.bold("Stage:")} ${stage}`);
|
|
249
|
+
console.log(` ${chalk.bold("Tiers:")} ${tiers.map((t) => TIER_LABELS[t] ?? t).join(" \u2192 ")}`);
|
|
250
|
+
console.log(` ${chalk.bold("Time:")} ${elapsed}s`);
|
|
251
|
+
console.log(chalk.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"));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/api-client.ts
|
|
255
|
+
function readTfVar(tfvarsPath, key) {
|
|
256
|
+
if (!existsSync3(tfvarsPath)) return null;
|
|
257
|
+
const content = readFileSync2(tfvarsPath, "utf-8");
|
|
258
|
+
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
|
|
259
|
+
return match ? match[1] : null;
|
|
260
|
+
}
|
|
261
|
+
function resolveTfvarsPath(stage) {
|
|
262
|
+
const tfDir = resolveTerraformDir(stage);
|
|
263
|
+
if (tfDir) {
|
|
264
|
+
const direct = `${tfDir}/terraform.tfvars`;
|
|
265
|
+
if (existsSync3(direct)) return direct;
|
|
266
|
+
}
|
|
267
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
268
|
+
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
269
|
+
return `${cwd}/terraform.tfvars`;
|
|
270
|
+
}
|
|
271
|
+
function getApiEndpoint2(stage, region) {
|
|
272
|
+
try {
|
|
273
|
+
const raw = execSync2(
|
|
274
|
+
`aws apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`,
|
|
275
|
+
{ encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
|
|
276
|
+
).trim();
|
|
277
|
+
return raw && raw !== "None" ? raw : null;
|
|
278
|
+
} catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function apiFetch(apiUrl, authSecret, path2, options = {}, extraHeaders = {}) {
|
|
283
|
+
const res = await fetch(`${apiUrl}${path2}`, {
|
|
284
|
+
...options,
|
|
285
|
+
headers: {
|
|
286
|
+
"Content-Type": "application/json",
|
|
287
|
+
Authorization: `Bearer ${authSecret}`,
|
|
288
|
+
...extraHeaders,
|
|
289
|
+
...options.headers
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
if (!res.ok) {
|
|
293
|
+
const body = await res.json().catch(() => ({}));
|
|
294
|
+
throw new Error(body.error || `HTTP ${res.status}`);
|
|
295
|
+
}
|
|
296
|
+
return res.json();
|
|
297
|
+
}
|
|
298
|
+
async function apiFetchRaw(apiUrl, authSecret, path2, options = {}, extraHeaders = {}) {
|
|
299
|
+
const res = await fetch(`${apiUrl}${path2}`, {
|
|
300
|
+
...options,
|
|
301
|
+
headers: {
|
|
302
|
+
"Content-Type": "application/json",
|
|
303
|
+
Authorization: `Bearer ${authSecret}`,
|
|
304
|
+
...extraHeaders,
|
|
305
|
+
...options.headers
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
const body = await res.json().catch(() => ({}));
|
|
309
|
+
return { ok: res.ok, status: res.status, body };
|
|
310
|
+
}
|
|
311
|
+
function resolveApiConfig(stage, regionOverride) {
|
|
312
|
+
const tfvarsPath = resolveTfvarsPath(stage);
|
|
313
|
+
const tfAuthSecret = readTfVar(tfvarsPath, "api_auth_secret");
|
|
314
|
+
const tfRegion = readTfVar(tfvarsPath, "region");
|
|
315
|
+
const region = regionOverride || tfRegion || "us-east-1";
|
|
316
|
+
const apiUrl = getApiEndpoint2(stage, region);
|
|
317
|
+
if (!apiUrl) {
|
|
318
|
+
printError(
|
|
319
|
+
`Cannot discover API endpoint for stage "${stage}" in ${region}. Is the stack deployed?`
|
|
320
|
+
);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const authSecret = tfAuthSecret ?? getApiAuthSecretFromLambda(stage, region);
|
|
324
|
+
if (!authSecret) {
|
|
325
|
+
printError(
|
|
326
|
+
`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.`
|
|
327
|
+
);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return { apiUrl, authSecret };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export {
|
|
334
|
+
__export,
|
|
335
|
+
resolveTierDir,
|
|
336
|
+
ensureWorkspace,
|
|
337
|
+
runTerraform,
|
|
338
|
+
ensureInit,
|
|
339
|
+
printHeader,
|
|
340
|
+
printTierHeader,
|
|
341
|
+
printSuccess,
|
|
342
|
+
printError,
|
|
343
|
+
printMissingApiSessionError,
|
|
344
|
+
printWarning,
|
|
345
|
+
printSummary,
|
|
346
|
+
listDeployedStages,
|
|
347
|
+
getApiEndpoint,
|
|
348
|
+
saveEnvironment,
|
|
349
|
+
saveEnterpriseDeployment,
|
|
350
|
+
loadEnvironment,
|
|
351
|
+
listEnvironments,
|
|
352
|
+
resolveTerraformDir,
|
|
353
|
+
readTfVar,
|
|
354
|
+
resolveTfvarsPath,
|
|
355
|
+
getApiEndpoint2,
|
|
356
|
+
apiFetch,
|
|
357
|
+
apiFetchRaw,
|
|
358
|
+
resolveApiConfig
|
|
359
|
+
};
|