auix 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2056 -151
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { formatWithOptions } from "node:util";
|
|
3
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import g$1 from "node:process";
|
|
5
5
|
import * as tty from "node:tty";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
-
import { execSync } from "node:child_process";
|
|
9
|
-
import {
|
|
8
|
+
import { execSync, spawn } from "node:child_process";
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
10
10
|
|
|
11
11
|
//#region ../../node_modules/consola/dist/core.mjs
|
|
12
12
|
const LogLevels = {
|
|
@@ -959,7 +959,7 @@ function _getDefaultLogLevel() {
|
|
|
959
959
|
const consola = createConsola();
|
|
960
960
|
|
|
961
961
|
//#endregion
|
|
962
|
-
//#region node_modules/citty/dist/index.mjs
|
|
962
|
+
//#region ../../node_modules/citty/dist/index.mjs
|
|
963
963
|
function toArray(val) {
|
|
964
964
|
if (Array.isArray(val)) return val;
|
|
965
965
|
return val === void 0 ? [] : [val];
|
|
@@ -1164,7 +1164,7 @@ function resolveArgs(argsDef) {
|
|
|
1164
1164
|
function defineCommand(def) {
|
|
1165
1165
|
return def;
|
|
1166
1166
|
}
|
|
1167
|
-
async function runCommand(cmd, opts) {
|
|
1167
|
+
async function runCommand$1(cmd, opts) {
|
|
1168
1168
|
const cmdArgs = await resolveValue(cmd.args || {});
|
|
1169
1169
|
const parsedArgs = parseArgs(opts.rawArgs, cmdArgs);
|
|
1170
1170
|
const context = {
|
|
@@ -1183,7 +1183,7 @@ async function runCommand(cmd, opts) {
|
|
|
1183
1183
|
if (subCommandName) {
|
|
1184
1184
|
if (!subCommands[subCommandName]) throw new CLIError(`Unknown command \`${subCommandName}\``, "E_UNKNOWN_COMMAND");
|
|
1185
1185
|
const subCommand = await resolveValue(subCommands[subCommandName]);
|
|
1186
|
-
if (subCommand) await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
|
|
1186
|
+
if (subCommand) await runCommand$1(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
|
|
1187
1187
|
} else if (!cmd.run) throw new CLIError(`No command specified.`, "E_NO_COMMAND");
|
|
1188
1188
|
}
|
|
1189
1189
|
if (typeof cmd.run === "function") result = await cmd.run(context);
|
|
@@ -1277,7 +1277,7 @@ async function runMain(cmd, opts = {}) {
|
|
|
1277
1277
|
const meta = typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta;
|
|
1278
1278
|
if (!meta?.version) throw new CLIError("No version specified", "E_NO_VERSION");
|
|
1279
1279
|
consola.log(meta.version);
|
|
1280
|
-
} else await runCommand(cmd, { rawArgs });
|
|
1280
|
+
} else await runCommand$1(cmd, { rawArgs });
|
|
1281
1281
|
} catch (error) {
|
|
1282
1282
|
const isCLIError = error instanceof CLIError;
|
|
1283
1283
|
if (!isCLIError) consola.error(error, "\n");
|
|
@@ -1289,30 +1289,85 @@ async function runMain(cmd, opts = {}) {
|
|
|
1289
1289
|
|
|
1290
1290
|
//#endregion
|
|
1291
1291
|
//#region ../env/dist/index.js
|
|
1292
|
+
function parseEnvString(content) {
|
|
1293
|
+
const result = {};
|
|
1294
|
+
for (const line of content.split("\n")) {
|
|
1295
|
+
const trimmed = line.trim();
|
|
1296
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1297
|
+
const eqIndex = trimmed.indexOf("=");
|
|
1298
|
+
if (eqIndex === -1) continue;
|
|
1299
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
1300
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
1301
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1302
|
+
result[key] = value;
|
|
1303
|
+
}
|
|
1304
|
+
return result;
|
|
1305
|
+
}
|
|
1292
1306
|
function formatEnvString(vars) {
|
|
1293
1307
|
return Object.entries(vars).map(([key, value]) => {
|
|
1294
1308
|
return `${key}=${value.includes(" ") || value.includes("#") || value.includes("\n") ? `"${value}"` : value}`;
|
|
1295
1309
|
}).join("\n");
|
|
1296
1310
|
}
|
|
1311
|
+
function formatJsonString(vars) {
|
|
1312
|
+
return JSON.stringify(vars, null, 2);
|
|
1313
|
+
}
|
|
1314
|
+
function formatYamlString(vars) {
|
|
1315
|
+
const specialChars = /[:#{}[\],&*?|<>=!%@`\-\s]/;
|
|
1316
|
+
return Object.entries(vars).map(([key, value]) => {
|
|
1317
|
+
return `${key}: ${value === "" || specialChars.test(value) ? `"${value}"` : value}`;
|
|
1318
|
+
}).join("\n");
|
|
1319
|
+
}
|
|
1320
|
+
function interpolateEnv(vars) {
|
|
1321
|
+
const result = {};
|
|
1322
|
+
for (const [key, value] of Object.entries(vars)) result[key] = value;
|
|
1323
|
+
const pattern = /\$\{([^}]+)\}/g;
|
|
1324
|
+
for (let i = 0; i < 10; i++) {
|
|
1325
|
+
let changed = false;
|
|
1326
|
+
for (const key of Object.keys(result)) {
|
|
1327
|
+
const value = result[key];
|
|
1328
|
+
if (!pattern.test(value)) continue;
|
|
1329
|
+
pattern.lastIndex = 0;
|
|
1330
|
+
const resolving = /* @__PURE__ */ new Set();
|
|
1331
|
+
resolving.add(key);
|
|
1332
|
+
const newValue = value.replace(pattern, (match, ref) => {
|
|
1333
|
+
if (resolving.has(ref)) return match;
|
|
1334
|
+
if (!(ref in result)) return match;
|
|
1335
|
+
const refValue = result[ref];
|
|
1336
|
+
if (pattern.test(refValue)) {
|
|
1337
|
+
pattern.lastIndex = 0;
|
|
1338
|
+
if ([...refValue.matchAll(/\$\{([^}]+)\}/g)].map((m) => m[1]).some((r) => resolving.has(r))) return match;
|
|
1339
|
+
}
|
|
1340
|
+
pattern.lastIndex = 0;
|
|
1341
|
+
return refValue;
|
|
1342
|
+
});
|
|
1343
|
+
if (newValue !== value) {
|
|
1344
|
+
result[key] = newValue;
|
|
1345
|
+
changed = true;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (!changed) break;
|
|
1349
|
+
}
|
|
1350
|
+
return result;
|
|
1351
|
+
}
|
|
1297
1352
|
|
|
1298
1353
|
//#endregion
|
|
1299
1354
|
//#region src/config.ts
|
|
1300
1355
|
const CONFIG_DIR = join(homedir(), ".auix");
|
|
1301
|
-
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
1356
|
+
const CONFIG_FILE$1 = join(CONFIG_DIR, "config.json");
|
|
1302
1357
|
const PROJECT_FILE = ".auixrc";
|
|
1303
1358
|
const DEFAULT_BASE_URL = "https://api.auix.dev";
|
|
1304
|
-
const DEFAULT_APP_URL = "https://
|
|
1359
|
+
const DEFAULT_APP_URL = "https://auix.dev";
|
|
1305
1360
|
function loadGlobalConfig() {
|
|
1306
|
-
if (!existsSync(CONFIG_FILE)) return {};
|
|
1361
|
+
if (!existsSync(CONFIG_FILE$1)) return {};
|
|
1307
1362
|
try {
|
|
1308
|
-
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
1363
|
+
return JSON.parse(readFileSync(CONFIG_FILE$1, "utf-8"));
|
|
1309
1364
|
} catch {
|
|
1310
1365
|
return {};
|
|
1311
1366
|
}
|
|
1312
1367
|
}
|
|
1313
1368
|
function saveGlobalConfig(config) {
|
|
1314
1369
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
1315
|
-
writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`);
|
|
1370
|
+
writeFileSync(CONFIG_FILE$1, `${JSON.stringify(config, null, 2)}\n`);
|
|
1316
1371
|
}
|
|
1317
1372
|
function loadProjectConfig() {
|
|
1318
1373
|
let dir = process.cwd();
|
|
@@ -1343,7 +1398,11 @@ function getApiKey() {
|
|
|
1343
1398
|
if (envKey) return envKey;
|
|
1344
1399
|
const config = loadGlobalConfig();
|
|
1345
1400
|
if (config.apiKey) return config.apiKey;
|
|
1346
|
-
|
|
1401
|
+
}
|
|
1402
|
+
function requireApiKey() {
|
|
1403
|
+
const key = getApiKey();
|
|
1404
|
+
if (!key) throw new Error("No API key found. Run `auix login` or set AUIX_API_KEY.");
|
|
1405
|
+
return key;
|
|
1347
1406
|
}
|
|
1348
1407
|
function getBaseUrl() {
|
|
1349
1408
|
return process.env.AUIX_BASE_URL ?? loadGlobalConfig().baseUrl ?? DEFAULT_BASE_URL;
|
|
@@ -1351,6 +1410,21 @@ function getBaseUrl() {
|
|
|
1351
1410
|
function getAppUrl() {
|
|
1352
1411
|
return process.env.AUIX_APP_URL ?? loadGlobalConfig().appUrl ?? DEFAULT_APP_URL;
|
|
1353
1412
|
}
|
|
1413
|
+
const SESSION_FILE = join(CONFIG_DIR, "session.json");
|
|
1414
|
+
function loadSession() {
|
|
1415
|
+
if (!existsSync(SESSION_FILE)) return void 0;
|
|
1416
|
+
try {
|
|
1417
|
+
const session = JSON.parse(readFileSync(SESSION_FILE, "utf-8"));
|
|
1418
|
+
if (new Date(session.expiresAt) <= /* @__PURE__ */ new Date()) return;
|
|
1419
|
+
return session;
|
|
1420
|
+
} catch {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function saveSession(session) {
|
|
1425
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
1426
|
+
writeFileSync(SESSION_FILE, `${JSON.stringify(session, null, 2)}\n`);
|
|
1427
|
+
}
|
|
1354
1428
|
|
|
1355
1429
|
//#endregion
|
|
1356
1430
|
//#region src/utils.ts
|
|
@@ -1360,11 +1434,30 @@ async function apiRequest(path, body) {
|
|
|
1360
1434
|
method: "POST",
|
|
1361
1435
|
headers: {
|
|
1362
1436
|
"Content-Type": "application/json",
|
|
1363
|
-
Authorization: `Bearer ${
|
|
1437
|
+
Authorization: `Bearer ${requireApiKey()}`
|
|
1438
|
+
},
|
|
1439
|
+
body: JSON.stringify(body)
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
async function apiRequestWithToken(path, body, token) {
|
|
1443
|
+
const baseUrl = getBaseUrl().replace(/\/$/, "");
|
|
1444
|
+
return fetch(`${baseUrl}${path}`, {
|
|
1445
|
+
method: "POST",
|
|
1446
|
+
headers: {
|
|
1447
|
+
"Content-Type": "application/json",
|
|
1448
|
+
Authorization: `Bearer ${token}`
|
|
1364
1449
|
},
|
|
1365
1450
|
body: JSON.stringify(body)
|
|
1366
1451
|
});
|
|
1367
1452
|
}
|
|
1453
|
+
async function apiRequestNoAuth(path, body) {
|
|
1454
|
+
const baseUrl = getBaseUrl().replace(/\/$/, "");
|
|
1455
|
+
return fetch(`${baseUrl}${path}`, {
|
|
1456
|
+
method: "POST",
|
|
1457
|
+
headers: { "Content-Type": "application/json" },
|
|
1458
|
+
body: JSON.stringify(body)
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1368
1461
|
function logError(message) {
|
|
1369
1462
|
console.error(`Error: ${message}`);
|
|
1370
1463
|
process.exit(1);
|
|
@@ -1374,6 +1467,134 @@ function maskValue(value) {
|
|
|
1374
1467
|
return `${value.slice(0, 2)}****${value.slice(-2)}`;
|
|
1375
1468
|
}
|
|
1376
1469
|
|
|
1470
|
+
//#endregion
|
|
1471
|
+
//#region src/commands/branch-create.ts
|
|
1472
|
+
const branchCreateCommand = defineCommand({
|
|
1473
|
+
meta: {
|
|
1474
|
+
name: "branch create",
|
|
1475
|
+
description: "Create an env branch"
|
|
1476
|
+
},
|
|
1477
|
+
args: {
|
|
1478
|
+
name: {
|
|
1479
|
+
type: "positional",
|
|
1480
|
+
description: "Branch name",
|
|
1481
|
+
required: true
|
|
1482
|
+
},
|
|
1483
|
+
"base-env": {
|
|
1484
|
+
type: "string",
|
|
1485
|
+
description: "Base environment (development, staging, production)"
|
|
1486
|
+
}
|
|
1487
|
+
},
|
|
1488
|
+
async run({ args }) {
|
|
1489
|
+
const res = await apiRequest("/v1/env/branch/create", {
|
|
1490
|
+
name: args.name,
|
|
1491
|
+
baseEnvironment: args["base-env"]
|
|
1492
|
+
});
|
|
1493
|
+
if (!res.ok) {
|
|
1494
|
+
const text = await res.text().catch(() => "");
|
|
1495
|
+
logError(`Failed to create branch (${res.status}): ${text}`);
|
|
1496
|
+
}
|
|
1497
|
+
const data = await res.json();
|
|
1498
|
+
console.log(`Created branch: ${data.name}`);
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
//#endregion
|
|
1503
|
+
//#region src/commands/branch-delete.ts
|
|
1504
|
+
const branchDeleteCommand = defineCommand({
|
|
1505
|
+
meta: {
|
|
1506
|
+
name: "branch delete",
|
|
1507
|
+
description: "Delete an env branch"
|
|
1508
|
+
},
|
|
1509
|
+
args: { name: {
|
|
1510
|
+
type: "positional",
|
|
1511
|
+
description: "Branch name",
|
|
1512
|
+
required: true
|
|
1513
|
+
} },
|
|
1514
|
+
async run({ args }) {
|
|
1515
|
+
const confirmed = await consola.prompt(`Delete branch "${args.name}" and all its variables?`, { type: "confirm" });
|
|
1516
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
1517
|
+
console.log("Cancelled.");
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const res = await apiRequest("/v1/env/branch/delete", { name: args.name });
|
|
1521
|
+
if (!res.ok) {
|
|
1522
|
+
const text = await res.text().catch(() => "");
|
|
1523
|
+
logError(`Failed to delete branch (${res.status}): ${text}`);
|
|
1524
|
+
}
|
|
1525
|
+
console.log(`Deleted branch: ${args.name}`);
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
//#endregion
|
|
1530
|
+
//#region src/commands/branch-list.ts
|
|
1531
|
+
const branchListCommand = defineCommand({
|
|
1532
|
+
meta: {
|
|
1533
|
+
name: "branch list",
|
|
1534
|
+
description: "List env branches"
|
|
1535
|
+
},
|
|
1536
|
+
args: {},
|
|
1537
|
+
async run() {
|
|
1538
|
+
const res = await apiRequest("/v1/env/branch/list", {});
|
|
1539
|
+
if (!res.ok) {
|
|
1540
|
+
const text = await res.text().catch(() => "");
|
|
1541
|
+
logError(`Failed to list branches (${res.status}): ${text}`);
|
|
1542
|
+
}
|
|
1543
|
+
const data = await res.json();
|
|
1544
|
+
if (data.branches.length === 0) {
|
|
1545
|
+
console.log("No branches found.");
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
1549
|
+
const nameW = col(data.branches.map((b) => b.name), 4);
|
|
1550
|
+
const baseW = col(data.branches.map((b) => b.baseEnvironment ?? "-"), 8);
|
|
1551
|
+
const header = [
|
|
1552
|
+
"NAME".padEnd(nameW),
|
|
1553
|
+
"BASE ENV".padEnd(baseW),
|
|
1554
|
+
"CREATED"
|
|
1555
|
+
].join(" ");
|
|
1556
|
+
console.log(header);
|
|
1557
|
+
console.log("-".repeat(header.length));
|
|
1558
|
+
for (const b of data.branches) {
|
|
1559
|
+
const date = new Date(b.createdAt).toLocaleString();
|
|
1560
|
+
const row = [
|
|
1561
|
+
b.name.padEnd(nameW),
|
|
1562
|
+
(b.baseEnvironment ?? "-").padEnd(baseW),
|
|
1563
|
+
date
|
|
1564
|
+
].join(" ");
|
|
1565
|
+
console.log(row);
|
|
1566
|
+
}
|
|
1567
|
+
console.log(`\n${data.branches.length} branches`);
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
//#endregion
|
|
1572
|
+
//#region src/commands/delete.ts
|
|
1573
|
+
const deleteCommand = defineCommand({
|
|
1574
|
+
meta: {
|
|
1575
|
+
name: "delete",
|
|
1576
|
+
description: "Delete an environment variable by ID"
|
|
1577
|
+
},
|
|
1578
|
+
args: { id: {
|
|
1579
|
+
type: "positional",
|
|
1580
|
+
description: "Variable ID (from `auix env list`)",
|
|
1581
|
+
required: true
|
|
1582
|
+
} },
|
|
1583
|
+
async run({ args }) {
|
|
1584
|
+
const confirmed = await consola.prompt(`Delete variable ${args.id}?`, { type: "confirm" });
|
|
1585
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
1586
|
+
console.log("Cancelled.");
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
const res = await apiRequest("/v1/env/delete", { id: args.id });
|
|
1590
|
+
if (!res.ok) {
|
|
1591
|
+
const text = await res.text().catch(() => "");
|
|
1592
|
+
logError(`Failed to delete (${res.status}): ${text}`);
|
|
1593
|
+
}
|
|
1594
|
+
console.log("Deleted.");
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1377
1598
|
//#endregion
|
|
1378
1599
|
//#region src/commands/diff.ts
|
|
1379
1600
|
const diffCommand = defineCommand({
|
|
@@ -1400,6 +1621,10 @@ const diffCommand = defineCommand({
|
|
|
1400
1621
|
type: "string",
|
|
1401
1622
|
description: "Project slug"
|
|
1402
1623
|
},
|
|
1624
|
+
branch: {
|
|
1625
|
+
type: "string",
|
|
1626
|
+
description: "Env branch name"
|
|
1627
|
+
},
|
|
1403
1628
|
"show-values": {
|
|
1404
1629
|
type: "boolean",
|
|
1405
1630
|
description: "Show unmasked values",
|
|
@@ -1413,11 +1638,13 @@ const diffCommand = defineCommand({
|
|
|
1413
1638
|
const [res1, res2] = await Promise.all([apiRequest("/v1/env/resolve", {
|
|
1414
1639
|
project,
|
|
1415
1640
|
environment: args.env1,
|
|
1416
|
-
app
|
|
1641
|
+
app,
|
|
1642
|
+
branch: args.branch
|
|
1417
1643
|
}), apiRequest("/v1/env/resolve", {
|
|
1418
1644
|
project,
|
|
1419
1645
|
environment: args.env2,
|
|
1420
|
-
app
|
|
1646
|
+
app,
|
|
1647
|
+
branch: args.branch
|
|
1421
1648
|
})]);
|
|
1422
1649
|
if (!res1.ok) {
|
|
1423
1650
|
const text = await res1.text().catch(() => "");
|
|
@@ -1459,17 +1686,143 @@ const diffCommand = defineCommand({
|
|
|
1459
1686
|
});
|
|
1460
1687
|
|
|
1461
1688
|
//#endregion
|
|
1462
|
-
//#region src/commands/
|
|
1463
|
-
const
|
|
1689
|
+
//#region src/commands/dynamic-create.ts
|
|
1690
|
+
const dynamicCreateCommand = defineCommand({
|
|
1464
1691
|
meta: {
|
|
1465
|
-
name: "
|
|
1466
|
-
description: "
|
|
1692
|
+
name: "dynamic create",
|
|
1693
|
+
description: "Create a dynamic secret config"
|
|
1694
|
+
},
|
|
1695
|
+
args: {
|
|
1696
|
+
name: {
|
|
1697
|
+
type: "string",
|
|
1698
|
+
description: "Dynamic secret name",
|
|
1699
|
+
required: true
|
|
1700
|
+
},
|
|
1701
|
+
generator: {
|
|
1702
|
+
type: "string",
|
|
1703
|
+
description: "Generator type (random-password, random-token, random-uuid)",
|
|
1704
|
+
required: true
|
|
1705
|
+
},
|
|
1706
|
+
ttl: {
|
|
1707
|
+
type: "string",
|
|
1708
|
+
description: "TTL in seconds (default: 3600)",
|
|
1709
|
+
default: "3600"
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
async run({ args }) {
|
|
1713
|
+
const validGenerators = [
|
|
1714
|
+
"random-password",
|
|
1715
|
+
"random-token",
|
|
1716
|
+
"random-uuid"
|
|
1717
|
+
];
|
|
1718
|
+
if (!validGenerators.includes(args.generator)) logError(`Invalid generator: ${args.generator}. Available: ${validGenerators.join(", ")}`);
|
|
1719
|
+
const ttlSeconds = Number.parseInt(args.ttl, 10);
|
|
1720
|
+
if (Number.isNaN(ttlSeconds) || ttlSeconds <= 0) logError("--ttl must be a positive number of seconds.");
|
|
1721
|
+
const res = await apiRequest("/v1/env/dynamic/create", {
|
|
1722
|
+
name: args.name,
|
|
1723
|
+
generator: args.generator,
|
|
1724
|
+
ttlSeconds
|
|
1725
|
+
});
|
|
1726
|
+
if (!res.ok) {
|
|
1727
|
+
const text = await res.text().catch(() => "");
|
|
1728
|
+
logError(`Failed to create dynamic secret (${res.status}): ${text}`);
|
|
1729
|
+
}
|
|
1730
|
+
const data = await res.json();
|
|
1731
|
+
console.log(`\nDynamic secret created`);
|
|
1732
|
+
console.log(` ID: ${data.id}`);
|
|
1733
|
+
console.log(` Name: ${data.name}`);
|
|
1734
|
+
console.log(` Generator: ${args.generator}`);
|
|
1735
|
+
console.log(` TTL: ${ttlSeconds}s`);
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
//#endregion
|
|
1740
|
+
//#region src/commands/dynamic-lease.ts
|
|
1741
|
+
const dynamicLeaseCommand = defineCommand({
|
|
1742
|
+
meta: {
|
|
1743
|
+
name: "dynamic lease",
|
|
1744
|
+
description: "Generate a new credential from a dynamic secret"
|
|
1745
|
+
},
|
|
1746
|
+
args: { name: {
|
|
1747
|
+
type: "positional",
|
|
1748
|
+
description: "Dynamic secret name",
|
|
1749
|
+
required: true
|
|
1750
|
+
} },
|
|
1751
|
+
async run({ args }) {
|
|
1752
|
+
const res = await apiRequest("/v1/env/dynamic/lease", { name: args.name });
|
|
1753
|
+
if (!res.ok) {
|
|
1754
|
+
const text = await res.text().catch(() => "");
|
|
1755
|
+
logError(`Failed to create lease (${res.status}): ${text}`);
|
|
1756
|
+
}
|
|
1757
|
+
const data = await res.json();
|
|
1758
|
+
console.log(`\nLease created`);
|
|
1759
|
+
console.log(` Lease ID: ${data.leaseId}`);
|
|
1760
|
+
console.log(` Value: ${data.value}`);
|
|
1761
|
+
console.log(` Expires: ${new Date(data.expiresAt).toLocaleString()}`);
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region src/commands/dynamic-list.ts
|
|
1767
|
+
const dynamicListCommand = defineCommand({
|
|
1768
|
+
meta: {
|
|
1769
|
+
name: "dynamic list",
|
|
1770
|
+
description: "List dynamic secret configs"
|
|
1771
|
+
},
|
|
1772
|
+
args: {},
|
|
1773
|
+
async run() {
|
|
1774
|
+
const res = await apiRequest("/v1/env/dynamic/list", {});
|
|
1775
|
+
if (!res.ok) {
|
|
1776
|
+
const text = await res.text().catch(() => "");
|
|
1777
|
+
logError(`Failed to list dynamic secrets (${res.status}): ${text}`);
|
|
1778
|
+
}
|
|
1779
|
+
const data = await res.json();
|
|
1780
|
+
if (data.secrets.length === 0) {
|
|
1781
|
+
console.log("No dynamic secrets found.");
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
1785
|
+
const idW = 8;
|
|
1786
|
+
const nameW = col(data.secrets.map((s) => s.name), 4);
|
|
1787
|
+
const genW = col(data.secrets.map((s) => s.generator), 9);
|
|
1788
|
+
const ttlW = col(data.secrets.map((s) => `${s.ttlSeconds}s`), 3);
|
|
1789
|
+
const header = [
|
|
1790
|
+
"ID".padEnd(idW),
|
|
1791
|
+
"NAME".padEnd(nameW),
|
|
1792
|
+
"GENERATOR".padEnd(genW),
|
|
1793
|
+
"TTL".padEnd(ttlW)
|
|
1794
|
+
].join(" ");
|
|
1795
|
+
console.log(header);
|
|
1796
|
+
console.log("-".repeat(header.length));
|
|
1797
|
+
for (const s of data.secrets) {
|
|
1798
|
+
const row = [
|
|
1799
|
+
s.id.slice(0, idW).padEnd(idW),
|
|
1800
|
+
s.name.padEnd(nameW),
|
|
1801
|
+
s.generator.padEnd(genW),
|
|
1802
|
+
`${s.ttlSeconds}s`.padEnd(ttlW)
|
|
1803
|
+
].join(" ");
|
|
1804
|
+
console.log(row);
|
|
1805
|
+
}
|
|
1806
|
+
console.log(`\n${data.secrets.length} dynamic secrets`);
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
//#endregion
|
|
1811
|
+
//#region src/commands/history.ts
|
|
1812
|
+
const historyCommand = defineCommand({
|
|
1813
|
+
meta: {
|
|
1814
|
+
name: "history",
|
|
1815
|
+
description: "Show version history for an environment variable"
|
|
1467
1816
|
},
|
|
1468
1817
|
args: {
|
|
1818
|
+
key: {
|
|
1819
|
+
type: "positional",
|
|
1820
|
+
description: "Variable key",
|
|
1821
|
+
required: true
|
|
1822
|
+
},
|
|
1469
1823
|
env: {
|
|
1470
1824
|
type: "string",
|
|
1471
|
-
description: "Environment (development, staging, production)"
|
|
1472
|
-
default: "development"
|
|
1825
|
+
description: "Environment (development, staging, production)"
|
|
1473
1826
|
},
|
|
1474
1827
|
app: {
|
|
1475
1828
|
type: "string",
|
|
@@ -1479,6 +1832,10 @@ const listCommand = defineCommand({
|
|
|
1479
1832
|
type: "string",
|
|
1480
1833
|
description: "Project slug"
|
|
1481
1834
|
},
|
|
1835
|
+
branch: {
|
|
1836
|
+
type: "string",
|
|
1837
|
+
description: "Env branch name"
|
|
1838
|
+
},
|
|
1482
1839
|
"show-values": {
|
|
1483
1840
|
type: "boolean",
|
|
1484
1841
|
description: "Show unmasked values",
|
|
@@ -1489,118 +1846,423 @@ const listCommand = defineCommand({
|
|
|
1489
1846
|
const projectConfig = loadProjectConfig();
|
|
1490
1847
|
const project = args.project ?? projectConfig.project;
|
|
1491
1848
|
const app = args.app ?? resolveApp();
|
|
1492
|
-
const res = await apiRequest("/v1/env/
|
|
1849
|
+
const res = await apiRequest("/v1/env/history", {
|
|
1850
|
+
key: args.key,
|
|
1493
1851
|
project,
|
|
1494
1852
|
environment: args.env,
|
|
1495
|
-
app
|
|
1853
|
+
app,
|
|
1854
|
+
branch: args.branch
|
|
1496
1855
|
});
|
|
1497
1856
|
if (!res.ok) {
|
|
1498
1857
|
const text = await res.text().catch(() => "");
|
|
1499
|
-
logError(`Failed to
|
|
1858
|
+
logError(`Failed to get history (${res.status}): ${text}`);
|
|
1500
1859
|
}
|
|
1501
1860
|
const data = await res.json();
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
console.log("No variables found.");
|
|
1861
|
+
if (data.versions.length === 0) {
|
|
1862
|
+
console.log("No history found.");
|
|
1505
1863
|
return;
|
|
1506
1864
|
}
|
|
1507
1865
|
const showValues = args["show-values"];
|
|
1508
|
-
for
|
|
1509
|
-
|
|
1510
|
-
|
|
1866
|
+
console.log(`History for ${args.key}\n`);
|
|
1867
|
+
for (const v of data.versions) {
|
|
1868
|
+
const val = showValues ? v.value : maskValue(v.value);
|
|
1869
|
+
const date = new Date(v.createdAt).toLocaleString();
|
|
1870
|
+
const label = v.version === "current" ? "current" : `v${v.version}`;
|
|
1871
|
+
console.log(` ${label.padEnd(10)} ${val.padEnd(20)} ${date}`);
|
|
1511
1872
|
}
|
|
1512
|
-
console.log(`\n${
|
|
1873
|
+
console.log(`\n${data.versions.length} versions`);
|
|
1513
1874
|
}
|
|
1514
1875
|
});
|
|
1515
1876
|
|
|
1516
1877
|
//#endregion
|
|
1517
|
-
//#region src/commands/
|
|
1518
|
-
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1878
|
+
//#region src/commands/init.ts
|
|
1879
|
+
function detectWorkspaceDirs() {
|
|
1880
|
+
const cwd = process.cwd();
|
|
1881
|
+
const pnpmPath = join(cwd, "pnpm-workspace.yaml");
|
|
1882
|
+
if (existsSync(pnpmPath)) {
|
|
1883
|
+
const content = readFileSync(pnpmPath, "utf-8");
|
|
1884
|
+
const patterns = [];
|
|
1885
|
+
let inPackages = false;
|
|
1886
|
+
for (const line of content.split("\n")) {
|
|
1887
|
+
const trimmed = line.trim();
|
|
1888
|
+
if (trimmed === "packages:" || trimmed === "packages :") {
|
|
1889
|
+
inPackages = true;
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
if (inPackages && trimmed.startsWith("- ")) {
|
|
1893
|
+
const pattern = trimmed.slice(2).replace(/["']/g, "").trim();
|
|
1894
|
+
patterns.push(pattern);
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
if (inPackages && trimmed && !trimmed.startsWith("-")) inPackages = false;
|
|
1898
|
+
}
|
|
1899
|
+
return expandPatterns(cwd, patterns);
|
|
1535
1900
|
}
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
if (
|
|
1558
|
-
if (err.error === "expired_token") logError("Device code expired.");
|
|
1559
|
-
if (err.error === "access_denied") logError("Access denied.");
|
|
1560
|
-
logError(`Unexpected error: ${err.error ?? tokenRes.status}`);
|
|
1901
|
+
const pkgPath = join(cwd, "package.json");
|
|
1902
|
+
if (existsSync(pkgPath)) try {
|
|
1903
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1904
|
+
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
1905
|
+
if (workspaces && workspaces.length > 0) return expandPatterns(cwd, workspaces);
|
|
1906
|
+
} catch {}
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
function expandPatterns(cwd, patterns) {
|
|
1910
|
+
const dirs = [];
|
|
1911
|
+
for (const pattern of patterns) {
|
|
1912
|
+
const clean = pattern.replace(/\/\*$/, "");
|
|
1913
|
+
const parentDir = join(cwd, clean);
|
|
1914
|
+
if (!existsSync(parentDir)) continue;
|
|
1915
|
+
if (pattern.endsWith("/*")) try {
|
|
1916
|
+
const entries = readdirSync(parentDir, { withFileTypes: true });
|
|
1917
|
+
for (const entry of entries) if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
1918
|
+
const rel = `${clean}/${entry.name}`;
|
|
1919
|
+
if (existsSync(join(join(cwd, rel), "package.json"))) dirs.push(rel);
|
|
1920
|
+
}
|
|
1921
|
+
} catch {}
|
|
1922
|
+
else if (existsSync(join(parentDir, "package.json"))) dirs.push(clean);
|
|
1561
1923
|
}
|
|
1562
|
-
|
|
1563
|
-
return "";
|
|
1924
|
+
return dirs.length > 0 ? dirs : null;
|
|
1564
1925
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1926
|
+
const initCommand = defineCommand({
|
|
1927
|
+
meta: {
|
|
1928
|
+
name: "init",
|
|
1929
|
+
description: "Initialize project configuration"
|
|
1930
|
+
},
|
|
1931
|
+
args: { project: {
|
|
1932
|
+
type: "string",
|
|
1933
|
+
description: "Project slug (skip selection prompt)"
|
|
1934
|
+
} },
|
|
1935
|
+
async run({ args }) {
|
|
1936
|
+
const rcPath = join(process.cwd(), ".auixrc");
|
|
1937
|
+
if (existsSync(rcPath)) {
|
|
1938
|
+
const overwrite = await consola.prompt(".auixrc already exists. Overwrite?", { type: "confirm" });
|
|
1939
|
+
if (!overwrite || typeof overwrite === "symbol") {
|
|
1940
|
+
console.log("Cancelled.");
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
let projectSlug = args.project;
|
|
1945
|
+
if (!projectSlug) projectSlug = await selectProject();
|
|
1946
|
+
const config = { project: projectSlug };
|
|
1947
|
+
const workspaceDirs = detectWorkspaceDirs();
|
|
1948
|
+
if (workspaceDirs && workspaceDirs.length > 0) {
|
|
1949
|
+
console.log(`\nDetected monorepo with ${workspaceDirs.length} packages:`);
|
|
1950
|
+
for (const dir of workspaceDirs) console.log(` ${dir}`);
|
|
1951
|
+
const mapApps = await consola.prompt("\nMap directories to app names?", { type: "confirm" });
|
|
1952
|
+
if (mapApps && typeof mapApps !== "symbol") {
|
|
1953
|
+
const apps = {};
|
|
1954
|
+
for (const dir of workspaceDirs) {
|
|
1955
|
+
const defaultName = basename(dir);
|
|
1956
|
+
const name = await consola.prompt(`App name for ${dir}`, {
|
|
1957
|
+
type: "text",
|
|
1958
|
+
default: defaultName,
|
|
1959
|
+
placeholder: defaultName
|
|
1960
|
+
});
|
|
1961
|
+
if (typeof name === "symbol") {
|
|
1962
|
+
console.log("Cancelled.");
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
const trimmed = name.trim();
|
|
1966
|
+
if (trimmed) apps[dir] = trimmed;
|
|
1967
|
+
}
|
|
1968
|
+
if (Object.keys(apps).length > 0) config.apps = apps;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
writeFileSync(rcPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
1972
|
+
console.log(`\nCreated .auixrc`);
|
|
1973
|
+
console.log(` project: ${config.project}`);
|
|
1974
|
+
if (config.apps) console.log(` apps: ${Object.entries(config.apps).map(([d, a]) => `${d} → ${a}`).join(", ")}`);
|
|
1574
1975
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1976
|
+
});
|
|
1977
|
+
async function selectProject() {
|
|
1978
|
+
try {
|
|
1979
|
+
const res = await apiRequest("/v1/env/projects/list", {});
|
|
1980
|
+
if (res.ok) {
|
|
1981
|
+
const data = await res.json();
|
|
1982
|
+
if (data.projects && data.projects.length > 0) {
|
|
1983
|
+
const selected = await consola.prompt("Select project", {
|
|
1984
|
+
type: "select",
|
|
1985
|
+
options: data.projects.map((p) => ({
|
|
1986
|
+
label: `${p.name} (${p.slug})`,
|
|
1987
|
+
value: p.slug
|
|
1988
|
+
}))
|
|
1989
|
+
});
|
|
1990
|
+
if (typeof selected === "symbol") logError("Cancelled.");
|
|
1991
|
+
return selected;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
} catch {}
|
|
1995
|
+
const slug = await consola.prompt("Project slug", {
|
|
1996
|
+
type: "text",
|
|
1997
|
+
placeholder: basename(process.cwd()),
|
|
1998
|
+
default: basename(process.cwd())
|
|
1596
1999
|
});
|
|
1597
|
-
if (
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
return (await res.json()).key;
|
|
2000
|
+
if (typeof slug === "symbol") logError("Cancelled.");
|
|
2001
|
+
const trimmed = slug.trim();
|
|
2002
|
+
if (!trimmed) logError("Project slug cannot be empty.");
|
|
2003
|
+
return trimmed;
|
|
1602
2004
|
}
|
|
1603
|
-
|
|
2005
|
+
|
|
2006
|
+
//#endregion
|
|
2007
|
+
//#region src/commands/list.ts
|
|
2008
|
+
const listCommand = defineCommand({
|
|
2009
|
+
meta: {
|
|
2010
|
+
name: "list",
|
|
2011
|
+
description: "List environment variables"
|
|
2012
|
+
},
|
|
2013
|
+
args: {
|
|
2014
|
+
env: {
|
|
2015
|
+
type: "string",
|
|
2016
|
+
description: "Filter by environment (development, staging, production)"
|
|
2017
|
+
},
|
|
2018
|
+
app: {
|
|
2019
|
+
type: "string",
|
|
2020
|
+
description: "Filter by app name"
|
|
2021
|
+
},
|
|
2022
|
+
project: {
|
|
2023
|
+
type: "string",
|
|
2024
|
+
description: "Filter by project slug"
|
|
2025
|
+
},
|
|
2026
|
+
branch: {
|
|
2027
|
+
type: "string",
|
|
2028
|
+
description: "Filter by env branch name"
|
|
2029
|
+
}
|
|
2030
|
+
},
|
|
2031
|
+
async run({ args }) {
|
|
2032
|
+
const projectConfig = loadProjectConfig();
|
|
2033
|
+
const project = args.project ?? projectConfig.project;
|
|
2034
|
+
const app = args.app ?? resolveApp();
|
|
2035
|
+
const res = await apiRequest("/v1/env/list", {
|
|
2036
|
+
project,
|
|
2037
|
+
environment: args.env,
|
|
2038
|
+
app,
|
|
2039
|
+
branch: args.branch
|
|
2040
|
+
});
|
|
2041
|
+
if (!res.ok) {
|
|
2042
|
+
const text = await res.text().catch(() => "");
|
|
2043
|
+
logError(`Failed to list variables (${res.status}): ${text}`);
|
|
2044
|
+
}
|
|
2045
|
+
const data = await res.json();
|
|
2046
|
+
if (data.variables.length === 0) {
|
|
2047
|
+
console.log("No variables found.");
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
2051
|
+
const idW = 8;
|
|
2052
|
+
const keyW = col(data.variables.map((v) => v.key), 3);
|
|
2053
|
+
const prjW = col(data.variables.map((v) => v.project ?? "-"), 7);
|
|
2054
|
+
const envW = col(data.variables.map((v) => v.environment ?? "-"), 3);
|
|
2055
|
+
const appW = col(data.variables.map((v) => v.app ?? "-"), 3);
|
|
2056
|
+
const header = [
|
|
2057
|
+
"ID".padEnd(idW),
|
|
2058
|
+
"KEY".padEnd(keyW),
|
|
2059
|
+
"PROJECT".padEnd(prjW),
|
|
2060
|
+
"ENV".padEnd(envW),
|
|
2061
|
+
"APP".padEnd(appW)
|
|
2062
|
+
].join(" ");
|
|
2063
|
+
console.log(header);
|
|
2064
|
+
console.log("-".repeat(header.length));
|
|
2065
|
+
for (const v of data.variables) {
|
|
2066
|
+
const row = [
|
|
2067
|
+
v.id.slice(0, idW).padEnd(idW),
|
|
2068
|
+
v.key.padEnd(keyW),
|
|
2069
|
+
(v.project ?? "-").padEnd(prjW),
|
|
2070
|
+
(v.environment ?? "-").padEnd(envW),
|
|
2071
|
+
(v.app ?? "-").padEnd(appW)
|
|
2072
|
+
].join(" ");
|
|
2073
|
+
console.log(row);
|
|
2074
|
+
}
|
|
2075
|
+
console.log(`\n${data.variables.length} variables`);
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
//#endregion
|
|
2080
|
+
//#region src/commands/lock.ts
|
|
2081
|
+
const lockCommand = defineCommand({
|
|
2082
|
+
meta: {
|
|
2083
|
+
name: "lock",
|
|
2084
|
+
description: "Lock a config scope"
|
|
2085
|
+
},
|
|
2086
|
+
args: {
|
|
2087
|
+
env: {
|
|
2088
|
+
type: "string",
|
|
2089
|
+
description: "Environment (development, staging, production)"
|
|
2090
|
+
},
|
|
2091
|
+
app: {
|
|
2092
|
+
type: "string",
|
|
2093
|
+
description: "App name"
|
|
2094
|
+
},
|
|
2095
|
+
project: {
|
|
2096
|
+
type: "string",
|
|
2097
|
+
description: "Project slug"
|
|
2098
|
+
},
|
|
2099
|
+
reason: {
|
|
2100
|
+
type: "string",
|
|
2101
|
+
description: "Reason for locking"
|
|
2102
|
+
}
|
|
2103
|
+
},
|
|
2104
|
+
async run({ args }) {
|
|
2105
|
+
const projectConfig = loadProjectConfig();
|
|
2106
|
+
const project = args.project ?? projectConfig.project;
|
|
2107
|
+
const app = args.app ?? resolveApp();
|
|
2108
|
+
const res = await apiRequest("/v1/env/lock", {
|
|
2109
|
+
project,
|
|
2110
|
+
environment: args.env,
|
|
2111
|
+
app,
|
|
2112
|
+
reason: args.reason
|
|
2113
|
+
});
|
|
2114
|
+
if (!res.ok) {
|
|
2115
|
+
const text = await res.text().catch(() => "");
|
|
2116
|
+
logError(`Failed to lock (${res.status}): ${text}`);
|
|
2117
|
+
}
|
|
2118
|
+
const data = await res.json();
|
|
2119
|
+
console.log(`Locked (${data.id}).`);
|
|
2120
|
+
}
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
//#endregion
|
|
2124
|
+
//#region src/commands/lock-status.ts
|
|
2125
|
+
const lockStatusCommand = defineCommand({
|
|
2126
|
+
meta: {
|
|
2127
|
+
name: "lock-status",
|
|
2128
|
+
description: "Check if config is locked"
|
|
2129
|
+
},
|
|
2130
|
+
args: {
|
|
2131
|
+
env: {
|
|
2132
|
+
type: "string",
|
|
2133
|
+
description: "Environment (development, staging, production)"
|
|
2134
|
+
},
|
|
2135
|
+
app: {
|
|
2136
|
+
type: "string",
|
|
2137
|
+
description: "App name"
|
|
2138
|
+
},
|
|
2139
|
+
project: {
|
|
2140
|
+
type: "string",
|
|
2141
|
+
description: "Project slug"
|
|
2142
|
+
}
|
|
2143
|
+
},
|
|
2144
|
+
async run({ args }) {
|
|
2145
|
+
const projectConfig = loadProjectConfig();
|
|
2146
|
+
const project = args.project ?? projectConfig.project;
|
|
2147
|
+
const app = args.app ?? resolveApp();
|
|
2148
|
+
const res = await apiRequest("/v1/env/lock/status", {
|
|
2149
|
+
project,
|
|
2150
|
+
environment: args.env,
|
|
2151
|
+
app
|
|
2152
|
+
});
|
|
2153
|
+
if (!res.ok) {
|
|
2154
|
+
const text = await res.text().catch(() => "");
|
|
2155
|
+
logError(`Failed to check lock status (${res.status}): ${text}`);
|
|
2156
|
+
}
|
|
2157
|
+
const data = await res.json();
|
|
2158
|
+
if (data.locked) {
|
|
2159
|
+
const parts = [`Locked: ${data.lock.reason ?? "(no reason)"}`];
|
|
2160
|
+
if (data.lock.lockedBy) parts.push(`by ${data.lock.lockedBy}`);
|
|
2161
|
+
if (data.lock.lockedAt) parts.push(`at ${data.lock.lockedAt}`);
|
|
2162
|
+
console.log(parts.join(" "));
|
|
2163
|
+
} else console.log("Unlocked");
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
|
|
2167
|
+
//#endregion
|
|
2168
|
+
//#region src/commands/login.ts
|
|
2169
|
+
const CLIENT_ID = "auix-cli";
|
|
2170
|
+
const POLL_INTERVAL_MS = 5e3;
|
|
2171
|
+
function openBrowser(url) {
|
|
2172
|
+
try {
|
|
2173
|
+
execSync(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
2174
|
+
} catch {}
|
|
2175
|
+
}
|
|
2176
|
+
async function deviceFlow(appUrl) {
|
|
2177
|
+
const base = appUrl.replace(/\/$/, "");
|
|
2178
|
+
const codeRes = await fetch(`${base}/api/auth/device/code`, {
|
|
2179
|
+
method: "POST",
|
|
2180
|
+
headers: { "Content-Type": "application/json" },
|
|
2181
|
+
body: JSON.stringify({ client_id: CLIENT_ID })
|
|
2182
|
+
});
|
|
2183
|
+
if (!codeRes.ok) {
|
|
2184
|
+
const text = await codeRes.text().catch(() => "");
|
|
2185
|
+
logError(`Failed to start device flow (${codeRes.status}): ${text}`);
|
|
2186
|
+
}
|
|
2187
|
+
const codeData = await codeRes.json();
|
|
2188
|
+
console.log("Opening browser for authentication...");
|
|
2189
|
+
console.log(`If the browser doesn't open, visit: ${codeData.verification_uri_complete}`);
|
|
2190
|
+
console.log(`Enter code: ${codeData.user_code}\n`);
|
|
2191
|
+
openBrowser(codeData.verification_uri_complete);
|
|
2192
|
+
const deadline = Date.now() + codeData.expires_in * 1e3;
|
|
2193
|
+
const interval = Math.max((codeData.interval || 5) * 1e3, POLL_INTERVAL_MS);
|
|
2194
|
+
while (Date.now() < deadline) {
|
|
2195
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
2196
|
+
const tokenRes = await fetch(`${base}/api/auth/device/token`, {
|
|
2197
|
+
method: "POST",
|
|
2198
|
+
headers: { "Content-Type": "application/json" },
|
|
2199
|
+
body: JSON.stringify({
|
|
2200
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
2201
|
+
device_code: codeData.device_code,
|
|
2202
|
+
client_id: CLIENT_ID
|
|
2203
|
+
})
|
|
2204
|
+
});
|
|
2205
|
+
if (tokenRes.ok) return (await tokenRes.json()).access_token;
|
|
2206
|
+
const err = await tokenRes.json().catch(() => ({}));
|
|
2207
|
+
if (err.error === "authorization_pending") continue;
|
|
2208
|
+
if (err.error === "slow_down") continue;
|
|
2209
|
+
if (err.error === "expired_token") logError("Device code expired.");
|
|
2210
|
+
if (err.error === "access_denied") logError("Access denied.");
|
|
2211
|
+
logError(`Unexpected error: ${err.error ?? tokenRes.status}`);
|
|
2212
|
+
}
|
|
2213
|
+
logError("Device code expired. Please try again.");
|
|
2214
|
+
return "";
|
|
2215
|
+
}
|
|
2216
|
+
async function selectOrganization(appUrl, token) {
|
|
2217
|
+
const base = appUrl.replace(/\/$/, "");
|
|
2218
|
+
const res = await fetch(`${base}/api/auth/organization/list`, { headers: { Authorization: `Bearer ${token}` } });
|
|
2219
|
+
if (!res.ok) logError(`Failed to list organizations (${res.status}).`);
|
|
2220
|
+
const orgs = await res.json();
|
|
2221
|
+
if (orgs.length === 0) logError("No organizations found. Create one at the web dashboard.");
|
|
2222
|
+
if (orgs.length === 1) {
|
|
2223
|
+
console.log(`Organization: ${orgs[0].name} (${orgs[0].slug})`);
|
|
2224
|
+
return orgs[0];
|
|
2225
|
+
}
|
|
2226
|
+
const selected = await consola.prompt("Select organization", {
|
|
2227
|
+
type: "select",
|
|
2228
|
+
options: orgs.map((o) => ({
|
|
2229
|
+
label: `${o.name} (${o.slug})`,
|
|
2230
|
+
value: o.id
|
|
2231
|
+
}))
|
|
2232
|
+
});
|
|
2233
|
+
if (typeof selected === "symbol") logError("Cancelled.");
|
|
2234
|
+
return orgs.find((o) => o.id === selected);
|
|
2235
|
+
}
|
|
2236
|
+
async function fetchUser(appUrl, token) {
|
|
2237
|
+
const base = appUrl.replace(/\/$/, "");
|
|
2238
|
+
const res = await fetch(`${base}/api/auth/get-session`, { headers: { Authorization: `Bearer ${token}` } });
|
|
2239
|
+
if (!res.ok) return {
|
|
2240
|
+
name: "unknown",
|
|
2241
|
+
email: "unknown"
|
|
2242
|
+
};
|
|
2243
|
+
const data = await res.json();
|
|
2244
|
+
return {
|
|
2245
|
+
name: data.user?.name ?? "unknown",
|
|
2246
|
+
email: data.user?.email ?? "unknown"
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
async function createApiKey$1(appUrl, token, organizationId) {
|
|
2250
|
+
const base = appUrl.replace(/\/$/, "");
|
|
2251
|
+
const res = await fetch(`${base}/api/cli/create-key`, {
|
|
2252
|
+
method: "POST",
|
|
2253
|
+
headers: {
|
|
2254
|
+
"Content-Type": "application/json",
|
|
2255
|
+
Authorization: `Bearer ${token}`
|
|
2256
|
+
},
|
|
2257
|
+
body: JSON.stringify({ organizationId })
|
|
2258
|
+
});
|
|
2259
|
+
if (!res.ok) {
|
|
2260
|
+
const text = await res.text().catch(() => "");
|
|
2261
|
+
logError(`Failed to create API key (${res.status}): ${text}`);
|
|
2262
|
+
}
|
|
2263
|
+
return (await res.json()).key;
|
|
2264
|
+
}
|
|
2265
|
+
const loginCommand = defineCommand({
|
|
1604
2266
|
meta: {
|
|
1605
2267
|
name: "login",
|
|
1606
2268
|
description: "Authenticate with auix"
|
|
@@ -1642,19 +2304,48 @@ const loginCommand = defineCommand({
|
|
|
1642
2304
|
return;
|
|
1643
2305
|
}
|
|
1644
2306
|
const token = await deviceFlow(appUrl);
|
|
1645
|
-
|
|
2307
|
+
const user = await fetchUser(appUrl, token);
|
|
2308
|
+
console.log(`Authenticated as ${user.email}\n`);
|
|
1646
2309
|
const org = await selectOrganization(appUrl, token);
|
|
1647
2310
|
saveGlobalConfig({
|
|
1648
|
-
apiKey: await createApiKey(appUrl, token, org.id),
|
|
2311
|
+
apiKey: await createApiKey$1(appUrl, token, org.id),
|
|
1649
2312
|
baseUrl,
|
|
1650
|
-
appUrl
|
|
2313
|
+
appUrl,
|
|
2314
|
+
user,
|
|
2315
|
+
organization: {
|
|
2316
|
+
name: org.name,
|
|
2317
|
+
slug: org.slug
|
|
2318
|
+
}
|
|
1651
2319
|
});
|
|
1652
2320
|
console.log(`\nLogged in to ${org.name}.`);
|
|
1653
2321
|
}
|
|
1654
2322
|
});
|
|
1655
2323
|
|
|
2324
|
+
//#endregion
|
|
2325
|
+
//#region src/commands/logout.ts
|
|
2326
|
+
const CONFIG_FILE = join(homedir(), ".auix", "config.json");
|
|
2327
|
+
const logoutCommand = defineCommand({
|
|
2328
|
+
meta: {
|
|
2329
|
+
name: "logout",
|
|
2330
|
+
description: "Log out and remove credentials"
|
|
2331
|
+
},
|
|
2332
|
+
async run() {
|
|
2333
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
2334
|
+
console.log("Not logged in.");
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
rmSync(CONFIG_FILE);
|
|
2338
|
+
console.log("Logged out.");
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
|
|
1656
2342
|
//#endregion
|
|
1657
2343
|
//#region src/commands/pull.ts
|
|
2344
|
+
const formatters = {
|
|
2345
|
+
env: formatEnvString,
|
|
2346
|
+
json: formatJsonString,
|
|
2347
|
+
yaml: formatYamlString
|
|
2348
|
+
};
|
|
1658
2349
|
const pullCommand = defineCommand({
|
|
1659
2350
|
meta: {
|
|
1660
2351
|
name: "pull",
|
|
@@ -1663,8 +2354,7 @@ const pullCommand = defineCommand({
|
|
|
1663
2354
|
args: {
|
|
1664
2355
|
env: {
|
|
1665
2356
|
type: "string",
|
|
1666
|
-
description: "Environment (development, staging, production)"
|
|
1667
|
-
default: "development"
|
|
2357
|
+
description: "Environment (development, staging, production)"
|
|
1668
2358
|
},
|
|
1669
2359
|
app: {
|
|
1670
2360
|
type: "string",
|
|
@@ -1674,32 +2364,50 @@ const pullCommand = defineCommand({
|
|
|
1674
2364
|
type: "string",
|
|
1675
2365
|
description: "Project slug"
|
|
1676
2366
|
},
|
|
2367
|
+
branch: {
|
|
2368
|
+
type: "string",
|
|
2369
|
+
description: "Env branch name"
|
|
2370
|
+
},
|
|
1677
2371
|
out: {
|
|
1678
2372
|
type: "string",
|
|
1679
2373
|
description: "Output file path",
|
|
1680
2374
|
default: ".env.local"
|
|
2375
|
+
},
|
|
2376
|
+
format: {
|
|
2377
|
+
type: "string",
|
|
2378
|
+
description: "Output format (env, json, yaml)",
|
|
2379
|
+
default: "env"
|
|
2380
|
+
},
|
|
2381
|
+
interpolate: {
|
|
2382
|
+
type: "boolean",
|
|
2383
|
+
description: "Interpolate variable references",
|
|
2384
|
+
default: false
|
|
1681
2385
|
}
|
|
1682
2386
|
},
|
|
1683
2387
|
async run({ args }) {
|
|
1684
2388
|
const projectConfig = loadProjectConfig();
|
|
1685
2389
|
const project = args.project ?? projectConfig.project;
|
|
1686
2390
|
const app = args.app ?? resolveApp();
|
|
2391
|
+
const formatter = formatters[args.format];
|
|
2392
|
+
if (!formatter) logError(`Unknown format: ${args.format}. Use env, json, or yaml.`);
|
|
1687
2393
|
const res = await apiRequest("/v1/env/resolve", {
|
|
1688
2394
|
project,
|
|
1689
2395
|
environment: args.env,
|
|
1690
|
-
app
|
|
2396
|
+
app,
|
|
2397
|
+
branch: args.branch
|
|
1691
2398
|
});
|
|
1692
2399
|
if (!res.ok) {
|
|
1693
2400
|
const text = await res.text().catch(() => "");
|
|
1694
2401
|
logError(`Failed to resolve env (${res.status}): ${text}`);
|
|
1695
2402
|
}
|
|
1696
|
-
|
|
1697
|
-
const count = Object.keys(
|
|
2403
|
+
let variables = (await res.json()).variables;
|
|
2404
|
+
const count = Object.keys(variables).length;
|
|
1698
2405
|
if (count === 0) {
|
|
1699
2406
|
console.log("No variables found.");
|
|
1700
2407
|
return;
|
|
1701
2408
|
}
|
|
1702
|
-
|
|
2409
|
+
if (args.interpolate) variables = interpolateEnv(variables);
|
|
2410
|
+
const content = formatter(variables);
|
|
1703
2411
|
writeFileSync(args.out, `${content}\n`);
|
|
1704
2412
|
console.log(`Pulled ${count} variables → ${args.out}`);
|
|
1705
2413
|
}
|
|
@@ -1720,8 +2428,7 @@ const pushCommand = defineCommand({
|
|
|
1720
2428
|
},
|
|
1721
2429
|
env: {
|
|
1722
2430
|
type: "string",
|
|
1723
|
-
description: "Environment (development, staging, production)"
|
|
1724
|
-
default: "development"
|
|
2431
|
+
description: "Environment (development, staging, production)"
|
|
1725
2432
|
},
|
|
1726
2433
|
app: {
|
|
1727
2434
|
type: "string",
|
|
@@ -1731,6 +2438,10 @@ const pushCommand = defineCommand({
|
|
|
1731
2438
|
type: "string",
|
|
1732
2439
|
description: "Project slug"
|
|
1733
2440
|
},
|
|
2441
|
+
branch: {
|
|
2442
|
+
type: "string",
|
|
2443
|
+
description: "Env branch name"
|
|
2444
|
+
},
|
|
1734
2445
|
overwrite: {
|
|
1735
2446
|
type: "boolean",
|
|
1736
2447
|
description: "Overwrite existing variables",
|
|
@@ -1748,11 +2459,21 @@ const pushCommand = defineCommand({
|
|
|
1748
2459
|
logError(`Cannot read file: ${args.file}`);
|
|
1749
2460
|
return;
|
|
1750
2461
|
}
|
|
2462
|
+
const parsed = parseEnvString(content);
|
|
2463
|
+
const variables = Object.entries(parsed).map(([key, value]) => ({
|
|
2464
|
+
key,
|
|
2465
|
+
value
|
|
2466
|
+
}));
|
|
2467
|
+
if (variables.length === 0) {
|
|
2468
|
+
console.log("No variables found in file.");
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
1751
2471
|
const res = await apiRequest("/v1/env/import", {
|
|
1752
|
-
|
|
2472
|
+
variables,
|
|
1753
2473
|
project,
|
|
1754
2474
|
environment: args.env,
|
|
1755
2475
|
app,
|
|
2476
|
+
branch: args.branch,
|
|
1756
2477
|
overwrite: args.overwrite
|
|
1757
2478
|
});
|
|
1758
2479
|
if (!res.ok) {
|
|
@@ -1765,22 +2486,26 @@ const pushCommand = defineCommand({
|
|
|
1765
2486
|
});
|
|
1766
2487
|
|
|
1767
2488
|
//#endregion
|
|
1768
|
-
//#region src/commands/
|
|
1769
|
-
const
|
|
2489
|
+
//#region src/commands/rollback.ts
|
|
2490
|
+
const rollbackCommand = defineCommand({
|
|
1770
2491
|
meta: {
|
|
1771
|
-
name: "
|
|
1772
|
-
description: "
|
|
2492
|
+
name: "rollback",
|
|
2493
|
+
description: "Rollback a variable to a previous version"
|
|
1773
2494
|
},
|
|
1774
2495
|
args: {
|
|
1775
|
-
|
|
2496
|
+
key: {
|
|
1776
2497
|
type: "positional",
|
|
1777
|
-
description: "
|
|
2498
|
+
description: "Variable key",
|
|
2499
|
+
required: true
|
|
2500
|
+
},
|
|
2501
|
+
version: {
|
|
2502
|
+
type: "positional",
|
|
2503
|
+
description: "Version number to rollback to",
|
|
1778
2504
|
required: true
|
|
1779
2505
|
},
|
|
1780
2506
|
env: {
|
|
1781
2507
|
type: "string",
|
|
1782
|
-
description: "Environment (development, staging, production)"
|
|
1783
|
-
default: "development"
|
|
2508
|
+
description: "Environment (development, staging, production)"
|
|
1784
2509
|
},
|
|
1785
2510
|
app: {
|
|
1786
2511
|
type: "string",
|
|
@@ -1790,34 +2515,1126 @@ const setCommand = defineCommand({
|
|
|
1790
2515
|
type: "string",
|
|
1791
2516
|
description: "Project slug"
|
|
1792
2517
|
},
|
|
1793
|
-
|
|
1794
|
-
type: "
|
|
1795
|
-
description: "
|
|
1796
|
-
default: true
|
|
2518
|
+
branch: {
|
|
2519
|
+
type: "string",
|
|
2520
|
+
description: "Env branch name"
|
|
1797
2521
|
}
|
|
1798
2522
|
},
|
|
1799
2523
|
async run({ args }) {
|
|
1800
|
-
const
|
|
1801
|
-
if (
|
|
1802
|
-
const
|
|
1803
|
-
|
|
1804
|
-
|
|
2524
|
+
const versionNum = Number.parseInt(args.version, 10);
|
|
2525
|
+
if (Number.isNaN(versionNum) || versionNum <= 0) logError("Version must be a positive integer.");
|
|
2526
|
+
const confirmed = await consola.prompt(`Rollback ${args.key} to v${versionNum}?`, { type: "confirm" });
|
|
2527
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
2528
|
+
console.log("Cancelled.");
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
1805
2531
|
const projectConfig = loadProjectConfig();
|
|
1806
2532
|
const project = args.project ?? projectConfig.project;
|
|
1807
2533
|
const app = args.app ?? resolveApp();
|
|
1808
|
-
const res = await apiRequest("/v1/env/
|
|
1809
|
-
key,
|
|
1810
|
-
|
|
2534
|
+
const res = await apiRequest("/v1/env/rollback", {
|
|
2535
|
+
key: args.key,
|
|
2536
|
+
version: versionNum,
|
|
1811
2537
|
project,
|
|
1812
2538
|
environment: args.env,
|
|
1813
2539
|
app,
|
|
1814
|
-
|
|
2540
|
+
branch: args.branch
|
|
1815
2541
|
});
|
|
1816
2542
|
if (!res.ok) {
|
|
1817
2543
|
const text = await res.text().catch(() => "");
|
|
1818
|
-
logError(`Failed to
|
|
2544
|
+
logError(`Failed to rollback (${res.status}): ${text}`);
|
|
1819
2545
|
}
|
|
1820
|
-
console.log(`
|
|
2546
|
+
console.log(`Rolled back ${args.key} to v${versionNum}.`);
|
|
2547
|
+
}
|
|
2548
|
+
});
|
|
2549
|
+
|
|
2550
|
+
//#endregion
|
|
2551
|
+
//#region src/commands/rotation-create.ts
|
|
2552
|
+
const rotationCreateCommand = defineCommand({
|
|
2553
|
+
meta: {
|
|
2554
|
+
name: "rotation create",
|
|
2555
|
+
description: "Set up a rotation policy for an env variable"
|
|
2556
|
+
},
|
|
2557
|
+
args: {
|
|
2558
|
+
key: {
|
|
2559
|
+
type: "string",
|
|
2560
|
+
description: "Environment variable key",
|
|
2561
|
+
required: true
|
|
2562
|
+
},
|
|
2563
|
+
generator: {
|
|
2564
|
+
type: "string",
|
|
2565
|
+
description: "Generator (random-password, random-token, random-uuid)",
|
|
2566
|
+
required: true
|
|
2567
|
+
},
|
|
2568
|
+
interval: {
|
|
2569
|
+
type: "string",
|
|
2570
|
+
description: "Rotation interval in days",
|
|
2571
|
+
required: true
|
|
2572
|
+
},
|
|
2573
|
+
env: {
|
|
2574
|
+
type: "string",
|
|
2575
|
+
description: "Scope: environment"
|
|
2576
|
+
},
|
|
2577
|
+
app: {
|
|
2578
|
+
type: "string",
|
|
2579
|
+
description: "Scope: app name"
|
|
2580
|
+
},
|
|
2581
|
+
project: {
|
|
2582
|
+
type: "string",
|
|
2583
|
+
description: "Scope: project slug"
|
|
2584
|
+
}
|
|
2585
|
+
},
|
|
2586
|
+
async run({ args }) {
|
|
2587
|
+
const intervalDays = Number.parseInt(args.interval, 10);
|
|
2588
|
+
if (Number.isNaN(intervalDays) || intervalDays <= 0) logError("--interval must be a positive number of days.");
|
|
2589
|
+
const body = {
|
|
2590
|
+
key: args.key,
|
|
2591
|
+
generator: args.generator,
|
|
2592
|
+
intervalDays
|
|
2593
|
+
};
|
|
2594
|
+
if (args.project) body.project = args.project;
|
|
2595
|
+
if (args.env) body.environment = args.env;
|
|
2596
|
+
if (args.app) body.app = args.app;
|
|
2597
|
+
const res = await apiRequest("/v1/env/rotation/create", body);
|
|
2598
|
+
if (!res.ok) {
|
|
2599
|
+
const text = await res.text().catch(() => "");
|
|
2600
|
+
logError(`Failed to create rotation policy (${res.status}): ${text}`);
|
|
2601
|
+
}
|
|
2602
|
+
const data = await res.json();
|
|
2603
|
+
console.log(`\nRotation policy created: ${data.id}`);
|
|
2604
|
+
console.log(`Key: ${args.key}`);
|
|
2605
|
+
console.log(`Generator: ${args.generator}`);
|
|
2606
|
+
console.log(`Interval: ${intervalDays} days`);
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
|
|
2610
|
+
//#endregion
|
|
2611
|
+
//#region src/commands/rotation-list.ts
|
|
2612
|
+
function formatDate$1(date) {
|
|
2613
|
+
if (!date) return "-";
|
|
2614
|
+
return new Date(date).toLocaleDateString();
|
|
2615
|
+
}
|
|
2616
|
+
const rotationListCommand = defineCommand({
|
|
2617
|
+
meta: {
|
|
2618
|
+
name: "rotation list",
|
|
2619
|
+
description: "List rotation policies"
|
|
2620
|
+
},
|
|
2621
|
+
args: {},
|
|
2622
|
+
async run() {
|
|
2623
|
+
const res = await apiRequest("/v1/env/rotation/list", {});
|
|
2624
|
+
if (!res.ok) {
|
|
2625
|
+
const text = await res.text().catch(() => "");
|
|
2626
|
+
logError(`Failed to list rotation policies (${res.status}): ${text}`);
|
|
2627
|
+
}
|
|
2628
|
+
const data = await res.json();
|
|
2629
|
+
if (data.policies.length === 0) {
|
|
2630
|
+
console.log("No rotation policies found.");
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
2634
|
+
const idW = 8;
|
|
2635
|
+
const keyW = col(data.policies.map((p) => p.key), 3);
|
|
2636
|
+
const genW = col(data.policies.map((p) => p.generator), 9);
|
|
2637
|
+
const intW = col(data.policies.map((p) => `${p.intervalDays}d`), 8);
|
|
2638
|
+
const lastW = col(data.policies.map((p) => formatDate$1(p.lastRotatedAt)), 12);
|
|
2639
|
+
const nextW = col(data.policies.map((p) => formatDate$1(p.nextRotationAt)), 13);
|
|
2640
|
+
const header = [
|
|
2641
|
+
"ID".padEnd(idW),
|
|
2642
|
+
"KEY".padEnd(keyW),
|
|
2643
|
+
"GENERATOR".padEnd(genW),
|
|
2644
|
+
"INTERVAL".padEnd(intW),
|
|
2645
|
+
"LAST ROTATED".padEnd(lastW),
|
|
2646
|
+
"NEXT ROTATION".padEnd(nextW)
|
|
2647
|
+
].join(" ");
|
|
2648
|
+
console.log(header);
|
|
2649
|
+
console.log("-".repeat(header.length));
|
|
2650
|
+
for (const p of data.policies) {
|
|
2651
|
+
const row = [
|
|
2652
|
+
p.id.slice(0, idW).padEnd(idW),
|
|
2653
|
+
p.key.padEnd(keyW),
|
|
2654
|
+
p.generator.padEnd(genW),
|
|
2655
|
+
`${p.intervalDays}d`.padEnd(intW),
|
|
2656
|
+
formatDate$1(p.lastRotatedAt).padEnd(lastW),
|
|
2657
|
+
formatDate$1(p.nextRotationAt).padEnd(nextW)
|
|
2658
|
+
].join(" ");
|
|
2659
|
+
console.log(row);
|
|
2660
|
+
}
|
|
2661
|
+
console.log(`\n${data.policies.length} policies`);
|
|
2662
|
+
}
|
|
2663
|
+
});
|
|
2664
|
+
|
|
2665
|
+
//#endregion
|
|
2666
|
+
//#region src/commands/rotation-rotate.ts
|
|
2667
|
+
const rotationRotateCommand = defineCommand({
|
|
2668
|
+
meta: {
|
|
2669
|
+
name: "rotation rotate",
|
|
2670
|
+
description: "Manually trigger rotation for a policy"
|
|
2671
|
+
},
|
|
2672
|
+
args: { id: {
|
|
2673
|
+
type: "positional",
|
|
2674
|
+
description: "Policy ID (from `auix env rotation list`)",
|
|
2675
|
+
required: true
|
|
2676
|
+
} },
|
|
2677
|
+
async run({ args }) {
|
|
2678
|
+
const confirmed = await consola.prompt(`Rotate credentials for policy ${args.id}?`, { type: "confirm" });
|
|
2679
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
2680
|
+
console.log("Cancelled.");
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
const res = await apiRequest("/v1/env/rotation/rotate", { id: args.id });
|
|
2684
|
+
if (!res.ok) {
|
|
2685
|
+
const text = await res.text().catch(() => "");
|
|
2686
|
+
logError(`Failed to rotate (${res.status}): ${text}`);
|
|
2687
|
+
}
|
|
2688
|
+
console.log("Rotated successfully.");
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
|
|
2692
|
+
//#endregion
|
|
2693
|
+
//#region src/commands/run.ts
|
|
2694
|
+
const FINGERPRINT_DIR = join(homedir(), ".auix");
|
|
2695
|
+
const FINGERPRINT_FILE = join(FINGERPRINT_DIR, "fingerprint");
|
|
2696
|
+
function computeHash(vars) {
|
|
2697
|
+
const input = Object.entries(vars).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join("\n");
|
|
2698
|
+
return createHash("sha256").update(input).digest("hex");
|
|
2699
|
+
}
|
|
2700
|
+
function getFingerprint() {
|
|
2701
|
+
if (existsSync(FINGERPRINT_FILE)) {
|
|
2702
|
+
const stored = readFileSync(FINGERPRINT_FILE, "utf-8").trim();
|
|
2703
|
+
if (stored) return stored;
|
|
2704
|
+
}
|
|
2705
|
+
const id = randomUUID();
|
|
2706
|
+
mkdirSync(FINGERPRINT_DIR, { recursive: true });
|
|
2707
|
+
writeFileSync(FINGERPRINT_FILE, id);
|
|
2708
|
+
return id;
|
|
2709
|
+
}
|
|
2710
|
+
async function ensureToken(project) {
|
|
2711
|
+
const apiKey = getApiKey();
|
|
2712
|
+
if (apiKey) return apiKey;
|
|
2713
|
+
const session = loadSession();
|
|
2714
|
+
if (session && session.project === project) return session.token;
|
|
2715
|
+
const fingerprint = getFingerprint();
|
|
2716
|
+
let gitRemote;
|
|
2717
|
+
try {
|
|
2718
|
+
gitRemote = execSync("git remote get-url origin", {
|
|
2719
|
+
encoding: "utf-8",
|
|
2720
|
+
timeout: 3e3,
|
|
2721
|
+
stdio: [
|
|
2722
|
+
"pipe",
|
|
2723
|
+
"pipe",
|
|
2724
|
+
"pipe"
|
|
2725
|
+
]
|
|
2726
|
+
}).trim();
|
|
2727
|
+
} catch {}
|
|
2728
|
+
const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI);
|
|
2729
|
+
const res = await apiRequestNoAuth("/v1/auth/anonymous/register", {
|
|
2730
|
+
fingerprint,
|
|
2731
|
+
project,
|
|
2732
|
+
meta: {
|
|
2733
|
+
os: process.platform,
|
|
2734
|
+
arch: process.arch,
|
|
2735
|
+
nodeVersion: process.version,
|
|
2736
|
+
cliVersion: "0.0.3",
|
|
2737
|
+
shell: process.env.SHELL ?? process.env.COMSPEC,
|
|
2738
|
+
ci: isCI || void 0,
|
|
2739
|
+
ciName: process.env.GITHUB_ACTIONS ? "github-actions" : process.env.GITLAB_CI ? "gitlab-ci" : process.env.CIRCLECI ? "circleci" : void 0,
|
|
2740
|
+
gitRemote,
|
|
2741
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
2742
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale
|
|
2743
|
+
}
|
|
2744
|
+
});
|
|
2745
|
+
if (!res.ok) {
|
|
2746
|
+
const text = await res.text().catch(() => "");
|
|
2747
|
+
if (res.status === 403) logError("Contributor access is not enabled for this project.");
|
|
2748
|
+
if (res.status === 429) {
|
|
2749
|
+
const data = JSON.parse(text);
|
|
2750
|
+
logError(`Rate limited. Resets at ${data.resetsAt ? new Date(data.resetsAt).toLocaleString() : "later"}.\n Run \`auix login\` to let project owners know who you are.`);
|
|
2751
|
+
}
|
|
2752
|
+
logError(`Failed to register anonymous session (${res.status}): ${text}`);
|
|
2753
|
+
}
|
|
2754
|
+
const data = await res.json();
|
|
2755
|
+
saveSession({
|
|
2756
|
+
token: data.token,
|
|
2757
|
+
fingerprint,
|
|
2758
|
+
project,
|
|
2759
|
+
expiresAt: data.expiresAt
|
|
2760
|
+
});
|
|
2761
|
+
console.log("Created anonymous session (expires %s)", data.expiresAt);
|
|
2762
|
+
return data.token;
|
|
2763
|
+
}
|
|
2764
|
+
const runCommand = defineCommand({
|
|
2765
|
+
meta: {
|
|
2766
|
+
name: "run",
|
|
2767
|
+
description: "Run a command with resolved environment variables"
|
|
2768
|
+
},
|
|
2769
|
+
args: {
|
|
2770
|
+
env: {
|
|
2771
|
+
type: "string",
|
|
2772
|
+
description: "Environment (development, staging, production)"
|
|
2773
|
+
},
|
|
2774
|
+
app: {
|
|
2775
|
+
type: "string",
|
|
2776
|
+
description: "App name"
|
|
2777
|
+
},
|
|
2778
|
+
project: {
|
|
2779
|
+
type: "string",
|
|
2780
|
+
description: "Project slug"
|
|
2781
|
+
},
|
|
2782
|
+
branch: {
|
|
2783
|
+
type: "string",
|
|
2784
|
+
description: "Env branch name"
|
|
2785
|
+
},
|
|
2786
|
+
interpolate: {
|
|
2787
|
+
type: "boolean",
|
|
2788
|
+
description: "Interpolate variable references",
|
|
2789
|
+
default: false
|
|
2790
|
+
},
|
|
2791
|
+
watch: {
|
|
2792
|
+
type: "boolean",
|
|
2793
|
+
description: "Watch for changes and restart",
|
|
2794
|
+
default: false
|
|
2795
|
+
},
|
|
2796
|
+
"watch-interval": {
|
|
2797
|
+
type: "string",
|
|
2798
|
+
description: "Poll interval in ms",
|
|
2799
|
+
default: "5000"
|
|
2800
|
+
}
|
|
2801
|
+
},
|
|
2802
|
+
async run({ args }) {
|
|
2803
|
+
const dashIndex = process.argv.indexOf("--");
|
|
2804
|
+
const command = dashIndex !== -1 ? process.argv.slice(dashIndex + 1) : [];
|
|
2805
|
+
if (command.length === 0) logError("No command specified. Usage: auix run -- <command>");
|
|
2806
|
+
const projectConfig = loadProjectConfig();
|
|
2807
|
+
const projectSlug = args.project ?? projectConfig.project;
|
|
2808
|
+
const app = args.app ?? resolveApp();
|
|
2809
|
+
if (!projectSlug) logError("No project specified. Use --project or create .auixrc with `auix env init`.");
|
|
2810
|
+
const token = await ensureToken(projectSlug);
|
|
2811
|
+
const resolvePayload = {
|
|
2812
|
+
project: projectSlug,
|
|
2813
|
+
environment: args.env,
|
|
2814
|
+
app,
|
|
2815
|
+
branch: args.branch
|
|
2816
|
+
};
|
|
2817
|
+
const resolveVars = async () => {
|
|
2818
|
+
const res = await apiRequestWithToken("/v1/env/resolve", resolvePayload, token);
|
|
2819
|
+
if (!res.ok) {
|
|
2820
|
+
const text = await res.text().catch(() => "");
|
|
2821
|
+
logError(`Failed to resolve env (${res.status}): ${text}`);
|
|
2822
|
+
}
|
|
2823
|
+
let variables = (await res.json()).variables;
|
|
2824
|
+
if (args.interpolate) variables = interpolateEnv(variables);
|
|
2825
|
+
return variables;
|
|
2826
|
+
};
|
|
2827
|
+
const spawnChild = (vars) => {
|
|
2828
|
+
const count = Object.keys(vars).length;
|
|
2829
|
+
console.log(`Injecting ${count} variables`);
|
|
2830
|
+
return spawn(command[0], command.slice(1), {
|
|
2831
|
+
stdio: "inherit",
|
|
2832
|
+
env: {
|
|
2833
|
+
...process.env,
|
|
2834
|
+
...vars
|
|
2835
|
+
}
|
|
2836
|
+
});
|
|
2837
|
+
};
|
|
2838
|
+
let variables = await resolveVars();
|
|
2839
|
+
let child = spawnChild(variables);
|
|
2840
|
+
if (!args.watch) {
|
|
2841
|
+
child.on("close", (code) => {
|
|
2842
|
+
process.exit(code ?? 1);
|
|
2843
|
+
});
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
let currentHash = computeHash(variables);
|
|
2847
|
+
const interval = Number.parseInt(args["watch-interval"], 10);
|
|
2848
|
+
console.log("Watching for changes...");
|
|
2849
|
+
const timer = setInterval(async () => {
|
|
2850
|
+
try {
|
|
2851
|
+
const res = await apiRequestWithToken("/v1/env/check", resolvePayload, token);
|
|
2852
|
+
if (!res.ok) return;
|
|
2853
|
+
if ((await res.json()).hash === currentHash) return;
|
|
2854
|
+
console.log("Env vars changed, restarting...");
|
|
2855
|
+
child.kill("SIGTERM");
|
|
2856
|
+
variables = await resolveVars();
|
|
2857
|
+
currentHash = computeHash(variables);
|
|
2858
|
+
child = spawnChild(variables);
|
|
2859
|
+
} catch {}
|
|
2860
|
+
}, interval);
|
|
2861
|
+
const cleanup = () => {
|
|
2862
|
+
clearInterval(timer);
|
|
2863
|
+
child.kill("SIGTERM");
|
|
2864
|
+
process.exit(0);
|
|
2865
|
+
};
|
|
2866
|
+
process.on("SIGINT", cleanup);
|
|
2867
|
+
process.on("SIGTERM", cleanup);
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2870
|
+
|
|
2871
|
+
//#endregion
|
|
2872
|
+
//#region src/commands/set.ts
|
|
2873
|
+
const setCommand = defineCommand({
|
|
2874
|
+
meta: {
|
|
2875
|
+
name: "set",
|
|
2876
|
+
description: "Set a single environment variable"
|
|
2877
|
+
},
|
|
2878
|
+
args: {
|
|
2879
|
+
pair: {
|
|
2880
|
+
type: "positional",
|
|
2881
|
+
description: "KEY=VALUE pair",
|
|
2882
|
+
required: true
|
|
2883
|
+
},
|
|
2884
|
+
env: {
|
|
2885
|
+
type: "string",
|
|
2886
|
+
description: "Environment (development, staging, production)"
|
|
2887
|
+
},
|
|
2888
|
+
app: {
|
|
2889
|
+
type: "string",
|
|
2890
|
+
description: "App name"
|
|
2891
|
+
},
|
|
2892
|
+
project: {
|
|
2893
|
+
type: "string",
|
|
2894
|
+
description: "Project slug"
|
|
2895
|
+
},
|
|
2896
|
+
branch: {
|
|
2897
|
+
type: "string",
|
|
2898
|
+
description: "Env branch name"
|
|
2899
|
+
},
|
|
2900
|
+
secret: {
|
|
2901
|
+
type: "boolean",
|
|
2902
|
+
description: "Mark as secret",
|
|
2903
|
+
default: true
|
|
2904
|
+
},
|
|
2905
|
+
description: {
|
|
2906
|
+
type: "string",
|
|
2907
|
+
description: "Variable description"
|
|
2908
|
+
}
|
|
2909
|
+
},
|
|
2910
|
+
async run({ args }) {
|
|
2911
|
+
const eqIndex = args.pair.indexOf("=");
|
|
2912
|
+
if (eqIndex === -1) logError("Expected KEY=VALUE format.");
|
|
2913
|
+
const key = args.pair.slice(0, eqIndex);
|
|
2914
|
+
const value = args.pair.slice(eqIndex + 1);
|
|
2915
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) logError("Invalid key. Use uppercase letters, digits, and underscores.");
|
|
2916
|
+
const projectConfig = loadProjectConfig();
|
|
2917
|
+
const project = args.project ?? projectConfig.project;
|
|
2918
|
+
const app = args.app ?? resolveApp();
|
|
2919
|
+
const res = await apiRequest("/v1/env/set", {
|
|
2920
|
+
key,
|
|
2921
|
+
value,
|
|
2922
|
+
project,
|
|
2923
|
+
environment: args.env,
|
|
2924
|
+
app,
|
|
2925
|
+
branch: args.branch,
|
|
2926
|
+
isSecret: args.secret,
|
|
2927
|
+
description: args.description
|
|
2928
|
+
});
|
|
2929
|
+
if (!res.ok) {
|
|
2930
|
+
const text = await res.text().catch(() => "");
|
|
2931
|
+
logError(`Failed to set variable (${res.status}): ${text}`);
|
|
2932
|
+
}
|
|
2933
|
+
console.log(`Set ${key}`);
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
|
|
2937
|
+
//#endregion
|
|
2938
|
+
//#region src/commands/share-access.ts
|
|
2939
|
+
const shareAccessCommand = defineCommand({
|
|
2940
|
+
meta: {
|
|
2941
|
+
name: "share access",
|
|
2942
|
+
description: "Access a shared secret by ID"
|
|
2943
|
+
},
|
|
2944
|
+
args: { id: {
|
|
2945
|
+
type: "positional",
|
|
2946
|
+
description: "The share link ID",
|
|
2947
|
+
required: true
|
|
2948
|
+
} },
|
|
2949
|
+
async run({ args }) {
|
|
2950
|
+
const res = await apiRequest("/v1/env/share/access", { id: args.id });
|
|
2951
|
+
if (res.status === 404) logError("Share link not found.");
|
|
2952
|
+
if (res.status === 410) logError("Share link has expired or has already been accessed.");
|
|
2953
|
+
if (!res.ok) {
|
|
2954
|
+
const text = await res.text().catch(() => "");
|
|
2955
|
+
logError(`Failed to access share link (${res.status}): ${text}`);
|
|
2956
|
+
}
|
|
2957
|
+
const data = await res.json();
|
|
2958
|
+
console.log(data.value);
|
|
2959
|
+
}
|
|
2960
|
+
});
|
|
2961
|
+
|
|
2962
|
+
//#endregion
|
|
2963
|
+
//#region src/commands/share-create.ts
|
|
2964
|
+
const shareCreateCommand = defineCommand({
|
|
2965
|
+
meta: {
|
|
2966
|
+
name: "share create",
|
|
2967
|
+
description: "Create a one-time share link for a secret"
|
|
2968
|
+
},
|
|
2969
|
+
args: {
|
|
2970
|
+
value: {
|
|
2971
|
+
type: "positional",
|
|
2972
|
+
description: "The secret value to share",
|
|
2973
|
+
required: true
|
|
2974
|
+
},
|
|
2975
|
+
expires: {
|
|
2976
|
+
type: "string",
|
|
2977
|
+
description: "Expiry in minutes (default: 60, max: 10080)",
|
|
2978
|
+
default: "60"
|
|
2979
|
+
}
|
|
2980
|
+
},
|
|
2981
|
+
async run({ args }) {
|
|
2982
|
+
const expiresInMinutes = Number.parseInt(args.expires, 10);
|
|
2983
|
+
if (Number.isNaN(expiresInMinutes) || expiresInMinutes < 1 || expiresInMinutes > 10080) logError("--expires must be between 1 and 10080 minutes (7 days).");
|
|
2984
|
+
const res = await apiRequest("/v1/env/share/create", {
|
|
2985
|
+
value: args.value,
|
|
2986
|
+
expiresInMinutes
|
|
2987
|
+
});
|
|
2988
|
+
if (!res.ok) {
|
|
2989
|
+
const text = await res.text().catch(() => "");
|
|
2990
|
+
logError(`Failed to create share link (${res.status}): ${text}`);
|
|
2991
|
+
}
|
|
2992
|
+
const data = await res.json();
|
|
2993
|
+
const appUrl = getAppUrl().replace(/\/$/, "");
|
|
2994
|
+
console.log(`\nShare URL: ${appUrl}/share/${data.id}`);
|
|
2995
|
+
console.log(`Expires at: ${data.expiresAt}`);
|
|
2996
|
+
console.log("\nThis link can only be accessed once.");
|
|
2997
|
+
}
|
|
2998
|
+
});
|
|
2999
|
+
|
|
3000
|
+
//#endregion
|
|
3001
|
+
//#region src/commands/switch.ts
|
|
3002
|
+
async function createApiKey(appUrl, token, organizationId) {
|
|
3003
|
+
const base = appUrl.replace(/\/$/, "");
|
|
3004
|
+
const res = await fetch(`${base}/api/cli/create-key`, {
|
|
3005
|
+
method: "POST",
|
|
3006
|
+
headers: {
|
|
3007
|
+
"Content-Type": "application/json",
|
|
3008
|
+
Authorization: `Bearer ${token}`
|
|
3009
|
+
},
|
|
3010
|
+
body: JSON.stringify({ organizationId })
|
|
3011
|
+
});
|
|
3012
|
+
if (!res.ok) {
|
|
3013
|
+
const text = await res.text().catch(() => "");
|
|
3014
|
+
logError(`Failed to create API key (${res.status}): ${text}`);
|
|
3015
|
+
}
|
|
3016
|
+
return (await res.json()).key;
|
|
3017
|
+
}
|
|
3018
|
+
const switchCommand = defineCommand({
|
|
3019
|
+
meta: {
|
|
3020
|
+
name: "switch",
|
|
3021
|
+
description: "Switch to a different organization"
|
|
3022
|
+
},
|
|
3023
|
+
async run() {
|
|
3024
|
+
const config = loadGlobalConfig();
|
|
3025
|
+
const appUrl = config.appUrl ?? getAppUrl();
|
|
3026
|
+
if (!config.apiKey) logError("Not logged in. Run `auix login`.");
|
|
3027
|
+
const base = appUrl.replace(/\/$/, "");
|
|
3028
|
+
consola.info("Re-authenticating to switch organization...\n");
|
|
3029
|
+
const codeRes = await fetch(`${base}/api/auth/device/code`, {
|
|
3030
|
+
method: "POST",
|
|
3031
|
+
headers: { "Content-Type": "application/json" },
|
|
3032
|
+
body: JSON.stringify({ client_id: "auix-cli" })
|
|
3033
|
+
});
|
|
3034
|
+
if (!codeRes.ok) logError("Failed to start authentication. Is the server running?");
|
|
3035
|
+
const codeData = await codeRes.json();
|
|
3036
|
+
console.log(`Open: ${codeData.verification_uri_complete}`);
|
|
3037
|
+
console.log(`Code: ${codeData.user_code}\n`);
|
|
3038
|
+
try {
|
|
3039
|
+
const { execSync } = await import("node:child_process");
|
|
3040
|
+
execSync(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} ${JSON.stringify(codeData.verification_uri_complete)}`, { stdio: "ignore" });
|
|
3041
|
+
} catch {}
|
|
3042
|
+
const deadline = Date.now() + codeData.expires_in * 1e3;
|
|
3043
|
+
const interval = Math.max((codeData.interval || 5) * 1e3, 5e3);
|
|
3044
|
+
let token = "";
|
|
3045
|
+
while (Date.now() < deadline) {
|
|
3046
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
3047
|
+
const tokenRes = await fetch(`${base}/api/auth/device/token`, {
|
|
3048
|
+
method: "POST",
|
|
3049
|
+
headers: { "Content-Type": "application/json" },
|
|
3050
|
+
body: JSON.stringify({
|
|
3051
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
3052
|
+
device_code: codeData.device_code,
|
|
3053
|
+
client_id: "auix-cli"
|
|
3054
|
+
})
|
|
3055
|
+
});
|
|
3056
|
+
if (tokenRes.ok) {
|
|
3057
|
+
token = (await tokenRes.json()).access_token;
|
|
3058
|
+
break;
|
|
3059
|
+
}
|
|
3060
|
+
const err = await tokenRes.json().catch(() => ({}));
|
|
3061
|
+
if (err.error === "authorization_pending" || err.error === "slow_down") continue;
|
|
3062
|
+
if (err.error === "expired_token") logError("Code expired.");
|
|
3063
|
+
if (err.error === "access_denied") logError("Access denied.");
|
|
3064
|
+
logError(`Unexpected error: ${err.error ?? tokenRes.status}`);
|
|
3065
|
+
}
|
|
3066
|
+
if (!token) logError("Code expired. Try again.");
|
|
3067
|
+
const orgsRes = await fetch(`${base}/api/auth/organization/list`, { headers: { Authorization: `Bearer ${token}` } });
|
|
3068
|
+
if (!orgsRes.ok) logError("Failed to list organizations.");
|
|
3069
|
+
const orgs = await orgsRes.json();
|
|
3070
|
+
if (orgs.length === 0) logError("No organizations found.");
|
|
3071
|
+
const selected = await consola.prompt("Select organization", {
|
|
3072
|
+
type: "select",
|
|
3073
|
+
options: orgs.map((o) => ({
|
|
3074
|
+
label: `${o.name} (${o.slug})`,
|
|
3075
|
+
value: o.id
|
|
3076
|
+
}))
|
|
3077
|
+
});
|
|
3078
|
+
if (typeof selected === "symbol") logError("Cancelled.");
|
|
3079
|
+
const org = orgs.find((o) => o.id === selected);
|
|
3080
|
+
const apiKey = await createApiKey(appUrl, token, org.id);
|
|
3081
|
+
saveGlobalConfig({
|
|
3082
|
+
...config,
|
|
3083
|
+
apiKey,
|
|
3084
|
+
organization: {
|
|
3085
|
+
name: org.name,
|
|
3086
|
+
slug: org.slug
|
|
3087
|
+
}
|
|
3088
|
+
});
|
|
3089
|
+
console.log(`Switched to ${org.name}.`);
|
|
3090
|
+
}
|
|
3091
|
+
});
|
|
3092
|
+
|
|
3093
|
+
//#endregion
|
|
3094
|
+
//#region src/commands/sync-add.ts
|
|
3095
|
+
const syncAddCommand = defineCommand({
|
|
3096
|
+
meta: {
|
|
3097
|
+
name: "sync add",
|
|
3098
|
+
description: "Add an env sync target"
|
|
3099
|
+
},
|
|
3100
|
+
args: {
|
|
3101
|
+
provider: {
|
|
3102
|
+
type: "string",
|
|
3103
|
+
description: "Provider (vercel, github-actions, aws-ssm)",
|
|
3104
|
+
required: true
|
|
3105
|
+
},
|
|
3106
|
+
name: {
|
|
3107
|
+
type: "string",
|
|
3108
|
+
description: "Target name",
|
|
3109
|
+
required: true
|
|
3110
|
+
},
|
|
3111
|
+
env: {
|
|
3112
|
+
type: "string",
|
|
3113
|
+
description: "Scope: environment"
|
|
3114
|
+
},
|
|
3115
|
+
app: {
|
|
3116
|
+
type: "string",
|
|
3117
|
+
description: "Scope: app name"
|
|
3118
|
+
},
|
|
3119
|
+
project: {
|
|
3120
|
+
type: "string",
|
|
3121
|
+
description: "Scope: project slug"
|
|
3122
|
+
},
|
|
3123
|
+
"auto-sync": {
|
|
3124
|
+
type: "boolean",
|
|
3125
|
+
description: "Enable automatic sync",
|
|
3126
|
+
default: false
|
|
3127
|
+
}
|
|
3128
|
+
},
|
|
3129
|
+
async run({ args }) {
|
|
3130
|
+
const provider = args.provider;
|
|
3131
|
+
const valid = [
|
|
3132
|
+
"vercel",
|
|
3133
|
+
"github-actions",
|
|
3134
|
+
"aws-ssm"
|
|
3135
|
+
];
|
|
3136
|
+
if (!valid.includes(provider)) logError(`Invalid provider: ${provider}. Must be one of: ${valid.join(", ")}`);
|
|
3137
|
+
const credentials = {};
|
|
3138
|
+
if (provider === "vercel" || provider === "github-actions") {
|
|
3139
|
+
const token = await consola.prompt("API token:", { type: "text" });
|
|
3140
|
+
if (!token || typeof token === "symbol") logError("Token is required.");
|
|
3141
|
+
credentials.token = token;
|
|
3142
|
+
} else if (provider === "aws-ssm") {
|
|
3143
|
+
const accessKeyId = await consola.prompt("AWS Access Key ID:", { type: "text" });
|
|
3144
|
+
if (!accessKeyId || typeof accessKeyId === "symbol") logError("Access Key ID is required.");
|
|
3145
|
+
credentials.accessKeyId = accessKeyId;
|
|
3146
|
+
const secretAccessKey = await consola.prompt("AWS Secret Access Key:", { type: "text" });
|
|
3147
|
+
if (!secretAccessKey || typeof secretAccessKey === "symbol") logError("Secret Access Key is required.");
|
|
3148
|
+
credentials.secretAccessKey = secretAccessKey;
|
|
3149
|
+
}
|
|
3150
|
+
const scope = {};
|
|
3151
|
+
if (args.project) scope.project = args.project;
|
|
3152
|
+
if (args.env) scope.environment = args.env;
|
|
3153
|
+
if (args.app) scope.app = args.app;
|
|
3154
|
+
const res = await apiRequest("/v1/env/sync/targets/create", {
|
|
3155
|
+
provider,
|
|
3156
|
+
name: args.name,
|
|
3157
|
+
credentials,
|
|
3158
|
+
scope: Object.keys(scope).length > 0 ? scope : void 0,
|
|
3159
|
+
autoSync: args["auto-sync"]
|
|
3160
|
+
});
|
|
3161
|
+
if (!res.ok) {
|
|
3162
|
+
const text = await res.text().catch(() => "");
|
|
3163
|
+
logError(`Failed to add sync target (${res.status}): ${text}`);
|
|
3164
|
+
}
|
|
3165
|
+
const data = await res.json();
|
|
3166
|
+
console.log(`Added sync target: ${args.name} (${data.id.slice(0, 8)})`);
|
|
3167
|
+
}
|
|
3168
|
+
});
|
|
3169
|
+
|
|
3170
|
+
//#endregion
|
|
3171
|
+
//#region src/commands/sync-list.ts
|
|
3172
|
+
const syncListCommand = defineCommand({
|
|
3173
|
+
meta: {
|
|
3174
|
+
name: "sync list",
|
|
3175
|
+
description: "List env sync targets"
|
|
3176
|
+
},
|
|
3177
|
+
args: {},
|
|
3178
|
+
async run() {
|
|
3179
|
+
const res = await apiRequest("/v1/env/sync/targets/list", {});
|
|
3180
|
+
if (!res.ok) {
|
|
3181
|
+
const text = await res.text().catch(() => "");
|
|
3182
|
+
logError(`Failed to list sync targets (${res.status}): ${text}`);
|
|
3183
|
+
}
|
|
3184
|
+
const data = await res.json();
|
|
3185
|
+
if (data.targets.length === 0) {
|
|
3186
|
+
console.log("No sync targets found.");
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
3190
|
+
const idW = 8;
|
|
3191
|
+
const provW = col(data.targets.map((t) => t.provider), 8);
|
|
3192
|
+
const nameW = col(data.targets.map((t) => t.name), 4);
|
|
3193
|
+
const formatScope = (s) => {
|
|
3194
|
+
if (!s) return "-";
|
|
3195
|
+
const parts = [];
|
|
3196
|
+
if (s.project) parts.push(`prj:${s.project}`);
|
|
3197
|
+
if (s.environment) parts.push(`env:${s.environment}`);
|
|
3198
|
+
if (s.app) parts.push(`app:${s.app}`);
|
|
3199
|
+
return parts.length > 0 ? parts.join(",") : "-";
|
|
3200
|
+
};
|
|
3201
|
+
const scopeW = col(data.targets.map((t) => formatScope(t.scope)), 5);
|
|
3202
|
+
const autoW = 9;
|
|
3203
|
+
const header = [
|
|
3204
|
+
"ID".padEnd(idW),
|
|
3205
|
+
"PROVIDER".padEnd(provW),
|
|
3206
|
+
"NAME".padEnd(nameW),
|
|
3207
|
+
"SCOPE".padEnd(scopeW),
|
|
3208
|
+
"AUTO-SYNC".padEnd(autoW),
|
|
3209
|
+
"LAST SYNCED"
|
|
3210
|
+
].join(" ");
|
|
3211
|
+
console.log(header);
|
|
3212
|
+
console.log("-".repeat(header.length));
|
|
3213
|
+
for (const t of data.targets) {
|
|
3214
|
+
const synced = t.lastSyncedAt ? new Date(t.lastSyncedAt).toLocaleString() : "-";
|
|
3215
|
+
const row = [
|
|
3216
|
+
t.id.slice(0, idW).padEnd(idW),
|
|
3217
|
+
t.provider.padEnd(provW),
|
|
3218
|
+
t.name.padEnd(nameW),
|
|
3219
|
+
formatScope(t.scope).padEnd(scopeW),
|
|
3220
|
+
(t.autoSync ? "yes" : "no").padEnd(autoW),
|
|
3221
|
+
synced
|
|
3222
|
+
].join(" ");
|
|
3223
|
+
console.log(row);
|
|
3224
|
+
}
|
|
3225
|
+
console.log(`\n${data.targets.length} targets`);
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
|
|
3229
|
+
//#endregion
|
|
3230
|
+
//#region src/commands/sync-push.ts
|
|
3231
|
+
const syncPushCommand = defineCommand({
|
|
3232
|
+
meta: {
|
|
3233
|
+
name: "sync push",
|
|
3234
|
+
description: "Push env vars to a sync target"
|
|
3235
|
+
},
|
|
3236
|
+
args: {
|
|
3237
|
+
name: {
|
|
3238
|
+
type: "string",
|
|
3239
|
+
description: "Target name"
|
|
3240
|
+
},
|
|
3241
|
+
id: {
|
|
3242
|
+
type: "string",
|
|
3243
|
+
description: "Target ID"
|
|
3244
|
+
}
|
|
3245
|
+
},
|
|
3246
|
+
async run({ args }) {
|
|
3247
|
+
let targetId = args.id;
|
|
3248
|
+
if (!targetId && !args.name) logError("Provide --name or --id to identify the target.");
|
|
3249
|
+
if (!targetId && args.name) {
|
|
3250
|
+
const listRes = await apiRequest("/v1/env/sync/targets/list", {});
|
|
3251
|
+
if (!listRes.ok) {
|
|
3252
|
+
const text = await listRes.text().catch(() => "");
|
|
3253
|
+
logError(`Failed to list targets (${listRes.status}): ${text}`);
|
|
3254
|
+
}
|
|
3255
|
+
const match = (await listRes.json()).targets.find((t) => t.name === args.name);
|
|
3256
|
+
if (!match) logError(`No sync target found with name: ${args.name}`);
|
|
3257
|
+
targetId = match.id;
|
|
3258
|
+
}
|
|
3259
|
+
console.log("Syncing...");
|
|
3260
|
+
const res = await apiRequest("/v1/env/sync/push", { targetId });
|
|
3261
|
+
if (!res.ok) {
|
|
3262
|
+
const text = await res.text().catch(() => "");
|
|
3263
|
+
logError(`Failed to sync (${res.status}): ${text}`);
|
|
3264
|
+
}
|
|
3265
|
+
const data = await res.json();
|
|
3266
|
+
if (data.status === "success") console.log(`Synced ${data.variableCount} variables.`);
|
|
3267
|
+
else logError(`Sync failed: ${data.error}`);
|
|
3268
|
+
}
|
|
3269
|
+
});
|
|
3270
|
+
|
|
3271
|
+
//#endregion
|
|
3272
|
+
//#region src/commands/sync-remove.ts
|
|
3273
|
+
const syncRemoveCommand = defineCommand({
|
|
3274
|
+
meta: {
|
|
3275
|
+
name: "sync remove",
|
|
3276
|
+
description: "Remove an env sync target"
|
|
3277
|
+
},
|
|
3278
|
+
args: { id: {
|
|
3279
|
+
type: "positional",
|
|
3280
|
+
description: "Target ID (from `auix env sync list`)",
|
|
3281
|
+
required: true
|
|
3282
|
+
} },
|
|
3283
|
+
async run({ args }) {
|
|
3284
|
+
const confirmed = await consola.prompt(`Delete sync target ${args.id}?`, { type: "confirm" });
|
|
3285
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
3286
|
+
console.log("Cancelled.");
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const res = await apiRequest("/v1/env/sync/targets/delete", { id: args.id });
|
|
3290
|
+
if (!res.ok) {
|
|
3291
|
+
const text = await res.text().catch(() => "");
|
|
3292
|
+
logError(`Failed to delete (${res.status}): ${text}`);
|
|
3293
|
+
}
|
|
3294
|
+
console.log("Deleted.");
|
|
3295
|
+
}
|
|
3296
|
+
});
|
|
3297
|
+
|
|
3298
|
+
//#endregion
|
|
3299
|
+
//#region src/commands/token-create.ts
|
|
3300
|
+
const tokenCreateCommand = defineCommand({
|
|
3301
|
+
meta: {
|
|
3302
|
+
name: "token create",
|
|
3303
|
+
description: "Create a service token"
|
|
3304
|
+
},
|
|
3305
|
+
args: {
|
|
3306
|
+
name: {
|
|
3307
|
+
type: "string",
|
|
3308
|
+
description: "Token name",
|
|
3309
|
+
required: true
|
|
3310
|
+
},
|
|
3311
|
+
permissions: {
|
|
3312
|
+
type: "string",
|
|
3313
|
+
description: "Comma-separated permissions (env:read,env:write,env:delete,env:admin)",
|
|
3314
|
+
default: "env:read"
|
|
3315
|
+
},
|
|
3316
|
+
env: {
|
|
3317
|
+
type: "string",
|
|
3318
|
+
description: "Scope: environment"
|
|
3319
|
+
},
|
|
3320
|
+
app: {
|
|
3321
|
+
type: "string",
|
|
3322
|
+
description: "Scope: app name"
|
|
3323
|
+
},
|
|
3324
|
+
project: {
|
|
3325
|
+
type: "string",
|
|
3326
|
+
description: "Scope: project slug"
|
|
3327
|
+
},
|
|
3328
|
+
expires: {
|
|
3329
|
+
type: "string",
|
|
3330
|
+
description: "Expiry in days (e.g. 30)"
|
|
3331
|
+
}
|
|
3332
|
+
},
|
|
3333
|
+
async run({ args }) {
|
|
3334
|
+
const permissions = args.permissions.split(",").map((p) => p.trim());
|
|
3335
|
+
const scope = {};
|
|
3336
|
+
if (args.project) scope.project = args.project;
|
|
3337
|
+
if (args.env) scope.environment = args.env;
|
|
3338
|
+
if (args.app) scope.app = args.app;
|
|
3339
|
+
const body = {
|
|
3340
|
+
name: args.name,
|
|
3341
|
+
permissions
|
|
3342
|
+
};
|
|
3343
|
+
if (Object.keys(scope).length > 0) body.scope = scope;
|
|
3344
|
+
if (args.expires) {
|
|
3345
|
+
const days = Number.parseInt(args.expires, 10);
|
|
3346
|
+
if (Number.isNaN(days) || days <= 0) logError("--expires must be a positive number of days.");
|
|
3347
|
+
body.expiresInDays = days;
|
|
3348
|
+
}
|
|
3349
|
+
const res = await apiRequest("/v1/env/tokens/create", body);
|
|
3350
|
+
if (!res.ok) {
|
|
3351
|
+
const text = await res.text().catch(() => "");
|
|
3352
|
+
logError(`Failed to create token (${res.status}): ${text}`);
|
|
3353
|
+
}
|
|
3354
|
+
const data = await res.json();
|
|
3355
|
+
console.log(`\nToken created: ${data.id}`);
|
|
3356
|
+
console.log(`Key: ${data.key}`);
|
|
3357
|
+
console.log("\nSave this key — it won't be shown again.");
|
|
3358
|
+
}
|
|
3359
|
+
});
|
|
3360
|
+
|
|
3361
|
+
//#endregion
|
|
3362
|
+
//#region src/commands/token-list.ts
|
|
3363
|
+
function formatScope(scope) {
|
|
3364
|
+
if (!scope) return "-";
|
|
3365
|
+
const parts = [];
|
|
3366
|
+
if (scope.project) parts.push(`prj:${scope.project}`);
|
|
3367
|
+
if (scope.environment) parts.push(`env:${scope.environment}`);
|
|
3368
|
+
if (scope.app) parts.push(`app:${scope.app}`);
|
|
3369
|
+
return parts.length > 0 ? parts.join(",") : "-";
|
|
3370
|
+
}
|
|
3371
|
+
function formatDate(date) {
|
|
3372
|
+
if (!date) return "-";
|
|
3373
|
+
return new Date(date).toLocaleDateString();
|
|
3374
|
+
}
|
|
3375
|
+
const tokenListCommand = defineCommand({
|
|
3376
|
+
meta: {
|
|
3377
|
+
name: "token list",
|
|
3378
|
+
description: "List service tokens"
|
|
3379
|
+
},
|
|
3380
|
+
args: {},
|
|
3381
|
+
async run() {
|
|
3382
|
+
const res = await apiRequest("/v1/env/tokens/list", {});
|
|
3383
|
+
if (!res.ok) {
|
|
3384
|
+
const text = await res.text().catch(() => "");
|
|
3385
|
+
logError(`Failed to list tokens (${res.status}): ${text}`);
|
|
3386
|
+
}
|
|
3387
|
+
const data = await res.json();
|
|
3388
|
+
if (data.tokens.length === 0) {
|
|
3389
|
+
console.log("No service tokens found.");
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
3393
|
+
const idW = 8;
|
|
3394
|
+
const nameW = col(data.tokens.map((t) => t.name), 4);
|
|
3395
|
+
const permW = col(data.tokens.map((t) => t.permissions.join(",")), 11);
|
|
3396
|
+
const scopeW = col(data.tokens.map((t) => formatScope(t.scope)), 5);
|
|
3397
|
+
const expW = col(data.tokens.map((t) => formatDate(t.expiresAt)), 7);
|
|
3398
|
+
const usedW = col(data.tokens.map((t) => formatDate(t.lastUsedAt)), 9);
|
|
3399
|
+
const header = [
|
|
3400
|
+
"ID".padEnd(idW),
|
|
3401
|
+
"NAME".padEnd(nameW),
|
|
3402
|
+
"PERMISSIONS".padEnd(permW),
|
|
3403
|
+
"SCOPE".padEnd(scopeW),
|
|
3404
|
+
"EXPIRES".padEnd(expW),
|
|
3405
|
+
"LAST USED".padEnd(usedW)
|
|
3406
|
+
].join(" ");
|
|
3407
|
+
console.log(header);
|
|
3408
|
+
console.log("-".repeat(header.length));
|
|
3409
|
+
for (const t of data.tokens) {
|
|
3410
|
+
const row = [
|
|
3411
|
+
t.id.slice(0, idW).padEnd(idW),
|
|
3412
|
+
t.name.padEnd(nameW),
|
|
3413
|
+
t.permissions.join(",").padEnd(permW),
|
|
3414
|
+
formatScope(t.scope).padEnd(scopeW),
|
|
3415
|
+
formatDate(t.expiresAt).padEnd(expW),
|
|
3416
|
+
formatDate(t.lastUsedAt).padEnd(usedW)
|
|
3417
|
+
].join(" ");
|
|
3418
|
+
console.log(row);
|
|
3419
|
+
}
|
|
3420
|
+
console.log(`\n${data.tokens.length} tokens`);
|
|
3421
|
+
}
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
//#endregion
|
|
3425
|
+
//#region src/commands/token-revoke.ts
|
|
3426
|
+
const tokenRevokeCommand = defineCommand({
|
|
3427
|
+
meta: {
|
|
3428
|
+
name: "token revoke",
|
|
3429
|
+
description: "Revoke a service token"
|
|
3430
|
+
},
|
|
3431
|
+
args: { id: {
|
|
3432
|
+
type: "positional",
|
|
3433
|
+
description: "Token ID (from `auix env token list`)",
|
|
3434
|
+
required: true
|
|
3435
|
+
} },
|
|
3436
|
+
async run({ args }) {
|
|
3437
|
+
const confirmed = await consola.prompt(`Revoke token ${args.id}?`, { type: "confirm" });
|
|
3438
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
3439
|
+
console.log("Cancelled.");
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
const res = await apiRequest("/v1/env/tokens/revoke", { id: args.id });
|
|
3443
|
+
if (!res.ok) {
|
|
3444
|
+
const text = await res.text().catch(() => "");
|
|
3445
|
+
logError(`Failed to revoke token (${res.status}): ${text}`);
|
|
3446
|
+
}
|
|
3447
|
+
console.log("Revoked.");
|
|
3448
|
+
}
|
|
3449
|
+
});
|
|
3450
|
+
|
|
3451
|
+
//#endregion
|
|
3452
|
+
//#region src/commands/unlock.ts
|
|
3453
|
+
const unlockCommand = defineCommand({
|
|
3454
|
+
meta: {
|
|
3455
|
+
name: "unlock",
|
|
3456
|
+
description: "Unlock a config scope"
|
|
3457
|
+
},
|
|
3458
|
+
args: {
|
|
3459
|
+
env: {
|
|
3460
|
+
type: "string",
|
|
3461
|
+
description: "Environment (development, staging, production)"
|
|
3462
|
+
},
|
|
3463
|
+
app: {
|
|
3464
|
+
type: "string",
|
|
3465
|
+
description: "App name"
|
|
3466
|
+
},
|
|
3467
|
+
project: {
|
|
3468
|
+
type: "string",
|
|
3469
|
+
description: "Project slug"
|
|
3470
|
+
}
|
|
3471
|
+
},
|
|
3472
|
+
async run({ args }) {
|
|
3473
|
+
const confirmed = await consola.prompt("Unlock this config scope?", { type: "confirm" });
|
|
3474
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
3475
|
+
console.log("Cancelled.");
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3478
|
+
const projectConfig = loadProjectConfig();
|
|
3479
|
+
const project = args.project ?? projectConfig.project;
|
|
3480
|
+
const app = args.app ?? resolveApp();
|
|
3481
|
+
const res = await apiRequest("/v1/env/unlock", {
|
|
3482
|
+
project,
|
|
3483
|
+
environment: args.env,
|
|
3484
|
+
app
|
|
3485
|
+
});
|
|
3486
|
+
if (!res.ok) {
|
|
3487
|
+
const text = await res.text().catch(() => "");
|
|
3488
|
+
logError(`Failed to unlock (${res.status}): ${text}`);
|
|
3489
|
+
}
|
|
3490
|
+
console.log("Unlocked.");
|
|
3491
|
+
}
|
|
3492
|
+
});
|
|
3493
|
+
|
|
3494
|
+
//#endregion
|
|
3495
|
+
//#region src/commands/webhook-add.ts
|
|
3496
|
+
const VALID_EVENTS = [
|
|
3497
|
+
"env:created",
|
|
3498
|
+
"env:updated",
|
|
3499
|
+
"env:deleted"
|
|
3500
|
+
];
|
|
3501
|
+
const webhookAddCommand = defineCommand({
|
|
3502
|
+
meta: {
|
|
3503
|
+
name: "add",
|
|
3504
|
+
description: "Register a webhook for env var changes"
|
|
3505
|
+
},
|
|
3506
|
+
args: {
|
|
3507
|
+
url: {
|
|
3508
|
+
type: "positional",
|
|
3509
|
+
description: "Webhook URL",
|
|
3510
|
+
required: true
|
|
3511
|
+
},
|
|
3512
|
+
events: {
|
|
3513
|
+
type: "string",
|
|
3514
|
+
description: "Comma-separated events (env:created,env:updated,env:deleted)",
|
|
3515
|
+
default: "env:created,env:updated,env:deleted"
|
|
3516
|
+
},
|
|
3517
|
+
env: {
|
|
3518
|
+
type: "string",
|
|
3519
|
+
description: "Scope: environment filter"
|
|
3520
|
+
},
|
|
3521
|
+
app: {
|
|
3522
|
+
type: "string",
|
|
3523
|
+
description: "Scope: app filter"
|
|
3524
|
+
},
|
|
3525
|
+
project: {
|
|
3526
|
+
type: "string",
|
|
3527
|
+
description: "Scope: project filter"
|
|
3528
|
+
}
|
|
3529
|
+
},
|
|
3530
|
+
async run({ args }) {
|
|
3531
|
+
const events = args.events.split(",").map((e) => e.trim());
|
|
3532
|
+
for (const e of events) if (!VALID_EVENTS.includes(e)) logError(`Invalid event "${e}". Valid: ${VALID_EVENTS.join(", ")}`);
|
|
3533
|
+
const scope = {};
|
|
3534
|
+
if (args.project) scope.project = args.project;
|
|
3535
|
+
if (args.env) scope.environment = args.env;
|
|
3536
|
+
if (args.app) scope.app = args.app;
|
|
3537
|
+
const body = {
|
|
3538
|
+
url: args.url,
|
|
3539
|
+
events
|
|
3540
|
+
};
|
|
3541
|
+
if (Object.keys(scope).length > 0) body.scope = scope;
|
|
3542
|
+
const res = await apiRequest("/v1/env/webhooks/create", body);
|
|
3543
|
+
if (!res.ok) {
|
|
3544
|
+
const text = await res.text().catch(() => "");
|
|
3545
|
+
logError(`Failed to create webhook (${res.status}): ${text}`);
|
|
3546
|
+
}
|
|
3547
|
+
const data = await res.json();
|
|
3548
|
+
console.log(`Created webhook ${data.id}`);
|
|
3549
|
+
}
|
|
3550
|
+
});
|
|
3551
|
+
|
|
3552
|
+
//#endregion
|
|
3553
|
+
//#region src/commands/webhook-list.ts
|
|
3554
|
+
const webhookListCommand = defineCommand({
|
|
3555
|
+
meta: {
|
|
3556
|
+
name: "list",
|
|
3557
|
+
description: "List registered webhooks"
|
|
3558
|
+
},
|
|
3559
|
+
async run() {
|
|
3560
|
+
const res = await apiRequest("/v1/env/webhooks/list", {});
|
|
3561
|
+
if (!res.ok) {
|
|
3562
|
+
const text = await res.text().catch(() => "");
|
|
3563
|
+
logError(`Failed to list webhooks (${res.status}): ${text}`);
|
|
3564
|
+
}
|
|
3565
|
+
const data = await res.json();
|
|
3566
|
+
if (data.webhooks.length === 0) {
|
|
3567
|
+
console.log("No webhooks found.");
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
3571
|
+
const idW = 8;
|
|
3572
|
+
const urlW = col(data.webhooks.map((w) => w.url), 3);
|
|
3573
|
+
const evtW = col(data.webhooks.map((w) => w.events.join(",")), 6);
|
|
3574
|
+
const enW = 7;
|
|
3575
|
+
const header = [
|
|
3576
|
+
"ID".padEnd(idW),
|
|
3577
|
+
"URL".padEnd(urlW),
|
|
3578
|
+
"EVENTS".padEnd(evtW),
|
|
3579
|
+
"ENABLED".padEnd(enW)
|
|
3580
|
+
].join(" ");
|
|
3581
|
+
console.log(header);
|
|
3582
|
+
console.log("-".repeat(header.length));
|
|
3583
|
+
for (const w of data.webhooks) {
|
|
3584
|
+
const row = [
|
|
3585
|
+
w.id.slice(0, idW).padEnd(idW),
|
|
3586
|
+
w.url.padEnd(urlW),
|
|
3587
|
+
w.events.join(",").padEnd(evtW),
|
|
3588
|
+
String(w.enabled).padEnd(enW)
|
|
3589
|
+
].join(" ");
|
|
3590
|
+
console.log(row);
|
|
3591
|
+
}
|
|
3592
|
+
console.log(`\n${data.webhooks.length} webhooks`);
|
|
3593
|
+
}
|
|
3594
|
+
});
|
|
3595
|
+
|
|
3596
|
+
//#endregion
|
|
3597
|
+
//#region src/commands/webhook-remove.ts
|
|
3598
|
+
const webhookRemoveCommand = defineCommand({
|
|
3599
|
+
meta: {
|
|
3600
|
+
name: "remove",
|
|
3601
|
+
description: "Remove a webhook by ID"
|
|
3602
|
+
},
|
|
3603
|
+
args: { id: {
|
|
3604
|
+
type: "positional",
|
|
3605
|
+
description: "Webhook ID (from `auix env webhook list`)",
|
|
3606
|
+
required: true
|
|
3607
|
+
} },
|
|
3608
|
+
async run({ args }) {
|
|
3609
|
+
const confirmed = await consola.prompt(`Remove webhook ${args.id}?`, { type: "confirm" });
|
|
3610
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
3611
|
+
console.log("Cancelled.");
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
const res = await apiRequest("/v1/env/webhooks/delete", { id: args.id });
|
|
3615
|
+
if (!res.ok) {
|
|
3616
|
+
const text = await res.text().catch(() => "");
|
|
3617
|
+
logError(`Failed to remove webhook (${res.status}): ${text}`);
|
|
3618
|
+
}
|
|
3619
|
+
console.log("Removed.");
|
|
3620
|
+
}
|
|
3621
|
+
});
|
|
3622
|
+
|
|
3623
|
+
//#endregion
|
|
3624
|
+
//#region src/commands/whoami.ts
|
|
3625
|
+
const whoamiCommand = defineCommand({
|
|
3626
|
+
meta: {
|
|
3627
|
+
name: "whoami",
|
|
3628
|
+
description: "Show current authentication"
|
|
3629
|
+
},
|
|
3630
|
+
async run() {
|
|
3631
|
+
const config = loadGlobalConfig();
|
|
3632
|
+
if (!config.apiKey) logError("Not logged in. Run `auix login`.");
|
|
3633
|
+
if (config.user) console.log(`User: ${config.user.name} (${config.user.email})`);
|
|
3634
|
+
if (config.organization) console.log(`Org: ${config.organization.name} (${config.organization.slug})`);
|
|
3635
|
+
console.log(`Key: ${config.apiKey.slice(0, 12)}...`);
|
|
3636
|
+
console.log(`API: ${config.baseUrl ?? "https://api.auix.dev"}`);
|
|
3637
|
+
console.log(`App: ${config.appUrl ?? "https://auix.dev"}`);
|
|
1821
3638
|
}
|
|
1822
3639
|
});
|
|
1823
3640
|
|
|
@@ -1826,22 +3643,110 @@ const setCommand = defineCommand({
|
|
|
1826
3643
|
runMain(defineCommand({
|
|
1827
3644
|
meta: {
|
|
1828
3645
|
name: "auix",
|
|
1829
|
-
version: "0.0.
|
|
3646
|
+
version: "0.0.3",
|
|
1830
3647
|
description: "AUIX CLI"
|
|
1831
3648
|
},
|
|
1832
3649
|
subCommands: {
|
|
1833
3650
|
login: loginCommand,
|
|
3651
|
+
logout: logoutCommand,
|
|
3652
|
+
whoami: whoamiCommand,
|
|
3653
|
+
switch: switchCommand,
|
|
1834
3654
|
env: defineCommand({
|
|
1835
3655
|
meta: {
|
|
1836
3656
|
name: "env",
|
|
1837
3657
|
description: "Manage environment variables"
|
|
1838
3658
|
},
|
|
1839
3659
|
subCommands: {
|
|
3660
|
+
init: initCommand,
|
|
3661
|
+
run: runCommand,
|
|
1840
3662
|
pull: pullCommand,
|
|
1841
3663
|
push: pushCommand,
|
|
1842
3664
|
list: listCommand,
|
|
1843
3665
|
set: setCommand,
|
|
1844
|
-
|
|
3666
|
+
delete: deleteCommand,
|
|
3667
|
+
diff: diffCommand,
|
|
3668
|
+
history: historyCommand,
|
|
3669
|
+
rollback: rollbackCommand,
|
|
3670
|
+
lock: lockCommand,
|
|
3671
|
+
unlock: unlockCommand,
|
|
3672
|
+
"lock-status": lockStatusCommand,
|
|
3673
|
+
branch: defineCommand({
|
|
3674
|
+
meta: {
|
|
3675
|
+
name: "branch",
|
|
3676
|
+
description: "Manage env branches"
|
|
3677
|
+
},
|
|
3678
|
+
subCommands: {
|
|
3679
|
+
create: branchCreateCommand,
|
|
3680
|
+
list: branchListCommand,
|
|
3681
|
+
delete: branchDeleteCommand
|
|
3682
|
+
}
|
|
3683
|
+
}),
|
|
3684
|
+
token: defineCommand({
|
|
3685
|
+
meta: {
|
|
3686
|
+
name: "token",
|
|
3687
|
+
description: "Manage service tokens"
|
|
3688
|
+
},
|
|
3689
|
+
subCommands: {
|
|
3690
|
+
create: tokenCreateCommand,
|
|
3691
|
+
list: tokenListCommand,
|
|
3692
|
+
revoke: tokenRevokeCommand
|
|
3693
|
+
}
|
|
3694
|
+
}),
|
|
3695
|
+
webhook: defineCommand({
|
|
3696
|
+
meta: {
|
|
3697
|
+
name: "webhook",
|
|
3698
|
+
description: "Manage env change webhooks"
|
|
3699
|
+
},
|
|
3700
|
+
subCommands: {
|
|
3701
|
+
add: webhookAddCommand,
|
|
3702
|
+
list: webhookListCommand,
|
|
3703
|
+
remove: webhookRemoveCommand
|
|
3704
|
+
}
|
|
3705
|
+
}),
|
|
3706
|
+
share: defineCommand({
|
|
3707
|
+
meta: {
|
|
3708
|
+
name: "share",
|
|
3709
|
+
description: "One-time encrypted share links"
|
|
3710
|
+
},
|
|
3711
|
+
subCommands: {
|
|
3712
|
+
create: shareCreateCommand,
|
|
3713
|
+
access: shareAccessCommand
|
|
3714
|
+
}
|
|
3715
|
+
}),
|
|
3716
|
+
sync: defineCommand({
|
|
3717
|
+
meta: {
|
|
3718
|
+
name: "sync",
|
|
3719
|
+
description: "Sync env vars to external providers"
|
|
3720
|
+
},
|
|
3721
|
+
subCommands: {
|
|
3722
|
+
add: syncAddCommand,
|
|
3723
|
+
list: syncListCommand,
|
|
3724
|
+
push: syncPushCommand,
|
|
3725
|
+
remove: syncRemoveCommand
|
|
3726
|
+
}
|
|
3727
|
+
}),
|
|
3728
|
+
rotation: defineCommand({
|
|
3729
|
+
meta: {
|
|
3730
|
+
name: "rotation",
|
|
3731
|
+
description: "Manage credential rotation policies"
|
|
3732
|
+
},
|
|
3733
|
+
subCommands: {
|
|
3734
|
+
create: rotationCreateCommand,
|
|
3735
|
+
list: rotationListCommand,
|
|
3736
|
+
rotate: rotationRotateCommand
|
|
3737
|
+
}
|
|
3738
|
+
}),
|
|
3739
|
+
dynamic: defineCommand({
|
|
3740
|
+
meta: {
|
|
3741
|
+
name: "dynamic",
|
|
3742
|
+
description: "Manage dynamic (ephemeral) secrets"
|
|
3743
|
+
},
|
|
3744
|
+
subCommands: {
|
|
3745
|
+
create: dynamicCreateCommand,
|
|
3746
|
+
list: dynamicListCommand,
|
|
3747
|
+
lease: dynamicLeaseCommand
|
|
3748
|
+
}
|
|
3749
|
+
})
|
|
1845
3750
|
}
|
|
1846
3751
|
})
|
|
1847
3752
|
}
|