@absolutejs/cli 0.1.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/LICENSE +93 -0
- package/README.md +136 -0
- package/bin/absolutejs.js +3 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +660 -0
- package/dist/cli.js.map +15 -0
- package/dist/commands/deploy.d.ts +20 -0
- package/dist/commands/env.d.ts +20 -0
- package/dist/commands/secrets.d.ts +21 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +10 -0
- package/dist/loadConfig.d.ts +12 -0
- package/dist/utils/output.d.ts +10 -0
- package/package.json +69 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/loadConfig.ts
|
|
3
|
+
import { existsSync, statSync } from "fs";
|
|
4
|
+
import { dirname, join, resolve } from "path";
|
|
5
|
+
var CONFIG_NAMES = [
|
|
6
|
+
"absolutejs.config.ts",
|
|
7
|
+
"absolutejs.config.mts",
|
|
8
|
+
"absolutejs.config.js",
|
|
9
|
+
"absolutejs.config.mjs"
|
|
10
|
+
];
|
|
11
|
+
var findConfigPath = (startDir) => {
|
|
12
|
+
let dir = resolve(startDir);
|
|
13
|
+
for (;; ) {
|
|
14
|
+
for (const name of CONFIG_NAMES) {
|
|
15
|
+
const candidate = join(dir, name);
|
|
16
|
+
try {
|
|
17
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
} catch {}
|
|
21
|
+
}
|
|
22
|
+
const parent = dirname(dir);
|
|
23
|
+
if (parent === dir)
|
|
24
|
+
return;
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var loadConfig = async (startDir = process.cwd()) => {
|
|
29
|
+
const path = findConfigPath(startDir);
|
|
30
|
+
if (path === undefined) {
|
|
31
|
+
throw new Error(`[absolutejs] no config file found.
|
|
32
|
+
` + `Looked for: ${CONFIG_NAMES.join(", ")} (walked up from ${startDir})
|
|
33
|
+
|
|
34
|
+
` + `Create an absolutejs.config.ts with:
|
|
35
|
+
|
|
36
|
+
` + ` import { defineConfig } from '@absolutejs/cli';
|
|
37
|
+
` + ` export default defineConfig({
|
|
38
|
+
` + ` secrets: /* your SecretBroker */,
|
|
39
|
+
` + ` deployments: [],
|
|
40
|
+
` + ` });`);
|
|
41
|
+
}
|
|
42
|
+
let mod;
|
|
43
|
+
try {
|
|
44
|
+
mod = await import(path);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(`[absolutejs] failed to load ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
}
|
|
48
|
+
const config = mod.default;
|
|
49
|
+
if (config === undefined || typeof config !== "object") {
|
|
50
|
+
throw new Error(`[absolutejs] ${path} must \`export default defineConfig({...})\``);
|
|
51
|
+
}
|
|
52
|
+
return { config, path };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/utils/output.ts
|
|
56
|
+
var renderTable = (headers, rows) => {
|
|
57
|
+
if (rows.length === 0) {
|
|
58
|
+
return `${headers.join(" ")}
|
|
59
|
+
(no rows)
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
const widths = headers.map((header, columnIndex) => {
|
|
63
|
+
const cells = rows.map((row) => row[columnIndex] ?? "");
|
|
64
|
+
return Math.max(header.length, ...cells.map((cell) => cell.length));
|
|
65
|
+
});
|
|
66
|
+
const formatRow = (row) => row.map((cell, columnIndex) => (cell ?? "").padEnd(widths[columnIndex] ?? 0)).join(" ").trimEnd();
|
|
67
|
+
const lines = [formatRow(headers), formatRow(widths.map((w) => "-".repeat(w)))];
|
|
68
|
+
for (const row of rows)
|
|
69
|
+
lines.push(formatRow(row));
|
|
70
|
+
return `${lines.join(`
|
|
71
|
+
`)}
|
|
72
|
+
`;
|
|
73
|
+
};
|
|
74
|
+
var writeOut = (text) => {
|
|
75
|
+
process.stdout.write(text.endsWith(`
|
|
76
|
+
`) ? text : `${text}
|
|
77
|
+
`);
|
|
78
|
+
};
|
|
79
|
+
var writeJson = (value) => {
|
|
80
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
81
|
+
`);
|
|
82
|
+
};
|
|
83
|
+
var writeErr = (text) => {
|
|
84
|
+
process.stderr.write(text.endsWith(`
|
|
85
|
+
`) ? text : `${text}
|
|
86
|
+
`);
|
|
87
|
+
};
|
|
88
|
+
var formatRelativeTime = (msAgo) => {
|
|
89
|
+
if (msAgo < 60000)
|
|
90
|
+
return `${Math.round(msAgo / 1000)}s ago`;
|
|
91
|
+
if (msAgo < 3600000)
|
|
92
|
+
return `${Math.round(msAgo / 60000)}m ago`;
|
|
93
|
+
if (msAgo < 86400000)
|
|
94
|
+
return `${Math.round(msAgo / 3600000)}h ago`;
|
|
95
|
+
return `${Math.round(msAgo / 86400000)}d ago`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/commands/secrets.ts
|
|
99
|
+
var requireBroker = (config) => {
|
|
100
|
+
if (config.secrets === undefined) {
|
|
101
|
+
throw new Error("config.secrets is not set \u2014 `secrets` verbs need a SecretBroker");
|
|
102
|
+
}
|
|
103
|
+
return config.secrets;
|
|
104
|
+
};
|
|
105
|
+
var requireAdapter = (config) => {
|
|
106
|
+
if (config.secretAdapter === undefined) {
|
|
107
|
+
throw new Error("config.secretAdapter is not set \u2014 pass the SecretAdapter you used to build the broker");
|
|
108
|
+
}
|
|
109
|
+
return config.secretAdapter;
|
|
110
|
+
};
|
|
111
|
+
var runSecrets = async (config, args, mode) => {
|
|
112
|
+
const { verb, positional, flags } = args;
|
|
113
|
+
switch (verb) {
|
|
114
|
+
case "list": {
|
|
115
|
+
const adapter = requireAdapter(config);
|
|
116
|
+
const broker = requireBroker(config);
|
|
117
|
+
if (adapter.list === undefined) {
|
|
118
|
+
throw new Error("the configured SecretAdapter does not implement `list()`");
|
|
119
|
+
}
|
|
120
|
+
const names = await adapter.list();
|
|
121
|
+
const rows = [];
|
|
122
|
+
for (const name of names.sort()) {
|
|
123
|
+
const value = await adapter.fetch(name);
|
|
124
|
+
const fingerprint = value === null ? "(empty)" : broker.fingerprint(value);
|
|
125
|
+
rows.push([name, fingerprint]);
|
|
126
|
+
}
|
|
127
|
+
if (mode === "json") {
|
|
128
|
+
writeJson(rows.map(([name, fingerprint]) => ({ fingerprint, name })));
|
|
129
|
+
} else {
|
|
130
|
+
writeOut(renderTable(["name", "fingerprint"], rows));
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
case "get": {
|
|
135
|
+
const broker = requireBroker(config);
|
|
136
|
+
const name = positional[0];
|
|
137
|
+
if (name === undefined) {
|
|
138
|
+
throw new Error("usage: secrets get <name> [--show]");
|
|
139
|
+
}
|
|
140
|
+
const resolved = await broker.resolve(name);
|
|
141
|
+
if (resolved === null) {
|
|
142
|
+
writeErr(`(no value for ${name})`);
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
if (mode === "json") {
|
|
146
|
+
writeJson({
|
|
147
|
+
fingerprint: resolved.fingerprint,
|
|
148
|
+
name,
|
|
149
|
+
value: flags.show === true ? resolved.value : "(redacted; pass --show)"
|
|
150
|
+
});
|
|
151
|
+
} else if (flags.show === true) {
|
|
152
|
+
writeOut(resolved.value);
|
|
153
|
+
} else {
|
|
154
|
+
writeOut(`${name}: fingerprint=${resolved.fingerprint} (pass --show to print plaintext)`);
|
|
155
|
+
}
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
case "set": {
|
|
159
|
+
const adapter = requireAdapter(config);
|
|
160
|
+
if (adapter.put === undefined) {
|
|
161
|
+
throw new Error("the configured SecretAdapter does not implement `put()`");
|
|
162
|
+
}
|
|
163
|
+
const pair = positional[0];
|
|
164
|
+
if (pair === undefined) {
|
|
165
|
+
throw new Error("usage: secrets set <NAME>=<value>");
|
|
166
|
+
}
|
|
167
|
+
const eq = pair.indexOf("=");
|
|
168
|
+
if (eq <= 0) {
|
|
169
|
+
throw new Error("usage: secrets set <NAME>=<value> (missing `=`)");
|
|
170
|
+
}
|
|
171
|
+
const name = pair.slice(0, eq);
|
|
172
|
+
const value = pair.slice(eq + 1);
|
|
173
|
+
await adapter.put(name, value);
|
|
174
|
+
if (mode === "json") {
|
|
175
|
+
writeJson({ name, set: true });
|
|
176
|
+
} else {
|
|
177
|
+
writeOut(`set ${name}`);
|
|
178
|
+
}
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
case "rotate": {
|
|
182
|
+
const broker = requireBroker(config);
|
|
183
|
+
const name = positional[0];
|
|
184
|
+
if (name === undefined) {
|
|
185
|
+
throw new Error("usage: secrets rotate <name>");
|
|
186
|
+
}
|
|
187
|
+
const rotated = await broker.rotate(name);
|
|
188
|
+
if (mode === "json") {
|
|
189
|
+
writeJson({
|
|
190
|
+
fingerprint: rotated.fingerprint,
|
|
191
|
+
name,
|
|
192
|
+
rotated: true
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
writeOut(`rotated ${name} (new fingerprint: ${rotated.fingerprint})`);
|
|
196
|
+
}
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
default:
|
|
200
|
+
throw new Error(`unknown secrets verb: "${verb}". try: list | get | set | rotate`);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/commands/env.ts
|
|
205
|
+
var KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
206
|
+
var NEEDS_QUOTING = /[\s"'`$\\#&|;<>(){}*?!]/;
|
|
207
|
+
var validateKey = (key) => {
|
|
208
|
+
if (!KEY_PATTERN.test(key)) {
|
|
209
|
+
throw new Error(`invalid env key "${key}" \u2014 must match /^[A-Z_][A-Z0-9_]*$/`);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
var serializeLine = (key, value) => {
|
|
213
|
+
validateKey(key);
|
|
214
|
+
if (value.includes(`
|
|
215
|
+
`) || value.includes("\r")) {
|
|
216
|
+
throw new Error(`value for "${key}" contains a newline \u2014 env files cannot represent multi-line values`);
|
|
217
|
+
}
|
|
218
|
+
if (NEEDS_QUOTING.test(value) || value.startsWith("=") || value === "") {
|
|
219
|
+
const escaped = value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"");
|
|
220
|
+
return `${key}="${escaped}"`;
|
|
221
|
+
}
|
|
222
|
+
return `${key}=${value}`;
|
|
223
|
+
};
|
|
224
|
+
var serializeEnvFile = (values) => {
|
|
225
|
+
const lines = [];
|
|
226
|
+
for (const key of Object.keys(values).sort()) {
|
|
227
|
+
const value = values[key];
|
|
228
|
+
if (value === undefined)
|
|
229
|
+
continue;
|
|
230
|
+
lines.push(serializeLine(key, value));
|
|
231
|
+
}
|
|
232
|
+
return `${lines.join(`
|
|
233
|
+
`)}
|
|
234
|
+
`;
|
|
235
|
+
};
|
|
236
|
+
var unquoteValue = (raw) => {
|
|
237
|
+
if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
|
|
238
|
+
return raw.slice(1, -1).replaceAll("\\\"", '"').replaceAll("\\\\", "\\");
|
|
239
|
+
}
|
|
240
|
+
if (raw.length >= 2 && raw.startsWith("'") && raw.endsWith("'")) {
|
|
241
|
+
return raw.slice(1, -1);
|
|
242
|
+
}
|
|
243
|
+
return raw;
|
|
244
|
+
};
|
|
245
|
+
var parseEnvFile = (text) => {
|
|
246
|
+
const result = {};
|
|
247
|
+
for (const rawLine of text.split(`
|
|
248
|
+
`)) {
|
|
249
|
+
const line = rawLine.trim();
|
|
250
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
251
|
+
continue;
|
|
252
|
+
const eq = line.indexOf("=");
|
|
253
|
+
if (eq <= 0)
|
|
254
|
+
continue;
|
|
255
|
+
const key = line.slice(0, eq).trim();
|
|
256
|
+
const value = unquoteValue(line.slice(eq + 1).trim());
|
|
257
|
+
if (KEY_PATTERN.test(key))
|
|
258
|
+
result[key] = value;
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
};
|
|
262
|
+
var findDeployment = (config, stage) => {
|
|
263
|
+
const match = (config.deployments ?? []).find((d) => d.name === stage);
|
|
264
|
+
if (match === undefined) {
|
|
265
|
+
const names = (config.deployments ?? []).map((d) => d.name);
|
|
266
|
+
throw new Error(`unknown deployment "${stage}". configured: ${names.length > 0 ? names.join(", ") : "(none)"}`);
|
|
267
|
+
}
|
|
268
|
+
return match;
|
|
269
|
+
};
|
|
270
|
+
var resolveValuesForDeployment = async (config, deployment) => {
|
|
271
|
+
const merged = {};
|
|
272
|
+
for (const [key, value] of Object.entries(deployment.extras ?? {})) {
|
|
273
|
+
validateKey(key);
|
|
274
|
+
merged[key] = value;
|
|
275
|
+
}
|
|
276
|
+
if ((deployment.secretNames ?? []).length > 0) {
|
|
277
|
+
if (config.secrets === undefined) {
|
|
278
|
+
throw new Error(`deployment "${deployment.name}" declares secretNames but config.secrets is not set`);
|
|
279
|
+
}
|
|
280
|
+
for (const name of deployment.secretNames ?? []) {
|
|
281
|
+
const resolved = await config.secrets.resolve(name);
|
|
282
|
+
if (resolved === null) {
|
|
283
|
+
throw new Error(`secret "${name}" not found in broker (deployment: ${deployment.name})`);
|
|
284
|
+
}
|
|
285
|
+
if (merged[name] !== undefined) {
|
|
286
|
+
throw new Error(`"${name}" defined in BOTH extras and secretNames for ${deployment.name}`);
|
|
287
|
+
}
|
|
288
|
+
merged[name] = resolved.value;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return merged;
|
|
292
|
+
};
|
|
293
|
+
var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
294
|
+
var readRemoteFile = async (deployment) => {
|
|
295
|
+
const target = await deployment.target();
|
|
296
|
+
const sentinel = "__ABS_DEPLOY_ENV_ABSENT__";
|
|
297
|
+
const result = await target.exec(`if [ -f ${shellQuote(deployment.remotePath)} ]; then cat ${shellQuote(deployment.remotePath)}; else echo ${sentinel}; fi`);
|
|
298
|
+
if (result.exitCode !== 0) {
|
|
299
|
+
throw new Error(`failed to read ${deployment.remotePath}: exit ${result.exitCode}: ${result.stderr || result.stdout}`);
|
|
300
|
+
}
|
|
301
|
+
if (result.stdout.trim() === sentinel)
|
|
302
|
+
return;
|
|
303
|
+
return result.stdout;
|
|
304
|
+
};
|
|
305
|
+
var writeRemoteFile = async (deployment, contents) => {
|
|
306
|
+
const target = await deployment.target();
|
|
307
|
+
const tempPath = `${deployment.remotePath}.new.${Math.floor(Date.now() / 1000)}`;
|
|
308
|
+
const dir = deployment.remotePath.split("/").slice(0, -1).join("/") || "/";
|
|
309
|
+
const mkdir = await target.exec(`mkdir -p ${shellQuote(dir)}`);
|
|
310
|
+
if (mkdir.exitCode !== 0) {
|
|
311
|
+
throw new Error(`mkdir ${dir} failed: ${mkdir.stderr || mkdir.stdout}`);
|
|
312
|
+
}
|
|
313
|
+
const write = await target.exec(`cat > ${shellQuote(tempPath)}`, {
|
|
314
|
+
stdin: contents
|
|
315
|
+
});
|
|
316
|
+
if (write.exitCode !== 0) {
|
|
317
|
+
throw new Error(`write to ${tempPath} failed: ${write.stderr || write.stdout}`);
|
|
318
|
+
}
|
|
319
|
+
const mode = deployment.mode ?? "600";
|
|
320
|
+
const chmod = await target.exec(`chmod ${shellQuote(mode)} ${shellQuote(tempPath)}`);
|
|
321
|
+
if (chmod.exitCode !== 0) {
|
|
322
|
+
throw new Error(`chmod failed: ${chmod.stderr || chmod.stdout}`);
|
|
323
|
+
}
|
|
324
|
+
if (deployment.owner !== undefined) {
|
|
325
|
+
const chown = await target.exec(`chown ${shellQuote(deployment.owner)} ${shellQuote(tempPath)}`);
|
|
326
|
+
if (chown.exitCode !== 0) {
|
|
327
|
+
throw new Error(`chown failed: ${chown.stderr || chown.stdout}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const mv = await target.exec(`mv ${shellQuote(tempPath)} ${shellQuote(deployment.remotePath)}`);
|
|
331
|
+
if (mv.exitCode !== 0) {
|
|
332
|
+
throw new Error(`mv failed: ${mv.stderr || mv.stdout}`);
|
|
333
|
+
}
|
|
334
|
+
if (deployment.reload !== undefined) {
|
|
335
|
+
const reload = await target.exec(deployment.reload);
|
|
336
|
+
if (reload.exitCode !== 0) {
|
|
337
|
+
throw new Error(`reload command failed: ${reload.stderr || reload.stdout}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
var runEnv = async (config, args, mode) => {
|
|
342
|
+
const { verb, positional, flags } = args;
|
|
343
|
+
const stage = positional[0];
|
|
344
|
+
if (stage === undefined) {
|
|
345
|
+
throw new Error(`usage: env ${verb} <stage>`);
|
|
346
|
+
}
|
|
347
|
+
const deployment = findDeployment(config, stage);
|
|
348
|
+
switch (verb) {
|
|
349
|
+
case "pull": {
|
|
350
|
+
const remoteText = await readRemoteFile(deployment);
|
|
351
|
+
if (remoteText === undefined) {
|
|
352
|
+
if (mode === "json")
|
|
353
|
+
writeJson({ exists: false });
|
|
354
|
+
else
|
|
355
|
+
writeErr(`(no env file at ${deployment.remotePath})`);
|
|
356
|
+
return 2;
|
|
357
|
+
}
|
|
358
|
+
if (mode === "json") {
|
|
359
|
+
writeJson({
|
|
360
|
+
exists: true,
|
|
361
|
+
path: deployment.remotePath,
|
|
362
|
+
values: parseEnvFile(remoteText)
|
|
363
|
+
});
|
|
364
|
+
} else {
|
|
365
|
+
writeOut(remoteText);
|
|
366
|
+
}
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
case "push": {
|
|
370
|
+
const resolved = await resolveValuesForDeployment(config, deployment);
|
|
371
|
+
const next = serializeEnvFile(resolved);
|
|
372
|
+
await writeRemoteFile(deployment, next);
|
|
373
|
+
const message = `pushed ${Object.keys(resolved).length} keys to ${stage}:${deployment.remotePath}`;
|
|
374
|
+
if (mode === "json") {
|
|
375
|
+
writeJson({
|
|
376
|
+
keys: Object.keys(resolved).sort(),
|
|
377
|
+
path: deployment.remotePath,
|
|
378
|
+
pushed: true,
|
|
379
|
+
stage
|
|
380
|
+
});
|
|
381
|
+
} else {
|
|
382
|
+
writeOut(message);
|
|
383
|
+
}
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
case "diff": {
|
|
387
|
+
const remoteText = await readRemoteFile(deployment);
|
|
388
|
+
const remote = remoteText === undefined ? {} : parseEnvFile(remoteText);
|
|
389
|
+
const next = await resolveValuesForDeployment(config, deployment);
|
|
390
|
+
const allKeys = [
|
|
391
|
+
...new Set([...Object.keys(remote), ...Object.keys(next)])
|
|
392
|
+
].sort();
|
|
393
|
+
const rows = [];
|
|
394
|
+
for (const key of allKeys) {
|
|
395
|
+
const before = remote[key];
|
|
396
|
+
const after = next[key];
|
|
397
|
+
if (before === undefined) {
|
|
398
|
+
rows.push({
|
|
399
|
+
detail: "(new)",
|
|
400
|
+
key,
|
|
401
|
+
status: "added"
|
|
402
|
+
});
|
|
403
|
+
} else if (after === undefined) {
|
|
404
|
+
rows.push({
|
|
405
|
+
detail: "(removed)",
|
|
406
|
+
key,
|
|
407
|
+
status: "removed"
|
|
408
|
+
});
|
|
409
|
+
} else if (before === after) {
|
|
410
|
+
if (flags.all === true) {
|
|
411
|
+
rows.push({ detail: "(unchanged)", key, status: "same" });
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
rows.push({
|
|
415
|
+
detail: `before \u2260 after`,
|
|
416
|
+
key,
|
|
417
|
+
status: "changed"
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (mode === "json") {
|
|
422
|
+
writeJson({ diff: rows, stage });
|
|
423
|
+
} else {
|
|
424
|
+
if (rows.length === 0) {
|
|
425
|
+
writeOut(`no differences (${allKeys.length} keys match)`);
|
|
426
|
+
} else {
|
|
427
|
+
writeOut(renderTable(["key", "status", "detail"], rows.map((r) => [r.key, r.status, r.detail])));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return 0;
|
|
431
|
+
}
|
|
432
|
+
default:
|
|
433
|
+
throw new Error(`unknown env verb: "${verb}". try: pull | push | diff`);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/commands/deploy.ts
|
|
438
|
+
var findDeployment2 = (config, stage) => {
|
|
439
|
+
const match = (config.deployments ?? []).find((d) => d.name === stage);
|
|
440
|
+
if (match === undefined) {
|
|
441
|
+
const names = (config.deployments ?? []).map((d) => d.name);
|
|
442
|
+
throw new Error(`unknown deployment "${stage}". configured: ${names.length > 0 ? names.join(", ") : "(none)"}`);
|
|
443
|
+
}
|
|
444
|
+
return match;
|
|
445
|
+
};
|
|
446
|
+
var requireDeployer = async (deployment) => {
|
|
447
|
+
if (deployment.deployer === undefined) {
|
|
448
|
+
throw new Error(`deployment "${deployment.name}" has no deployer() factory in config`);
|
|
449
|
+
}
|
|
450
|
+
return deployment.deployer();
|
|
451
|
+
};
|
|
452
|
+
var runDeploy = async (config, args, mode) => {
|
|
453
|
+
const { verb, positional, flags } = args;
|
|
454
|
+
const stage = positional[0];
|
|
455
|
+
if (stage === undefined) {
|
|
456
|
+
throw new Error(`usage: deploy ${verb} <stage>`);
|
|
457
|
+
}
|
|
458
|
+
const deployment = findDeployment2(config, stage);
|
|
459
|
+
const deployer = await requireDeployer(deployment);
|
|
460
|
+
switch (verb) {
|
|
461
|
+
case "releases": {
|
|
462
|
+
if (deployer.listReleases === undefined) {
|
|
463
|
+
throw new Error("the deployer for this stage does not implement listReleases()");
|
|
464
|
+
}
|
|
465
|
+
const releases = await deployer.listReleases();
|
|
466
|
+
if (mode === "json") {
|
|
467
|
+
writeJson({ releases, stage });
|
|
468
|
+
} else {
|
|
469
|
+
const rows = releases.map((release) => [
|
|
470
|
+
release.active === true ? "*" : " ",
|
|
471
|
+
release.id,
|
|
472
|
+
new Date(release.at).toISOString(),
|
|
473
|
+
formatRelativeTime(Date.now() - release.at)
|
|
474
|
+
]);
|
|
475
|
+
writeOut(renderTable(["", "id", "at", "age"], rows));
|
|
476
|
+
}
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
case "status": {
|
|
480
|
+
const releases = deployer.listReleases ? await deployer.listReleases() : [];
|
|
481
|
+
const currentId = deployer.currentReleaseId ? await deployer.currentReleaseId() : undefined;
|
|
482
|
+
if (mode === "json") {
|
|
483
|
+
writeJson({
|
|
484
|
+
currentReleaseId: currentId ?? null,
|
|
485
|
+
recentReleases: releases.slice(0, 5),
|
|
486
|
+
stage
|
|
487
|
+
});
|
|
488
|
+
} else {
|
|
489
|
+
writeOut(`stage: ${stage}`);
|
|
490
|
+
writeOut(`current release: ${currentId ?? "(unknown)"}`);
|
|
491
|
+
if (releases.length > 0) {
|
|
492
|
+
writeOut(`
|
|
493
|
+
recent releases:`);
|
|
494
|
+
const rows = releases.slice(0, 5).map((release) => [
|
|
495
|
+
release.active === true ? "*" : " ",
|
|
496
|
+
release.id,
|
|
497
|
+
formatRelativeTime(Date.now() - release.at)
|
|
498
|
+
]);
|
|
499
|
+
writeOut(renderTable(["", "id", "age"], rows));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return 0;
|
|
503
|
+
}
|
|
504
|
+
case "rollback": {
|
|
505
|
+
if (deployer.rollback === undefined) {
|
|
506
|
+
throw new Error("the deployer for this stage does not implement rollback()");
|
|
507
|
+
}
|
|
508
|
+
let target = typeof flags.to === "string" ? flags.to : undefined;
|
|
509
|
+
if (target === undefined) {
|
|
510
|
+
if (deployer.listReleases === undefined) {
|
|
511
|
+
throw new Error("rollback without --to requires deployer.listReleases() to find the previous release");
|
|
512
|
+
}
|
|
513
|
+
const releases = await deployer.listReleases();
|
|
514
|
+
if (releases.length < 2) {
|
|
515
|
+
throw new Error(`stage ${stage} has fewer than 2 releases \u2014 nothing to roll back to`);
|
|
516
|
+
}
|
|
517
|
+
const activeIndex = releases.findIndex((r) => r.active === true);
|
|
518
|
+
const previous = activeIndex >= 0 && activeIndex + 1 < releases.length ? releases[activeIndex + 1] : releases[1];
|
|
519
|
+
target = previous?.id;
|
|
520
|
+
if (target === undefined) {
|
|
521
|
+
throw new Error("could not determine previous release id");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
await deployer.rollback(target);
|
|
525
|
+
if (mode === "json") {
|
|
526
|
+
writeJson({ rolledBackTo: target, stage });
|
|
527
|
+
} else {
|
|
528
|
+
writeOut(`${stage}: rolled back to ${target}`);
|
|
529
|
+
}
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
default:
|
|
533
|
+
throw new Error(`unknown deploy verb: "${verb}". try: releases | status | rollback`);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// src/cli.ts
|
|
538
|
+
var parseArgs = (argv) => {
|
|
539
|
+
const positional = [];
|
|
540
|
+
const flags = {};
|
|
541
|
+
let command;
|
|
542
|
+
let verb;
|
|
543
|
+
let index = 0;
|
|
544
|
+
while (index < argv.length) {
|
|
545
|
+
const arg = argv[index];
|
|
546
|
+
if (arg.startsWith("--")) {
|
|
547
|
+
const body = arg.slice(2);
|
|
548
|
+
const eq = body.indexOf("=");
|
|
549
|
+
if (eq >= 0) {
|
|
550
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
551
|
+
} else {
|
|
552
|
+
const next = argv[index + 1];
|
|
553
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
554
|
+
flags[body] = next;
|
|
555
|
+
index += 1;
|
|
556
|
+
} else {
|
|
557
|
+
flags[body] = true;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
} else if (command === undefined) {
|
|
561
|
+
command = arg;
|
|
562
|
+
} else if (verb === undefined) {
|
|
563
|
+
verb = arg;
|
|
564
|
+
} else {
|
|
565
|
+
positional.push(arg);
|
|
566
|
+
}
|
|
567
|
+
index += 1;
|
|
568
|
+
}
|
|
569
|
+
return { command, flags, positional, verb };
|
|
570
|
+
};
|
|
571
|
+
var HELP = `absolutejs \u2014 substrate CLI for the AbsoluteJS PaaS
|
|
572
|
+
|
|
573
|
+
USAGE
|
|
574
|
+
absolutejs <command> <verb> [args...] [--flags]
|
|
575
|
+
|
|
576
|
+
COMMANDS
|
|
577
|
+
secrets list list secret names + fingerprints from the broker
|
|
578
|
+
secrets get <name> [--show] resolve one secret (--show prints plaintext)
|
|
579
|
+
secrets set <NAME>=<value> put a value via the configured adapter
|
|
580
|
+
secrets rotate <name> generate + persist a new value, fire onRotate
|
|
581
|
+
|
|
582
|
+
env push <stage> resolve secrets + extras, atomic write remote env file
|
|
583
|
+
env pull <stage> print the remote env file (or --json its values)
|
|
584
|
+
env diff <stage> [--all] show what env push would change
|
|
585
|
+
|
|
586
|
+
deploy releases <stage> list release history for a stage
|
|
587
|
+
deploy status <stage> current release id + recent history
|
|
588
|
+
deploy rollback <stage> [--to <id>] roll back to <id> or the previous release
|
|
589
|
+
|
|
590
|
+
GLOBAL FLAGS
|
|
591
|
+
--json machine-readable output
|
|
592
|
+
--help this banner
|
|
593
|
+
|
|
594
|
+
CONFIG
|
|
595
|
+
Reads ./absolutejs.config.ts (walks parent dirs). Author it with:
|
|
596
|
+
|
|
597
|
+
import { defineConfig } from '@absolutejs/cli';
|
|
598
|
+
export default defineConfig({
|
|
599
|
+
secrets: /* SecretBroker */,
|
|
600
|
+
secretAdapter: /* SecretAdapter */,
|
|
601
|
+
deployments: [
|
|
602
|
+
{ name: 'prod', target: () => ..., remotePath: '/etc/api.env',
|
|
603
|
+
secretNames: ['STRIPE_KEY'], reload: 'systemctl reload api' }
|
|
604
|
+
],
|
|
605
|
+
});`;
|
|
606
|
+
var main = async (argv) => {
|
|
607
|
+
const args = parseArgs(argv);
|
|
608
|
+
if (args.flags.help === true || args.command === undefined) {
|
|
609
|
+
writeOut(HELP);
|
|
610
|
+
return args.command === undefined && args.flags.help !== true ? 1 : 0;
|
|
611
|
+
}
|
|
612
|
+
const mode = args.flags.json === true ? "json" : "human";
|
|
613
|
+
const verb = args.verb;
|
|
614
|
+
if (verb === undefined) {
|
|
615
|
+
writeErr(`missing verb for "${args.command}". run \`absolutejs --help\``);
|
|
616
|
+
return 2;
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
const { config } = await loadConfig();
|
|
620
|
+
switch (args.command) {
|
|
621
|
+
case "secrets":
|
|
622
|
+
return await runSecrets(config, {
|
|
623
|
+
flags: args.flags,
|
|
624
|
+
positional: args.positional,
|
|
625
|
+
verb
|
|
626
|
+
}, mode);
|
|
627
|
+
case "env":
|
|
628
|
+
return await runEnv(config, {
|
|
629
|
+
flags: args.flags,
|
|
630
|
+
positional: args.positional,
|
|
631
|
+
verb
|
|
632
|
+
}, mode);
|
|
633
|
+
case "deploy":
|
|
634
|
+
return await runDeploy(config, {
|
|
635
|
+
flags: args.flags,
|
|
636
|
+
positional: args.positional,
|
|
637
|
+
verb
|
|
638
|
+
}, mode);
|
|
639
|
+
default:
|
|
640
|
+
writeErr(`unknown command "${args.command}". try: secrets | env | deploy`);
|
|
641
|
+
return 2;
|
|
642
|
+
}
|
|
643
|
+
} catch (error) {
|
|
644
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
645
|
+
if (mode === "json") {
|
|
646
|
+
process.stdout.write(`${JSON.stringify({ error: message })}
|
|
647
|
+
`);
|
|
648
|
+
} else {
|
|
649
|
+
writeErr(`error: ${message}`);
|
|
650
|
+
}
|
|
651
|
+
return 1;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
export {
|
|
655
|
+
parseArgs,
|
|
656
|
+
main
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
//# debugId=1486CB5B222DF56264756E2164756E21
|
|
660
|
+
//# sourceMappingURL=cli.js.map
|