auix 0.0.1 → 0.0.4
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 +2111 -142
- 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,111 +1846,480 @@ 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
|
-
|
|
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);
|
|
1900
|
+
}
|
|
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);
|
|
1523
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);
|
|
1923
|
+
}
|
|
1924
|
+
return dirs.length > 0 ? dirs : null;
|
|
1524
1925
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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
|
+
const apps = {};
|
|
1950
|
+
for (const dir of workspaceDirs) apps[dir] = basename(dir);
|
|
1951
|
+
console.log(`\nDetected monorepo with ${workspaceDirs.length} packages:\n`);
|
|
1952
|
+
for (const [dir, name] of Object.entries(apps)) console.log(` ${dir} → ${name}`);
|
|
1953
|
+
const action = await consola.prompt("\nUse these app mappings?", {
|
|
1954
|
+
type: "select",
|
|
1955
|
+
options: [
|
|
1956
|
+
{
|
|
1957
|
+
label: "Yes, use as-is",
|
|
1958
|
+
value: "accept"
|
|
1959
|
+
},
|
|
1960
|
+
{
|
|
1961
|
+
label: "Edit some mappings",
|
|
1962
|
+
value: "edit"
|
|
1963
|
+
},
|
|
1964
|
+
{
|
|
1965
|
+
label: "Skip app mapping",
|
|
1966
|
+
value: "skip"
|
|
1967
|
+
}
|
|
1968
|
+
]
|
|
1969
|
+
});
|
|
1970
|
+
if (typeof action === "symbol") {
|
|
1971
|
+
console.log("Cancelled.");
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
if (action === "accept") config.apps = apps;
|
|
1975
|
+
else if (action === "edit") {
|
|
1976
|
+
if (await editAppMappings(apps, workspaceDirs)) return;
|
|
1977
|
+
config.apps = apps;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
writeFileSync(rcPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
1981
|
+
console.log(`\nCreated .auixrc`);
|
|
1982
|
+
console.log(` project: ${config.project}`);
|
|
1983
|
+
if (config.apps) console.log(` apps: ${Object.entries(config.apps).map(([d, a]) => `${d} → ${a}`).join(", ")}`);
|
|
1535
1984
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
1545
|
-
const tokenRes = await fetch(`${base}/api/auth/device/token`, {
|
|
1546
|
-
method: "POST",
|
|
1547
|
-
headers: { "Content-Type": "application/json" },
|
|
1548
|
-
body: JSON.stringify({
|
|
1549
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
1550
|
-
device_code: codeData.device_code,
|
|
1551
|
-
client_id: CLIENT_ID
|
|
1552
|
-
})
|
|
1985
|
+
});
|
|
1986
|
+
const DONE_VALUE = "__done__";
|
|
1987
|
+
async function editAppMappings(apps, dirs) {
|
|
1988
|
+
while (true) {
|
|
1989
|
+
const search = await consola.prompt("Search apps (empty to show all)", {
|
|
1990
|
+
type: "text",
|
|
1991
|
+
default: "",
|
|
1992
|
+
placeholder: "e.g. web, api, database..."
|
|
1553
1993
|
});
|
|
1554
|
-
if (
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1994
|
+
if (typeof search === "symbol") {
|
|
1995
|
+
console.log("Cancelled.");
|
|
1996
|
+
return true;
|
|
1997
|
+
}
|
|
1998
|
+
const query = search.trim().toLowerCase();
|
|
1999
|
+
const matched = dirs.filter((dir) => {
|
|
2000
|
+
if (!query) return true;
|
|
2001
|
+
return dir.toLowerCase().includes(query) || apps[dir].toLowerCase().includes(query);
|
|
2002
|
+
});
|
|
2003
|
+
if (matched.length === 0) {
|
|
2004
|
+
console.log(" No matches found.\n");
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
const options = [...matched.map((dir) => ({
|
|
2008
|
+
label: `${dir} → ${apps[dir]}`,
|
|
2009
|
+
value: dir
|
|
2010
|
+
})), {
|
|
2011
|
+
label: "Done editing",
|
|
2012
|
+
value: DONE_VALUE
|
|
2013
|
+
}];
|
|
2014
|
+
const selected = await consola.prompt("Select app to rename", {
|
|
2015
|
+
type: "select",
|
|
2016
|
+
options
|
|
2017
|
+
});
|
|
2018
|
+
if (typeof selected === "symbol") {
|
|
2019
|
+
console.log("Cancelled.");
|
|
2020
|
+
return true;
|
|
2021
|
+
}
|
|
2022
|
+
if (selected === DONE_VALUE) break;
|
|
2023
|
+
const dir = selected;
|
|
2024
|
+
const name = await consola.prompt(`App name for ${dir}`, {
|
|
2025
|
+
type: "text",
|
|
2026
|
+
default: apps[dir],
|
|
2027
|
+
placeholder: apps[dir]
|
|
2028
|
+
});
|
|
2029
|
+
if (typeof name === "symbol") {
|
|
2030
|
+
console.log("Cancelled.");
|
|
2031
|
+
return true;
|
|
2032
|
+
}
|
|
2033
|
+
const trimmed = name.trim();
|
|
2034
|
+
if (trimmed) {
|
|
2035
|
+
apps[dir] = trimmed;
|
|
2036
|
+
console.log(` ${dir} → ${trimmed}\n`);
|
|
2037
|
+
}
|
|
1561
2038
|
}
|
|
1562
|
-
|
|
1563
|
-
return "";
|
|
2039
|
+
return false;
|
|
1564
2040
|
}
|
|
1565
|
-
async function
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
2041
|
+
async function selectProject() {
|
|
2042
|
+
try {
|
|
2043
|
+
const res = await apiRequest("/v1/env/projects/list", {});
|
|
2044
|
+
if (res.ok) {
|
|
2045
|
+
const data = await res.json();
|
|
2046
|
+
if (data.projects && data.projects.length > 0) {
|
|
2047
|
+
const selected = await consola.prompt("Select project", {
|
|
2048
|
+
type: "select",
|
|
2049
|
+
options: data.projects.map((p) => ({
|
|
2050
|
+
label: `${p.name} (${p.slug})`,
|
|
2051
|
+
value: p.slug
|
|
2052
|
+
}))
|
|
2053
|
+
});
|
|
2054
|
+
if (typeof selected === "symbol") logError("Cancelled.");
|
|
2055
|
+
return selected;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
} catch {}
|
|
2059
|
+
const slug = await consola.prompt("Project slug", {
|
|
2060
|
+
type: "text",
|
|
2061
|
+
placeholder: basename(process.cwd()),
|
|
2062
|
+
default: basename(process.cwd())
|
|
1580
2063
|
});
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
return orgs[idx];
|
|
2064
|
+
if (typeof slug === "symbol") logError("Cancelled.");
|
|
2065
|
+
const trimmed = slug.trim();
|
|
2066
|
+
if (!trimmed) logError("Project slug cannot be empty.");
|
|
2067
|
+
return trimmed;
|
|
1586
2068
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
2069
|
+
|
|
2070
|
+
//#endregion
|
|
2071
|
+
//#region src/commands/list.ts
|
|
2072
|
+
const listCommand = defineCommand({
|
|
2073
|
+
meta: {
|
|
2074
|
+
name: "list",
|
|
2075
|
+
description: "List environment variables"
|
|
2076
|
+
},
|
|
2077
|
+
args: {
|
|
2078
|
+
env: {
|
|
2079
|
+
type: "string",
|
|
2080
|
+
description: "Filter by environment (development, staging, production)"
|
|
2081
|
+
},
|
|
2082
|
+
app: {
|
|
2083
|
+
type: "string",
|
|
2084
|
+
description: "Filter by app name"
|
|
2085
|
+
},
|
|
2086
|
+
project: {
|
|
2087
|
+
type: "string",
|
|
2088
|
+
description: "Filter by project slug"
|
|
2089
|
+
},
|
|
2090
|
+
branch: {
|
|
2091
|
+
type: "string",
|
|
2092
|
+
description: "Filter by env branch name"
|
|
2093
|
+
}
|
|
2094
|
+
},
|
|
2095
|
+
async run({ args }) {
|
|
2096
|
+
const projectConfig = loadProjectConfig();
|
|
2097
|
+
const project = args.project ?? projectConfig.project;
|
|
2098
|
+
const app = args.app ?? resolveApp();
|
|
2099
|
+
const res = await apiRequest("/v1/env/list", {
|
|
2100
|
+
project,
|
|
2101
|
+
environment: args.env,
|
|
2102
|
+
app,
|
|
2103
|
+
branch: args.branch
|
|
2104
|
+
});
|
|
2105
|
+
if (!res.ok) {
|
|
2106
|
+
const text = await res.text().catch(() => "");
|
|
2107
|
+
logError(`Failed to list variables (${res.status}): ${text}`);
|
|
2108
|
+
}
|
|
2109
|
+
const data = await res.json();
|
|
2110
|
+
if (data.variables.length === 0) {
|
|
2111
|
+
console.log("No variables found.");
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
2115
|
+
const idW = 8;
|
|
2116
|
+
const keyW = col(data.variables.map((v) => v.key), 3);
|
|
2117
|
+
const prjW = col(data.variables.map((v) => v.project ?? "-"), 7);
|
|
2118
|
+
const envW = col(data.variables.map((v) => v.environment ?? "-"), 3);
|
|
2119
|
+
const appW = col(data.variables.map((v) => v.app ?? "-"), 3);
|
|
2120
|
+
const header = [
|
|
2121
|
+
"ID".padEnd(idW),
|
|
2122
|
+
"KEY".padEnd(keyW),
|
|
2123
|
+
"PROJECT".padEnd(prjW),
|
|
2124
|
+
"ENV".padEnd(envW),
|
|
2125
|
+
"APP".padEnd(appW)
|
|
2126
|
+
].join(" ");
|
|
2127
|
+
console.log(header);
|
|
2128
|
+
console.log("-".repeat(header.length));
|
|
2129
|
+
for (const v of data.variables) {
|
|
2130
|
+
const row = [
|
|
2131
|
+
v.id.slice(0, idW).padEnd(idW),
|
|
2132
|
+
v.key.padEnd(keyW),
|
|
2133
|
+
(v.project ?? "-").padEnd(prjW),
|
|
2134
|
+
(v.environment ?? "-").padEnd(envW),
|
|
2135
|
+
(v.app ?? "-").padEnd(appW)
|
|
2136
|
+
].join(" ");
|
|
2137
|
+
console.log(row);
|
|
2138
|
+
}
|
|
2139
|
+
console.log(`\n${data.variables.length} variables`);
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
//#endregion
|
|
2144
|
+
//#region src/commands/lock.ts
|
|
2145
|
+
const lockCommand = defineCommand({
|
|
2146
|
+
meta: {
|
|
2147
|
+
name: "lock",
|
|
2148
|
+
description: "Lock a config scope"
|
|
2149
|
+
},
|
|
2150
|
+
args: {
|
|
2151
|
+
env: {
|
|
2152
|
+
type: "string",
|
|
2153
|
+
description: "Environment (development, staging, production)"
|
|
2154
|
+
},
|
|
2155
|
+
app: {
|
|
2156
|
+
type: "string",
|
|
2157
|
+
description: "App name"
|
|
2158
|
+
},
|
|
2159
|
+
project: {
|
|
2160
|
+
type: "string",
|
|
2161
|
+
description: "Project slug"
|
|
2162
|
+
},
|
|
2163
|
+
reason: {
|
|
2164
|
+
type: "string",
|
|
2165
|
+
description: "Reason for locking"
|
|
2166
|
+
}
|
|
2167
|
+
},
|
|
2168
|
+
async run({ args }) {
|
|
2169
|
+
const projectConfig = loadProjectConfig();
|
|
2170
|
+
const project = args.project ?? projectConfig.project;
|
|
2171
|
+
const app = args.app ?? resolveApp();
|
|
2172
|
+
const res = await apiRequest("/v1/env/lock", {
|
|
2173
|
+
project,
|
|
2174
|
+
environment: args.env,
|
|
2175
|
+
app,
|
|
2176
|
+
reason: args.reason
|
|
2177
|
+
});
|
|
2178
|
+
if (!res.ok) {
|
|
2179
|
+
const text = await res.text().catch(() => "");
|
|
2180
|
+
logError(`Failed to lock (${res.status}): ${text}`);
|
|
2181
|
+
}
|
|
2182
|
+
const data = await res.json();
|
|
2183
|
+
console.log(`Locked (${data.id}).`);
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
//#endregion
|
|
2188
|
+
//#region src/commands/lock-status.ts
|
|
2189
|
+
const lockStatusCommand = defineCommand({
|
|
2190
|
+
meta: {
|
|
2191
|
+
name: "lock-status",
|
|
2192
|
+
description: "Check if config is locked"
|
|
2193
|
+
},
|
|
2194
|
+
args: {
|
|
2195
|
+
env: {
|
|
2196
|
+
type: "string",
|
|
2197
|
+
description: "Environment (development, staging, production)"
|
|
2198
|
+
},
|
|
2199
|
+
app: {
|
|
2200
|
+
type: "string",
|
|
2201
|
+
description: "App name"
|
|
2202
|
+
},
|
|
2203
|
+
project: {
|
|
2204
|
+
type: "string",
|
|
2205
|
+
description: "Project slug"
|
|
2206
|
+
}
|
|
2207
|
+
},
|
|
2208
|
+
async run({ args }) {
|
|
2209
|
+
const projectConfig = loadProjectConfig();
|
|
2210
|
+
const project = args.project ?? projectConfig.project;
|
|
2211
|
+
const app = args.app ?? resolveApp();
|
|
2212
|
+
const res = await apiRequest("/v1/env/lock/status", {
|
|
2213
|
+
project,
|
|
2214
|
+
environment: args.env,
|
|
2215
|
+
app
|
|
2216
|
+
});
|
|
2217
|
+
if (!res.ok) {
|
|
2218
|
+
const text = await res.text().catch(() => "");
|
|
2219
|
+
logError(`Failed to check lock status (${res.status}): ${text}`);
|
|
2220
|
+
}
|
|
2221
|
+
const data = await res.json();
|
|
2222
|
+
if (data.locked) {
|
|
2223
|
+
const parts = [`Locked: ${data.lock.reason ?? "(no reason)"}`];
|
|
2224
|
+
if (data.lock.lockedBy) parts.push(`by ${data.lock.lockedBy}`);
|
|
2225
|
+
if (data.lock.lockedAt) parts.push(`at ${data.lock.lockedAt}`);
|
|
2226
|
+
console.log(parts.join(" "));
|
|
2227
|
+
} else console.log("Unlocked");
|
|
2228
|
+
}
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
//#endregion
|
|
2232
|
+
//#region src/commands/login.ts
|
|
2233
|
+
const CLIENT_ID = "auix-cli";
|
|
2234
|
+
const POLL_INTERVAL_MS = 5e3;
|
|
2235
|
+
function openBrowser(url) {
|
|
2236
|
+
try {
|
|
2237
|
+
execSync(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
2238
|
+
} catch {}
|
|
2239
|
+
}
|
|
2240
|
+
async function deviceFlow(appUrl) {
|
|
2241
|
+
const base = appUrl.replace(/\/$/, "");
|
|
2242
|
+
const codeRes = await fetch(`${base}/api/auth/device/code`, {
|
|
2243
|
+
method: "POST",
|
|
2244
|
+
headers: { "Content-Type": "application/json" },
|
|
2245
|
+
body: JSON.stringify({ client_id: CLIENT_ID })
|
|
2246
|
+
});
|
|
2247
|
+
if (!codeRes.ok) {
|
|
2248
|
+
const text = await codeRes.text().catch(() => "");
|
|
2249
|
+
logError(`Failed to start device flow (${codeRes.status}): ${text}`);
|
|
2250
|
+
}
|
|
2251
|
+
const codeData = await codeRes.json();
|
|
2252
|
+
console.log("Opening browser for authentication...");
|
|
2253
|
+
console.log(`If the browser doesn't open, visit: ${codeData.verification_uri_complete}`);
|
|
2254
|
+
console.log(`Enter code: ${codeData.user_code}\n`);
|
|
2255
|
+
openBrowser(codeData.verification_uri_complete);
|
|
2256
|
+
const deadline = Date.now() + codeData.expires_in * 1e3;
|
|
2257
|
+
const interval = Math.max((codeData.interval || 5) * 1e3, POLL_INTERVAL_MS);
|
|
2258
|
+
while (Date.now() < deadline) {
|
|
2259
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
2260
|
+
const tokenRes = await fetch(`${base}/api/auth/device/token`, {
|
|
2261
|
+
method: "POST",
|
|
2262
|
+
headers: { "Content-Type": "application/json" },
|
|
2263
|
+
body: JSON.stringify({
|
|
2264
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
2265
|
+
device_code: codeData.device_code,
|
|
2266
|
+
client_id: CLIENT_ID
|
|
2267
|
+
})
|
|
2268
|
+
});
|
|
2269
|
+
if (tokenRes.ok) return (await tokenRes.json()).access_token;
|
|
2270
|
+
const err = await tokenRes.json().catch(() => ({}));
|
|
2271
|
+
if (err.error === "authorization_pending") continue;
|
|
2272
|
+
if (err.error === "slow_down") continue;
|
|
2273
|
+
if (err.error === "expired_token") logError("Device code expired.");
|
|
2274
|
+
if (err.error === "access_denied") logError("Access denied.");
|
|
2275
|
+
logError(`Unexpected error: ${err.error ?? tokenRes.status}`);
|
|
2276
|
+
}
|
|
2277
|
+
logError("Device code expired. Please try again.");
|
|
2278
|
+
return "";
|
|
2279
|
+
}
|
|
2280
|
+
async function selectOrganization(appUrl, token) {
|
|
2281
|
+
const base = appUrl.replace(/\/$/, "");
|
|
2282
|
+
const res = await fetch(`${base}/api/auth/organization/list`, { headers: { Authorization: `Bearer ${token}` } });
|
|
2283
|
+
if (!res.ok) logError(`Failed to list organizations (${res.status}).`);
|
|
2284
|
+
const orgs = await res.json();
|
|
2285
|
+
if (orgs.length === 0) logError("No organizations found. Create one at the web dashboard.");
|
|
2286
|
+
if (orgs.length === 1) {
|
|
2287
|
+
console.log(`Organization: ${orgs[0].name} (${orgs[0].slug})`);
|
|
2288
|
+
return orgs[0];
|
|
2289
|
+
}
|
|
2290
|
+
const selected = await consola.prompt("Select organization", {
|
|
2291
|
+
type: "select",
|
|
2292
|
+
options: orgs.map((o) => ({
|
|
2293
|
+
label: `${o.name} (${o.slug})`,
|
|
2294
|
+
value: o.id
|
|
2295
|
+
}))
|
|
2296
|
+
});
|
|
2297
|
+
if (typeof selected === "symbol") logError("Cancelled.");
|
|
2298
|
+
return orgs.find((o) => o.id === selected);
|
|
2299
|
+
}
|
|
2300
|
+
async function fetchUser(appUrl, token) {
|
|
2301
|
+
const base = appUrl.replace(/\/$/, "");
|
|
2302
|
+
const res = await fetch(`${base}/api/auth/get-session`, { headers: { Authorization: `Bearer ${token}` } });
|
|
2303
|
+
if (!res.ok) return {
|
|
2304
|
+
name: "unknown",
|
|
2305
|
+
email: "unknown"
|
|
2306
|
+
};
|
|
2307
|
+
const data = await res.json();
|
|
2308
|
+
return {
|
|
2309
|
+
name: data.user?.name ?? "unknown",
|
|
2310
|
+
email: data.user?.email ?? "unknown"
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
async function createApiKey$1(appUrl, token, organizationId) {
|
|
2314
|
+
const base = appUrl.replace(/\/$/, "");
|
|
2315
|
+
const res = await fetch(`${base}/api/cli/create-key`, {
|
|
2316
|
+
method: "POST",
|
|
2317
|
+
headers: {
|
|
2318
|
+
"Content-Type": "application/json",
|
|
2319
|
+
Authorization: `Bearer ${token}`
|
|
2320
|
+
},
|
|
2321
|
+
body: JSON.stringify({ organizationId })
|
|
2322
|
+
});
|
|
1597
2323
|
if (!res.ok) {
|
|
1598
2324
|
const text = await res.text().catch(() => "");
|
|
1599
2325
|
logError(`Failed to create API key (${res.status}): ${text}`);
|
|
@@ -1642,19 +2368,48 @@ const loginCommand = defineCommand({
|
|
|
1642
2368
|
return;
|
|
1643
2369
|
}
|
|
1644
2370
|
const token = await deviceFlow(appUrl);
|
|
1645
|
-
|
|
2371
|
+
const user = await fetchUser(appUrl, token);
|
|
2372
|
+
console.log(`Authenticated as ${user.email}\n`);
|
|
1646
2373
|
const org = await selectOrganization(appUrl, token);
|
|
1647
2374
|
saveGlobalConfig({
|
|
1648
|
-
apiKey: await createApiKey(appUrl, token, org.id),
|
|
2375
|
+
apiKey: await createApiKey$1(appUrl, token, org.id),
|
|
1649
2376
|
baseUrl,
|
|
1650
|
-
appUrl
|
|
2377
|
+
appUrl,
|
|
2378
|
+
user,
|
|
2379
|
+
organization: {
|
|
2380
|
+
name: org.name,
|
|
2381
|
+
slug: org.slug
|
|
2382
|
+
}
|
|
1651
2383
|
});
|
|
1652
2384
|
console.log(`\nLogged in to ${org.name}.`);
|
|
1653
2385
|
}
|
|
1654
2386
|
});
|
|
1655
2387
|
|
|
2388
|
+
//#endregion
|
|
2389
|
+
//#region src/commands/logout.ts
|
|
2390
|
+
const CONFIG_FILE = join(homedir(), ".auix", "config.json");
|
|
2391
|
+
const logoutCommand = defineCommand({
|
|
2392
|
+
meta: {
|
|
2393
|
+
name: "logout",
|
|
2394
|
+
description: "Log out and remove credentials"
|
|
2395
|
+
},
|
|
2396
|
+
async run() {
|
|
2397
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
2398
|
+
console.log("Not logged in.");
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
rmSync(CONFIG_FILE);
|
|
2402
|
+
console.log("Logged out.");
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2405
|
+
|
|
1656
2406
|
//#endregion
|
|
1657
2407
|
//#region src/commands/pull.ts
|
|
2408
|
+
const formatters = {
|
|
2409
|
+
env: formatEnvString,
|
|
2410
|
+
json: formatJsonString,
|
|
2411
|
+
yaml: formatYamlString
|
|
2412
|
+
};
|
|
1658
2413
|
const pullCommand = defineCommand({
|
|
1659
2414
|
meta: {
|
|
1660
2415
|
name: "pull",
|
|
@@ -1663,8 +2418,7 @@ const pullCommand = defineCommand({
|
|
|
1663
2418
|
args: {
|
|
1664
2419
|
env: {
|
|
1665
2420
|
type: "string",
|
|
1666
|
-
description: "Environment (development, staging, production)"
|
|
1667
|
-
default: "development"
|
|
2421
|
+
description: "Environment (development, staging, production)"
|
|
1668
2422
|
},
|
|
1669
2423
|
app: {
|
|
1670
2424
|
type: "string",
|
|
@@ -1674,32 +2428,50 @@ const pullCommand = defineCommand({
|
|
|
1674
2428
|
type: "string",
|
|
1675
2429
|
description: "Project slug"
|
|
1676
2430
|
},
|
|
2431
|
+
branch: {
|
|
2432
|
+
type: "string",
|
|
2433
|
+
description: "Env branch name"
|
|
2434
|
+
},
|
|
1677
2435
|
out: {
|
|
1678
2436
|
type: "string",
|
|
1679
2437
|
description: "Output file path",
|
|
1680
2438
|
default: ".env.local"
|
|
2439
|
+
},
|
|
2440
|
+
format: {
|
|
2441
|
+
type: "string",
|
|
2442
|
+
description: "Output format (env, json, yaml)",
|
|
2443
|
+
default: "env"
|
|
2444
|
+
},
|
|
2445
|
+
interpolate: {
|
|
2446
|
+
type: "boolean",
|
|
2447
|
+
description: "Interpolate variable references",
|
|
2448
|
+
default: false
|
|
1681
2449
|
}
|
|
1682
2450
|
},
|
|
1683
2451
|
async run({ args }) {
|
|
1684
2452
|
const projectConfig = loadProjectConfig();
|
|
1685
2453
|
const project = args.project ?? projectConfig.project;
|
|
1686
2454
|
const app = args.app ?? resolveApp();
|
|
2455
|
+
const formatter = formatters[args.format];
|
|
2456
|
+
if (!formatter) logError(`Unknown format: ${args.format}. Use env, json, or yaml.`);
|
|
1687
2457
|
const res = await apiRequest("/v1/env/resolve", {
|
|
1688
2458
|
project,
|
|
1689
2459
|
environment: args.env,
|
|
1690
|
-
app
|
|
2460
|
+
app,
|
|
2461
|
+
branch: args.branch
|
|
1691
2462
|
});
|
|
1692
2463
|
if (!res.ok) {
|
|
1693
2464
|
const text = await res.text().catch(() => "");
|
|
1694
2465
|
logError(`Failed to resolve env (${res.status}): ${text}`);
|
|
1695
2466
|
}
|
|
1696
|
-
|
|
1697
|
-
const count = Object.keys(
|
|
2467
|
+
let variables = (await res.json()).variables;
|
|
2468
|
+
const count = Object.keys(variables).length;
|
|
1698
2469
|
if (count === 0) {
|
|
1699
2470
|
console.log("No variables found.");
|
|
1700
2471
|
return;
|
|
1701
2472
|
}
|
|
1702
|
-
|
|
2473
|
+
if (args.interpolate) variables = interpolateEnv(variables);
|
|
2474
|
+
const content = formatter(variables);
|
|
1703
2475
|
writeFileSync(args.out, `${content}\n`);
|
|
1704
2476
|
console.log(`Pulled ${count} variables → ${args.out}`);
|
|
1705
2477
|
}
|
|
@@ -1720,8 +2492,7 @@ const pushCommand = defineCommand({
|
|
|
1720
2492
|
},
|
|
1721
2493
|
env: {
|
|
1722
2494
|
type: "string",
|
|
1723
|
-
description: "Environment (development, staging, production)"
|
|
1724
|
-
default: "development"
|
|
2495
|
+
description: "Environment (development, staging, production)"
|
|
1725
2496
|
},
|
|
1726
2497
|
app: {
|
|
1727
2498
|
type: "string",
|
|
@@ -1731,6 +2502,10 @@ const pushCommand = defineCommand({
|
|
|
1731
2502
|
type: "string",
|
|
1732
2503
|
description: "Project slug"
|
|
1733
2504
|
},
|
|
2505
|
+
branch: {
|
|
2506
|
+
type: "string",
|
|
2507
|
+
description: "Env branch name"
|
|
2508
|
+
},
|
|
1734
2509
|
overwrite: {
|
|
1735
2510
|
type: "boolean",
|
|
1736
2511
|
description: "Overwrite existing variables",
|
|
@@ -1748,11 +2523,21 @@ const pushCommand = defineCommand({
|
|
|
1748
2523
|
logError(`Cannot read file: ${args.file}`);
|
|
1749
2524
|
return;
|
|
1750
2525
|
}
|
|
2526
|
+
const parsed = parseEnvString(content);
|
|
2527
|
+
const variables = Object.entries(parsed).map(([key, value]) => ({
|
|
2528
|
+
key,
|
|
2529
|
+
value
|
|
2530
|
+
}));
|
|
2531
|
+
if (variables.length === 0) {
|
|
2532
|
+
console.log("No variables found in file.");
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
1751
2535
|
const res = await apiRequest("/v1/env/import", {
|
|
1752
|
-
|
|
2536
|
+
variables,
|
|
1753
2537
|
project,
|
|
1754
2538
|
environment: args.env,
|
|
1755
2539
|
app,
|
|
2540
|
+
branch: args.branch,
|
|
1756
2541
|
overwrite: args.overwrite
|
|
1757
2542
|
});
|
|
1758
2543
|
if (!res.ok) {
|
|
@@ -1765,22 +2550,26 @@ const pushCommand = defineCommand({
|
|
|
1765
2550
|
});
|
|
1766
2551
|
|
|
1767
2552
|
//#endregion
|
|
1768
|
-
//#region src/commands/
|
|
1769
|
-
const
|
|
2553
|
+
//#region src/commands/rollback.ts
|
|
2554
|
+
const rollbackCommand = defineCommand({
|
|
1770
2555
|
meta: {
|
|
1771
|
-
name: "
|
|
1772
|
-
description: "
|
|
2556
|
+
name: "rollback",
|
|
2557
|
+
description: "Rollback a variable to a previous version"
|
|
1773
2558
|
},
|
|
1774
2559
|
args: {
|
|
1775
|
-
|
|
2560
|
+
key: {
|
|
1776
2561
|
type: "positional",
|
|
1777
|
-
description: "
|
|
2562
|
+
description: "Variable key",
|
|
2563
|
+
required: true
|
|
2564
|
+
},
|
|
2565
|
+
version: {
|
|
2566
|
+
type: "positional",
|
|
2567
|
+
description: "Version number to rollback to",
|
|
1778
2568
|
required: true
|
|
1779
2569
|
},
|
|
1780
2570
|
env: {
|
|
1781
2571
|
type: "string",
|
|
1782
|
-
description: "Environment (development, staging, production)"
|
|
1783
|
-
default: "development"
|
|
2572
|
+
description: "Environment (development, staging, production)"
|
|
1784
2573
|
},
|
|
1785
2574
|
app: {
|
|
1786
2575
|
type: "string",
|
|
@@ -1790,34 +2579,1126 @@ const setCommand = defineCommand({
|
|
|
1790
2579
|
type: "string",
|
|
1791
2580
|
description: "Project slug"
|
|
1792
2581
|
},
|
|
1793
|
-
|
|
1794
|
-
type: "
|
|
1795
|
-
description: "
|
|
1796
|
-
default: true
|
|
2582
|
+
branch: {
|
|
2583
|
+
type: "string",
|
|
2584
|
+
description: "Env branch name"
|
|
1797
2585
|
}
|
|
1798
2586
|
},
|
|
1799
2587
|
async run({ args }) {
|
|
1800
|
-
const
|
|
1801
|
-
if (
|
|
1802
|
-
const
|
|
1803
|
-
|
|
1804
|
-
|
|
2588
|
+
const versionNum = Number.parseInt(args.version, 10);
|
|
2589
|
+
if (Number.isNaN(versionNum) || versionNum <= 0) logError("Version must be a positive integer.");
|
|
2590
|
+
const confirmed = await consola.prompt(`Rollback ${args.key} to v${versionNum}?`, { type: "confirm" });
|
|
2591
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
2592
|
+
console.log("Cancelled.");
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
1805
2595
|
const projectConfig = loadProjectConfig();
|
|
1806
2596
|
const project = args.project ?? projectConfig.project;
|
|
1807
2597
|
const app = args.app ?? resolveApp();
|
|
1808
|
-
const res = await apiRequest("/v1/env/
|
|
1809
|
-
key,
|
|
1810
|
-
|
|
2598
|
+
const res = await apiRequest("/v1/env/rollback", {
|
|
2599
|
+
key: args.key,
|
|
2600
|
+
version: versionNum,
|
|
1811
2601
|
project,
|
|
1812
2602
|
environment: args.env,
|
|
1813
2603
|
app,
|
|
1814
|
-
|
|
2604
|
+
branch: args.branch
|
|
1815
2605
|
});
|
|
1816
2606
|
if (!res.ok) {
|
|
1817
2607
|
const text = await res.text().catch(() => "");
|
|
1818
|
-
logError(`Failed to
|
|
2608
|
+
logError(`Failed to rollback (${res.status}): ${text}`);
|
|
1819
2609
|
}
|
|
1820
|
-
console.log(`
|
|
2610
|
+
console.log(`Rolled back ${args.key} to v${versionNum}.`);
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2614
|
+
//#endregion
|
|
2615
|
+
//#region src/commands/rotation-create.ts
|
|
2616
|
+
const rotationCreateCommand = defineCommand({
|
|
2617
|
+
meta: {
|
|
2618
|
+
name: "rotation create",
|
|
2619
|
+
description: "Set up a rotation policy for an env variable"
|
|
2620
|
+
},
|
|
2621
|
+
args: {
|
|
2622
|
+
key: {
|
|
2623
|
+
type: "string",
|
|
2624
|
+
description: "Environment variable key",
|
|
2625
|
+
required: true
|
|
2626
|
+
},
|
|
2627
|
+
generator: {
|
|
2628
|
+
type: "string",
|
|
2629
|
+
description: "Generator (random-password, random-token, random-uuid)",
|
|
2630
|
+
required: true
|
|
2631
|
+
},
|
|
2632
|
+
interval: {
|
|
2633
|
+
type: "string",
|
|
2634
|
+
description: "Rotation interval in days",
|
|
2635
|
+
required: true
|
|
2636
|
+
},
|
|
2637
|
+
env: {
|
|
2638
|
+
type: "string",
|
|
2639
|
+
description: "Scope: environment"
|
|
2640
|
+
},
|
|
2641
|
+
app: {
|
|
2642
|
+
type: "string",
|
|
2643
|
+
description: "Scope: app name"
|
|
2644
|
+
},
|
|
2645
|
+
project: {
|
|
2646
|
+
type: "string",
|
|
2647
|
+
description: "Scope: project slug"
|
|
2648
|
+
}
|
|
2649
|
+
},
|
|
2650
|
+
async run({ args }) {
|
|
2651
|
+
const intervalDays = Number.parseInt(args.interval, 10);
|
|
2652
|
+
if (Number.isNaN(intervalDays) || intervalDays <= 0) logError("--interval must be a positive number of days.");
|
|
2653
|
+
const body = {
|
|
2654
|
+
key: args.key,
|
|
2655
|
+
generator: args.generator,
|
|
2656
|
+
intervalDays
|
|
2657
|
+
};
|
|
2658
|
+
if (args.project) body.project = args.project;
|
|
2659
|
+
if (args.env) body.environment = args.env;
|
|
2660
|
+
if (args.app) body.app = args.app;
|
|
2661
|
+
const res = await apiRequest("/v1/env/rotation/create", body);
|
|
2662
|
+
if (!res.ok) {
|
|
2663
|
+
const text = await res.text().catch(() => "");
|
|
2664
|
+
logError(`Failed to create rotation policy (${res.status}): ${text}`);
|
|
2665
|
+
}
|
|
2666
|
+
const data = await res.json();
|
|
2667
|
+
console.log(`\nRotation policy created: ${data.id}`);
|
|
2668
|
+
console.log(`Key: ${args.key}`);
|
|
2669
|
+
console.log(`Generator: ${args.generator}`);
|
|
2670
|
+
console.log(`Interval: ${intervalDays} days`);
|
|
2671
|
+
}
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
//#endregion
|
|
2675
|
+
//#region src/commands/rotation-list.ts
|
|
2676
|
+
function formatDate$1(date) {
|
|
2677
|
+
if (!date) return "-";
|
|
2678
|
+
return new Date(date).toLocaleDateString();
|
|
2679
|
+
}
|
|
2680
|
+
const rotationListCommand = defineCommand({
|
|
2681
|
+
meta: {
|
|
2682
|
+
name: "rotation list",
|
|
2683
|
+
description: "List rotation policies"
|
|
2684
|
+
},
|
|
2685
|
+
args: {},
|
|
2686
|
+
async run() {
|
|
2687
|
+
const res = await apiRequest("/v1/env/rotation/list", {});
|
|
2688
|
+
if (!res.ok) {
|
|
2689
|
+
const text = await res.text().catch(() => "");
|
|
2690
|
+
logError(`Failed to list rotation policies (${res.status}): ${text}`);
|
|
2691
|
+
}
|
|
2692
|
+
const data = await res.json();
|
|
2693
|
+
if (data.policies.length === 0) {
|
|
2694
|
+
console.log("No rotation policies found.");
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
2698
|
+
const idW = 8;
|
|
2699
|
+
const keyW = col(data.policies.map((p) => p.key), 3);
|
|
2700
|
+
const genW = col(data.policies.map((p) => p.generator), 9);
|
|
2701
|
+
const intW = col(data.policies.map((p) => `${p.intervalDays}d`), 8);
|
|
2702
|
+
const lastW = col(data.policies.map((p) => formatDate$1(p.lastRotatedAt)), 12);
|
|
2703
|
+
const nextW = col(data.policies.map((p) => formatDate$1(p.nextRotationAt)), 13);
|
|
2704
|
+
const header = [
|
|
2705
|
+
"ID".padEnd(idW),
|
|
2706
|
+
"KEY".padEnd(keyW),
|
|
2707
|
+
"GENERATOR".padEnd(genW),
|
|
2708
|
+
"INTERVAL".padEnd(intW),
|
|
2709
|
+
"LAST ROTATED".padEnd(lastW),
|
|
2710
|
+
"NEXT ROTATION".padEnd(nextW)
|
|
2711
|
+
].join(" ");
|
|
2712
|
+
console.log(header);
|
|
2713
|
+
console.log("-".repeat(header.length));
|
|
2714
|
+
for (const p of data.policies) {
|
|
2715
|
+
const row = [
|
|
2716
|
+
p.id.slice(0, idW).padEnd(idW),
|
|
2717
|
+
p.key.padEnd(keyW),
|
|
2718
|
+
p.generator.padEnd(genW),
|
|
2719
|
+
`${p.intervalDays}d`.padEnd(intW),
|
|
2720
|
+
formatDate$1(p.lastRotatedAt).padEnd(lastW),
|
|
2721
|
+
formatDate$1(p.nextRotationAt).padEnd(nextW)
|
|
2722
|
+
].join(" ");
|
|
2723
|
+
console.log(row);
|
|
2724
|
+
}
|
|
2725
|
+
console.log(`\n${data.policies.length} policies`);
|
|
2726
|
+
}
|
|
2727
|
+
});
|
|
2728
|
+
|
|
2729
|
+
//#endregion
|
|
2730
|
+
//#region src/commands/rotation-rotate.ts
|
|
2731
|
+
const rotationRotateCommand = defineCommand({
|
|
2732
|
+
meta: {
|
|
2733
|
+
name: "rotation rotate",
|
|
2734
|
+
description: "Manually trigger rotation for a policy"
|
|
2735
|
+
},
|
|
2736
|
+
args: { id: {
|
|
2737
|
+
type: "positional",
|
|
2738
|
+
description: "Policy ID (from `auix env rotation list`)",
|
|
2739
|
+
required: true
|
|
2740
|
+
} },
|
|
2741
|
+
async run({ args }) {
|
|
2742
|
+
const confirmed = await consola.prompt(`Rotate credentials for policy ${args.id}?`, { type: "confirm" });
|
|
2743
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
2744
|
+
console.log("Cancelled.");
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
const res = await apiRequest("/v1/env/rotation/rotate", { id: args.id });
|
|
2748
|
+
if (!res.ok) {
|
|
2749
|
+
const text = await res.text().catch(() => "");
|
|
2750
|
+
logError(`Failed to rotate (${res.status}): ${text}`);
|
|
2751
|
+
}
|
|
2752
|
+
console.log("Rotated successfully.");
|
|
2753
|
+
}
|
|
2754
|
+
});
|
|
2755
|
+
|
|
2756
|
+
//#endregion
|
|
2757
|
+
//#region src/commands/run.ts
|
|
2758
|
+
const FINGERPRINT_DIR = join(homedir(), ".auix");
|
|
2759
|
+
const FINGERPRINT_FILE = join(FINGERPRINT_DIR, "fingerprint");
|
|
2760
|
+
function computeHash(vars) {
|
|
2761
|
+
const input = Object.entries(vars).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join("\n");
|
|
2762
|
+
return createHash("sha256").update(input).digest("hex");
|
|
2763
|
+
}
|
|
2764
|
+
function getFingerprint() {
|
|
2765
|
+
if (existsSync(FINGERPRINT_FILE)) {
|
|
2766
|
+
const stored = readFileSync(FINGERPRINT_FILE, "utf-8").trim();
|
|
2767
|
+
if (stored) return stored;
|
|
2768
|
+
}
|
|
2769
|
+
const id = randomUUID();
|
|
2770
|
+
mkdirSync(FINGERPRINT_DIR, { recursive: true });
|
|
2771
|
+
writeFileSync(FINGERPRINT_FILE, id);
|
|
2772
|
+
return id;
|
|
2773
|
+
}
|
|
2774
|
+
async function ensureToken(project) {
|
|
2775
|
+
const apiKey = getApiKey();
|
|
2776
|
+
if (apiKey) return apiKey;
|
|
2777
|
+
const session = loadSession();
|
|
2778
|
+
if (session && session.project === project) return session.token;
|
|
2779
|
+
const fingerprint = getFingerprint();
|
|
2780
|
+
let gitRemote;
|
|
2781
|
+
try {
|
|
2782
|
+
gitRemote = execSync("git remote get-url origin", {
|
|
2783
|
+
encoding: "utf-8",
|
|
2784
|
+
timeout: 3e3,
|
|
2785
|
+
stdio: [
|
|
2786
|
+
"pipe",
|
|
2787
|
+
"pipe",
|
|
2788
|
+
"pipe"
|
|
2789
|
+
]
|
|
2790
|
+
}).trim();
|
|
2791
|
+
} catch {}
|
|
2792
|
+
const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI);
|
|
2793
|
+
const res = await apiRequestNoAuth("/v1/auth/anonymous/register", {
|
|
2794
|
+
fingerprint,
|
|
2795
|
+
project,
|
|
2796
|
+
meta: {
|
|
2797
|
+
os: process.platform,
|
|
2798
|
+
arch: process.arch,
|
|
2799
|
+
nodeVersion: process.version,
|
|
2800
|
+
cliVersion: "0.0.4",
|
|
2801
|
+
shell: process.env.SHELL ?? process.env.COMSPEC,
|
|
2802
|
+
ci: isCI || void 0,
|
|
2803
|
+
ciName: process.env.GITHUB_ACTIONS ? "github-actions" : process.env.GITLAB_CI ? "gitlab-ci" : process.env.CIRCLECI ? "circleci" : void 0,
|
|
2804
|
+
gitRemote,
|
|
2805
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
2806
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
if (!res.ok) {
|
|
2810
|
+
const text = await res.text().catch(() => "");
|
|
2811
|
+
if (res.status === 403) logError("Contributor access is not enabled for this project.");
|
|
2812
|
+
if (res.status === 429) {
|
|
2813
|
+
const data = JSON.parse(text);
|
|
2814
|
+
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.`);
|
|
2815
|
+
}
|
|
2816
|
+
logError(`Failed to register anonymous session (${res.status}): ${text}`);
|
|
2817
|
+
}
|
|
2818
|
+
const data = await res.json();
|
|
2819
|
+
saveSession({
|
|
2820
|
+
token: data.token,
|
|
2821
|
+
fingerprint,
|
|
2822
|
+
project,
|
|
2823
|
+
expiresAt: data.expiresAt
|
|
2824
|
+
});
|
|
2825
|
+
console.log("Created anonymous session (expires %s)", data.expiresAt);
|
|
2826
|
+
return data.token;
|
|
2827
|
+
}
|
|
2828
|
+
const runCommand = defineCommand({
|
|
2829
|
+
meta: {
|
|
2830
|
+
name: "run",
|
|
2831
|
+
description: "Run a command with resolved environment variables"
|
|
2832
|
+
},
|
|
2833
|
+
args: {
|
|
2834
|
+
env: {
|
|
2835
|
+
type: "string",
|
|
2836
|
+
description: "Environment (development, staging, production)"
|
|
2837
|
+
},
|
|
2838
|
+
app: {
|
|
2839
|
+
type: "string",
|
|
2840
|
+
description: "App name"
|
|
2841
|
+
},
|
|
2842
|
+
project: {
|
|
2843
|
+
type: "string",
|
|
2844
|
+
description: "Project slug"
|
|
2845
|
+
},
|
|
2846
|
+
branch: {
|
|
2847
|
+
type: "string",
|
|
2848
|
+
description: "Env branch name"
|
|
2849
|
+
},
|
|
2850
|
+
interpolate: {
|
|
2851
|
+
type: "boolean",
|
|
2852
|
+
description: "Interpolate variable references",
|
|
2853
|
+
default: false
|
|
2854
|
+
},
|
|
2855
|
+
watch: {
|
|
2856
|
+
type: "boolean",
|
|
2857
|
+
description: "Watch for changes and restart",
|
|
2858
|
+
default: false
|
|
2859
|
+
},
|
|
2860
|
+
"watch-interval": {
|
|
2861
|
+
type: "string",
|
|
2862
|
+
description: "Poll interval in ms",
|
|
2863
|
+
default: "5000"
|
|
2864
|
+
}
|
|
2865
|
+
},
|
|
2866
|
+
async run({ args }) {
|
|
2867
|
+
const dashIndex = process.argv.indexOf("--");
|
|
2868
|
+
const command = dashIndex !== -1 ? process.argv.slice(dashIndex + 1) : [];
|
|
2869
|
+
if (command.length === 0) logError("No command specified. Usage: auix run -- <command>");
|
|
2870
|
+
const projectConfig = loadProjectConfig();
|
|
2871
|
+
const projectSlug = args.project ?? projectConfig.project;
|
|
2872
|
+
const app = args.app ?? resolveApp();
|
|
2873
|
+
if (!projectSlug) logError("No project specified. Use --project or create .auixrc with `auix env init`.");
|
|
2874
|
+
const token = await ensureToken(projectSlug);
|
|
2875
|
+
const resolvePayload = {
|
|
2876
|
+
project: projectSlug,
|
|
2877
|
+
environment: args.env,
|
|
2878
|
+
app,
|
|
2879
|
+
branch: args.branch
|
|
2880
|
+
};
|
|
2881
|
+
const resolveVars = async () => {
|
|
2882
|
+
const res = await apiRequestWithToken("/v1/env/resolve", resolvePayload, token);
|
|
2883
|
+
if (!res.ok) {
|
|
2884
|
+
const text = await res.text().catch(() => "");
|
|
2885
|
+
logError(`Failed to resolve env (${res.status}): ${text}`);
|
|
2886
|
+
}
|
|
2887
|
+
let variables = (await res.json()).variables;
|
|
2888
|
+
if (args.interpolate) variables = interpolateEnv(variables);
|
|
2889
|
+
return variables;
|
|
2890
|
+
};
|
|
2891
|
+
const spawnChild = (vars) => {
|
|
2892
|
+
const count = Object.keys(vars).length;
|
|
2893
|
+
console.log(`Injecting ${count} variables`);
|
|
2894
|
+
return spawn(command[0], command.slice(1), {
|
|
2895
|
+
stdio: "inherit",
|
|
2896
|
+
env: {
|
|
2897
|
+
...process.env,
|
|
2898
|
+
...vars
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
};
|
|
2902
|
+
let variables = await resolveVars();
|
|
2903
|
+
let child = spawnChild(variables);
|
|
2904
|
+
if (!args.watch) {
|
|
2905
|
+
child.on("close", (code) => {
|
|
2906
|
+
process.exit(code ?? 1);
|
|
2907
|
+
});
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
let currentHash = computeHash(variables);
|
|
2911
|
+
const interval = Number.parseInt(args["watch-interval"], 10);
|
|
2912
|
+
console.log("Watching for changes...");
|
|
2913
|
+
const timer = setInterval(async () => {
|
|
2914
|
+
try {
|
|
2915
|
+
const res = await apiRequestWithToken("/v1/env/check", resolvePayload, token);
|
|
2916
|
+
if (!res.ok) return;
|
|
2917
|
+
if ((await res.json()).hash === currentHash) return;
|
|
2918
|
+
console.log("Env vars changed, restarting...");
|
|
2919
|
+
child.kill("SIGTERM");
|
|
2920
|
+
variables = await resolveVars();
|
|
2921
|
+
currentHash = computeHash(variables);
|
|
2922
|
+
child = spawnChild(variables);
|
|
2923
|
+
} catch {}
|
|
2924
|
+
}, interval);
|
|
2925
|
+
const cleanup = () => {
|
|
2926
|
+
clearInterval(timer);
|
|
2927
|
+
child.kill("SIGTERM");
|
|
2928
|
+
process.exit(0);
|
|
2929
|
+
};
|
|
2930
|
+
process.on("SIGINT", cleanup);
|
|
2931
|
+
process.on("SIGTERM", cleanup);
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2934
|
+
|
|
2935
|
+
//#endregion
|
|
2936
|
+
//#region src/commands/set.ts
|
|
2937
|
+
const setCommand = defineCommand({
|
|
2938
|
+
meta: {
|
|
2939
|
+
name: "set",
|
|
2940
|
+
description: "Set a single environment variable"
|
|
2941
|
+
},
|
|
2942
|
+
args: {
|
|
2943
|
+
pair: {
|
|
2944
|
+
type: "positional",
|
|
2945
|
+
description: "KEY=VALUE pair",
|
|
2946
|
+
required: true
|
|
2947
|
+
},
|
|
2948
|
+
env: {
|
|
2949
|
+
type: "string",
|
|
2950
|
+
description: "Environment (development, staging, production)"
|
|
2951
|
+
},
|
|
2952
|
+
app: {
|
|
2953
|
+
type: "string",
|
|
2954
|
+
description: "App name"
|
|
2955
|
+
},
|
|
2956
|
+
project: {
|
|
2957
|
+
type: "string",
|
|
2958
|
+
description: "Project slug"
|
|
2959
|
+
},
|
|
2960
|
+
branch: {
|
|
2961
|
+
type: "string",
|
|
2962
|
+
description: "Env branch name"
|
|
2963
|
+
},
|
|
2964
|
+
secret: {
|
|
2965
|
+
type: "boolean",
|
|
2966
|
+
description: "Mark as secret",
|
|
2967
|
+
default: true
|
|
2968
|
+
},
|
|
2969
|
+
description: {
|
|
2970
|
+
type: "string",
|
|
2971
|
+
description: "Variable description"
|
|
2972
|
+
}
|
|
2973
|
+
},
|
|
2974
|
+
async run({ args }) {
|
|
2975
|
+
const eqIndex = args.pair.indexOf("=");
|
|
2976
|
+
if (eqIndex === -1) logError("Expected KEY=VALUE format.");
|
|
2977
|
+
const key = args.pair.slice(0, eqIndex);
|
|
2978
|
+
const value = args.pair.slice(eqIndex + 1);
|
|
2979
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) logError("Invalid key. Use uppercase letters, digits, and underscores.");
|
|
2980
|
+
const projectConfig = loadProjectConfig();
|
|
2981
|
+
const project = args.project ?? projectConfig.project;
|
|
2982
|
+
const app = args.app ?? resolveApp();
|
|
2983
|
+
const res = await apiRequest("/v1/env/set", {
|
|
2984
|
+
key,
|
|
2985
|
+
value,
|
|
2986
|
+
project,
|
|
2987
|
+
environment: args.env,
|
|
2988
|
+
app,
|
|
2989
|
+
branch: args.branch,
|
|
2990
|
+
isSecret: args.secret,
|
|
2991
|
+
description: args.description
|
|
2992
|
+
});
|
|
2993
|
+
if (!res.ok) {
|
|
2994
|
+
const text = await res.text().catch(() => "");
|
|
2995
|
+
logError(`Failed to set variable (${res.status}): ${text}`);
|
|
2996
|
+
}
|
|
2997
|
+
console.log(`Set ${key}`);
|
|
2998
|
+
}
|
|
2999
|
+
});
|
|
3000
|
+
|
|
3001
|
+
//#endregion
|
|
3002
|
+
//#region src/commands/share-access.ts
|
|
3003
|
+
const shareAccessCommand = defineCommand({
|
|
3004
|
+
meta: {
|
|
3005
|
+
name: "share access",
|
|
3006
|
+
description: "Access a shared secret by ID"
|
|
3007
|
+
},
|
|
3008
|
+
args: { id: {
|
|
3009
|
+
type: "positional",
|
|
3010
|
+
description: "The share link ID",
|
|
3011
|
+
required: true
|
|
3012
|
+
} },
|
|
3013
|
+
async run({ args }) {
|
|
3014
|
+
const res = await apiRequest("/v1/env/share/access", { id: args.id });
|
|
3015
|
+
if (res.status === 404) logError("Share link not found.");
|
|
3016
|
+
if (res.status === 410) logError("Share link has expired or has already been accessed.");
|
|
3017
|
+
if (!res.ok) {
|
|
3018
|
+
const text = await res.text().catch(() => "");
|
|
3019
|
+
logError(`Failed to access share link (${res.status}): ${text}`);
|
|
3020
|
+
}
|
|
3021
|
+
const data = await res.json();
|
|
3022
|
+
console.log(data.value);
|
|
3023
|
+
}
|
|
3024
|
+
});
|
|
3025
|
+
|
|
3026
|
+
//#endregion
|
|
3027
|
+
//#region src/commands/share-create.ts
|
|
3028
|
+
const shareCreateCommand = defineCommand({
|
|
3029
|
+
meta: {
|
|
3030
|
+
name: "share create",
|
|
3031
|
+
description: "Create a one-time share link for a secret"
|
|
3032
|
+
},
|
|
3033
|
+
args: {
|
|
3034
|
+
value: {
|
|
3035
|
+
type: "positional",
|
|
3036
|
+
description: "The secret value to share",
|
|
3037
|
+
required: true
|
|
3038
|
+
},
|
|
3039
|
+
expires: {
|
|
3040
|
+
type: "string",
|
|
3041
|
+
description: "Expiry in minutes (default: 60, max: 10080)",
|
|
3042
|
+
default: "60"
|
|
3043
|
+
}
|
|
3044
|
+
},
|
|
3045
|
+
async run({ args }) {
|
|
3046
|
+
const expiresInMinutes = Number.parseInt(args.expires, 10);
|
|
3047
|
+
if (Number.isNaN(expiresInMinutes) || expiresInMinutes < 1 || expiresInMinutes > 10080) logError("--expires must be between 1 and 10080 minutes (7 days).");
|
|
3048
|
+
const res = await apiRequest("/v1/env/share/create", {
|
|
3049
|
+
value: args.value,
|
|
3050
|
+
expiresInMinutes
|
|
3051
|
+
});
|
|
3052
|
+
if (!res.ok) {
|
|
3053
|
+
const text = await res.text().catch(() => "");
|
|
3054
|
+
logError(`Failed to create share link (${res.status}): ${text}`);
|
|
3055
|
+
}
|
|
3056
|
+
const data = await res.json();
|
|
3057
|
+
const appUrl = getAppUrl().replace(/\/$/, "");
|
|
3058
|
+
console.log(`\nShare URL: ${appUrl}/share/${data.id}`);
|
|
3059
|
+
console.log(`Expires at: ${data.expiresAt}`);
|
|
3060
|
+
console.log("\nThis link can only be accessed once.");
|
|
3061
|
+
}
|
|
3062
|
+
});
|
|
3063
|
+
|
|
3064
|
+
//#endregion
|
|
3065
|
+
//#region src/commands/switch.ts
|
|
3066
|
+
async function createApiKey(appUrl, token, organizationId) {
|
|
3067
|
+
const base = appUrl.replace(/\/$/, "");
|
|
3068
|
+
const res = await fetch(`${base}/api/cli/create-key`, {
|
|
3069
|
+
method: "POST",
|
|
3070
|
+
headers: {
|
|
3071
|
+
"Content-Type": "application/json",
|
|
3072
|
+
Authorization: `Bearer ${token}`
|
|
3073
|
+
},
|
|
3074
|
+
body: JSON.stringify({ organizationId })
|
|
3075
|
+
});
|
|
3076
|
+
if (!res.ok) {
|
|
3077
|
+
const text = await res.text().catch(() => "");
|
|
3078
|
+
logError(`Failed to create API key (${res.status}): ${text}`);
|
|
3079
|
+
}
|
|
3080
|
+
return (await res.json()).key;
|
|
3081
|
+
}
|
|
3082
|
+
const switchCommand = defineCommand({
|
|
3083
|
+
meta: {
|
|
3084
|
+
name: "switch",
|
|
3085
|
+
description: "Switch to a different organization"
|
|
3086
|
+
},
|
|
3087
|
+
async run() {
|
|
3088
|
+
const config = loadGlobalConfig();
|
|
3089
|
+
const appUrl = config.appUrl ?? getAppUrl();
|
|
3090
|
+
if (!config.apiKey) logError("Not logged in. Run `auix login`.");
|
|
3091
|
+
const base = appUrl.replace(/\/$/, "");
|
|
3092
|
+
consola.info("Re-authenticating to switch organization...\n");
|
|
3093
|
+
const codeRes = await fetch(`${base}/api/auth/device/code`, {
|
|
3094
|
+
method: "POST",
|
|
3095
|
+
headers: { "Content-Type": "application/json" },
|
|
3096
|
+
body: JSON.stringify({ client_id: "auix-cli" })
|
|
3097
|
+
});
|
|
3098
|
+
if (!codeRes.ok) logError("Failed to start authentication. Is the server running?");
|
|
3099
|
+
const codeData = await codeRes.json();
|
|
3100
|
+
console.log(`Open: ${codeData.verification_uri_complete}`);
|
|
3101
|
+
console.log(`Code: ${codeData.user_code}\n`);
|
|
3102
|
+
try {
|
|
3103
|
+
const { execSync } = await import("node:child_process");
|
|
3104
|
+
execSync(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} ${JSON.stringify(codeData.verification_uri_complete)}`, { stdio: "ignore" });
|
|
3105
|
+
} catch {}
|
|
3106
|
+
const deadline = Date.now() + codeData.expires_in * 1e3;
|
|
3107
|
+
const interval = Math.max((codeData.interval || 5) * 1e3, 5e3);
|
|
3108
|
+
let token = "";
|
|
3109
|
+
while (Date.now() < deadline) {
|
|
3110
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
3111
|
+
const tokenRes = await fetch(`${base}/api/auth/device/token`, {
|
|
3112
|
+
method: "POST",
|
|
3113
|
+
headers: { "Content-Type": "application/json" },
|
|
3114
|
+
body: JSON.stringify({
|
|
3115
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
3116
|
+
device_code: codeData.device_code,
|
|
3117
|
+
client_id: "auix-cli"
|
|
3118
|
+
})
|
|
3119
|
+
});
|
|
3120
|
+
if (tokenRes.ok) {
|
|
3121
|
+
token = (await tokenRes.json()).access_token;
|
|
3122
|
+
break;
|
|
3123
|
+
}
|
|
3124
|
+
const err = await tokenRes.json().catch(() => ({}));
|
|
3125
|
+
if (err.error === "authorization_pending" || err.error === "slow_down") continue;
|
|
3126
|
+
if (err.error === "expired_token") logError("Code expired.");
|
|
3127
|
+
if (err.error === "access_denied") logError("Access denied.");
|
|
3128
|
+
logError(`Unexpected error: ${err.error ?? tokenRes.status}`);
|
|
3129
|
+
}
|
|
3130
|
+
if (!token) logError("Code expired. Try again.");
|
|
3131
|
+
const orgsRes = await fetch(`${base}/api/auth/organization/list`, { headers: { Authorization: `Bearer ${token}` } });
|
|
3132
|
+
if (!orgsRes.ok) logError("Failed to list organizations.");
|
|
3133
|
+
const orgs = await orgsRes.json();
|
|
3134
|
+
if (orgs.length === 0) logError("No organizations found.");
|
|
3135
|
+
const selected = await consola.prompt("Select organization", {
|
|
3136
|
+
type: "select",
|
|
3137
|
+
options: orgs.map((o) => ({
|
|
3138
|
+
label: `${o.name} (${o.slug})`,
|
|
3139
|
+
value: o.id
|
|
3140
|
+
}))
|
|
3141
|
+
});
|
|
3142
|
+
if (typeof selected === "symbol") logError("Cancelled.");
|
|
3143
|
+
const org = orgs.find((o) => o.id === selected);
|
|
3144
|
+
const apiKey = await createApiKey(appUrl, token, org.id);
|
|
3145
|
+
saveGlobalConfig({
|
|
3146
|
+
...config,
|
|
3147
|
+
apiKey,
|
|
3148
|
+
organization: {
|
|
3149
|
+
name: org.name,
|
|
3150
|
+
slug: org.slug
|
|
3151
|
+
}
|
|
3152
|
+
});
|
|
3153
|
+
console.log(`Switched to ${org.name}.`);
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3156
|
+
|
|
3157
|
+
//#endregion
|
|
3158
|
+
//#region src/commands/sync-add.ts
|
|
3159
|
+
const syncAddCommand = defineCommand({
|
|
3160
|
+
meta: {
|
|
3161
|
+
name: "sync add",
|
|
3162
|
+
description: "Add an env sync target"
|
|
3163
|
+
},
|
|
3164
|
+
args: {
|
|
3165
|
+
provider: {
|
|
3166
|
+
type: "string",
|
|
3167
|
+
description: "Provider (vercel, github-actions, aws-ssm)",
|
|
3168
|
+
required: true
|
|
3169
|
+
},
|
|
3170
|
+
name: {
|
|
3171
|
+
type: "string",
|
|
3172
|
+
description: "Target name",
|
|
3173
|
+
required: true
|
|
3174
|
+
},
|
|
3175
|
+
env: {
|
|
3176
|
+
type: "string",
|
|
3177
|
+
description: "Scope: environment"
|
|
3178
|
+
},
|
|
3179
|
+
app: {
|
|
3180
|
+
type: "string",
|
|
3181
|
+
description: "Scope: app name"
|
|
3182
|
+
},
|
|
3183
|
+
project: {
|
|
3184
|
+
type: "string",
|
|
3185
|
+
description: "Scope: project slug"
|
|
3186
|
+
},
|
|
3187
|
+
"auto-sync": {
|
|
3188
|
+
type: "boolean",
|
|
3189
|
+
description: "Enable automatic sync",
|
|
3190
|
+
default: false
|
|
3191
|
+
}
|
|
3192
|
+
},
|
|
3193
|
+
async run({ args }) {
|
|
3194
|
+
const provider = args.provider;
|
|
3195
|
+
const valid = [
|
|
3196
|
+
"vercel",
|
|
3197
|
+
"github-actions",
|
|
3198
|
+
"aws-ssm"
|
|
3199
|
+
];
|
|
3200
|
+
if (!valid.includes(provider)) logError(`Invalid provider: ${provider}. Must be one of: ${valid.join(", ")}`);
|
|
3201
|
+
const credentials = {};
|
|
3202
|
+
if (provider === "vercel" || provider === "github-actions") {
|
|
3203
|
+
const token = await consola.prompt("API token:", { type: "text" });
|
|
3204
|
+
if (!token || typeof token === "symbol") logError("Token is required.");
|
|
3205
|
+
credentials.token = token;
|
|
3206
|
+
} else if (provider === "aws-ssm") {
|
|
3207
|
+
const accessKeyId = await consola.prompt("AWS Access Key ID:", { type: "text" });
|
|
3208
|
+
if (!accessKeyId || typeof accessKeyId === "symbol") logError("Access Key ID is required.");
|
|
3209
|
+
credentials.accessKeyId = accessKeyId;
|
|
3210
|
+
const secretAccessKey = await consola.prompt("AWS Secret Access Key:", { type: "text" });
|
|
3211
|
+
if (!secretAccessKey || typeof secretAccessKey === "symbol") logError("Secret Access Key is required.");
|
|
3212
|
+
credentials.secretAccessKey = secretAccessKey;
|
|
3213
|
+
}
|
|
3214
|
+
const scope = {};
|
|
3215
|
+
if (args.project) scope.project = args.project;
|
|
3216
|
+
if (args.env) scope.environment = args.env;
|
|
3217
|
+
if (args.app) scope.app = args.app;
|
|
3218
|
+
const res = await apiRequest("/v1/env/sync/targets/create", {
|
|
3219
|
+
provider,
|
|
3220
|
+
name: args.name,
|
|
3221
|
+
credentials,
|
|
3222
|
+
scope: Object.keys(scope).length > 0 ? scope : void 0,
|
|
3223
|
+
autoSync: args["auto-sync"]
|
|
3224
|
+
});
|
|
3225
|
+
if (!res.ok) {
|
|
3226
|
+
const text = await res.text().catch(() => "");
|
|
3227
|
+
logError(`Failed to add sync target (${res.status}): ${text}`);
|
|
3228
|
+
}
|
|
3229
|
+
const data = await res.json();
|
|
3230
|
+
console.log(`Added sync target: ${args.name} (${data.id.slice(0, 8)})`);
|
|
3231
|
+
}
|
|
3232
|
+
});
|
|
3233
|
+
|
|
3234
|
+
//#endregion
|
|
3235
|
+
//#region src/commands/sync-list.ts
|
|
3236
|
+
const syncListCommand = defineCommand({
|
|
3237
|
+
meta: {
|
|
3238
|
+
name: "sync list",
|
|
3239
|
+
description: "List env sync targets"
|
|
3240
|
+
},
|
|
3241
|
+
args: {},
|
|
3242
|
+
async run() {
|
|
3243
|
+
const res = await apiRequest("/v1/env/sync/targets/list", {});
|
|
3244
|
+
if (!res.ok) {
|
|
3245
|
+
const text = await res.text().catch(() => "");
|
|
3246
|
+
logError(`Failed to list sync targets (${res.status}): ${text}`);
|
|
3247
|
+
}
|
|
3248
|
+
const data = await res.json();
|
|
3249
|
+
if (data.targets.length === 0) {
|
|
3250
|
+
console.log("No sync targets found.");
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
3254
|
+
const idW = 8;
|
|
3255
|
+
const provW = col(data.targets.map((t) => t.provider), 8);
|
|
3256
|
+
const nameW = col(data.targets.map((t) => t.name), 4);
|
|
3257
|
+
const formatScope = (s) => {
|
|
3258
|
+
if (!s) return "-";
|
|
3259
|
+
const parts = [];
|
|
3260
|
+
if (s.project) parts.push(`prj:${s.project}`);
|
|
3261
|
+
if (s.environment) parts.push(`env:${s.environment}`);
|
|
3262
|
+
if (s.app) parts.push(`app:${s.app}`);
|
|
3263
|
+
return parts.length > 0 ? parts.join(",") : "-";
|
|
3264
|
+
};
|
|
3265
|
+
const scopeW = col(data.targets.map((t) => formatScope(t.scope)), 5);
|
|
3266
|
+
const autoW = 9;
|
|
3267
|
+
const header = [
|
|
3268
|
+
"ID".padEnd(idW),
|
|
3269
|
+
"PROVIDER".padEnd(provW),
|
|
3270
|
+
"NAME".padEnd(nameW),
|
|
3271
|
+
"SCOPE".padEnd(scopeW),
|
|
3272
|
+
"AUTO-SYNC".padEnd(autoW),
|
|
3273
|
+
"LAST SYNCED"
|
|
3274
|
+
].join(" ");
|
|
3275
|
+
console.log(header);
|
|
3276
|
+
console.log("-".repeat(header.length));
|
|
3277
|
+
for (const t of data.targets) {
|
|
3278
|
+
const synced = t.lastSyncedAt ? new Date(t.lastSyncedAt).toLocaleString() : "-";
|
|
3279
|
+
const row = [
|
|
3280
|
+
t.id.slice(0, idW).padEnd(idW),
|
|
3281
|
+
t.provider.padEnd(provW),
|
|
3282
|
+
t.name.padEnd(nameW),
|
|
3283
|
+
formatScope(t.scope).padEnd(scopeW),
|
|
3284
|
+
(t.autoSync ? "yes" : "no").padEnd(autoW),
|
|
3285
|
+
synced
|
|
3286
|
+
].join(" ");
|
|
3287
|
+
console.log(row);
|
|
3288
|
+
}
|
|
3289
|
+
console.log(`\n${data.targets.length} targets`);
|
|
3290
|
+
}
|
|
3291
|
+
});
|
|
3292
|
+
|
|
3293
|
+
//#endregion
|
|
3294
|
+
//#region src/commands/sync-push.ts
|
|
3295
|
+
const syncPushCommand = defineCommand({
|
|
3296
|
+
meta: {
|
|
3297
|
+
name: "sync push",
|
|
3298
|
+
description: "Push env vars to a sync target"
|
|
3299
|
+
},
|
|
3300
|
+
args: {
|
|
3301
|
+
name: {
|
|
3302
|
+
type: "string",
|
|
3303
|
+
description: "Target name"
|
|
3304
|
+
},
|
|
3305
|
+
id: {
|
|
3306
|
+
type: "string",
|
|
3307
|
+
description: "Target ID"
|
|
3308
|
+
}
|
|
3309
|
+
},
|
|
3310
|
+
async run({ args }) {
|
|
3311
|
+
let targetId = args.id;
|
|
3312
|
+
if (!targetId && !args.name) logError("Provide --name or --id to identify the target.");
|
|
3313
|
+
if (!targetId && args.name) {
|
|
3314
|
+
const listRes = await apiRequest("/v1/env/sync/targets/list", {});
|
|
3315
|
+
if (!listRes.ok) {
|
|
3316
|
+
const text = await listRes.text().catch(() => "");
|
|
3317
|
+
logError(`Failed to list targets (${listRes.status}): ${text}`);
|
|
3318
|
+
}
|
|
3319
|
+
const match = (await listRes.json()).targets.find((t) => t.name === args.name);
|
|
3320
|
+
if (!match) logError(`No sync target found with name: ${args.name}`);
|
|
3321
|
+
targetId = match.id;
|
|
3322
|
+
}
|
|
3323
|
+
console.log("Syncing...");
|
|
3324
|
+
const res = await apiRequest("/v1/env/sync/push", { targetId });
|
|
3325
|
+
if (!res.ok) {
|
|
3326
|
+
const text = await res.text().catch(() => "");
|
|
3327
|
+
logError(`Failed to sync (${res.status}): ${text}`);
|
|
3328
|
+
}
|
|
3329
|
+
const data = await res.json();
|
|
3330
|
+
if (data.status === "success") console.log(`Synced ${data.variableCount} variables.`);
|
|
3331
|
+
else logError(`Sync failed: ${data.error}`);
|
|
3332
|
+
}
|
|
3333
|
+
});
|
|
3334
|
+
|
|
3335
|
+
//#endregion
|
|
3336
|
+
//#region src/commands/sync-remove.ts
|
|
3337
|
+
const syncRemoveCommand = defineCommand({
|
|
3338
|
+
meta: {
|
|
3339
|
+
name: "sync remove",
|
|
3340
|
+
description: "Remove an env sync target"
|
|
3341
|
+
},
|
|
3342
|
+
args: { id: {
|
|
3343
|
+
type: "positional",
|
|
3344
|
+
description: "Target ID (from `auix env sync list`)",
|
|
3345
|
+
required: true
|
|
3346
|
+
} },
|
|
3347
|
+
async run({ args }) {
|
|
3348
|
+
const confirmed = await consola.prompt(`Delete sync target ${args.id}?`, { type: "confirm" });
|
|
3349
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
3350
|
+
console.log("Cancelled.");
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
const res = await apiRequest("/v1/env/sync/targets/delete", { id: args.id });
|
|
3354
|
+
if (!res.ok) {
|
|
3355
|
+
const text = await res.text().catch(() => "");
|
|
3356
|
+
logError(`Failed to delete (${res.status}): ${text}`);
|
|
3357
|
+
}
|
|
3358
|
+
console.log("Deleted.");
|
|
3359
|
+
}
|
|
3360
|
+
});
|
|
3361
|
+
|
|
3362
|
+
//#endregion
|
|
3363
|
+
//#region src/commands/token-create.ts
|
|
3364
|
+
const tokenCreateCommand = defineCommand({
|
|
3365
|
+
meta: {
|
|
3366
|
+
name: "token create",
|
|
3367
|
+
description: "Create a service token"
|
|
3368
|
+
},
|
|
3369
|
+
args: {
|
|
3370
|
+
name: {
|
|
3371
|
+
type: "string",
|
|
3372
|
+
description: "Token name",
|
|
3373
|
+
required: true
|
|
3374
|
+
},
|
|
3375
|
+
permissions: {
|
|
3376
|
+
type: "string",
|
|
3377
|
+
description: "Comma-separated permissions (env:read,env:write,env:delete,env:admin)",
|
|
3378
|
+
default: "env:read"
|
|
3379
|
+
},
|
|
3380
|
+
env: {
|
|
3381
|
+
type: "string",
|
|
3382
|
+
description: "Scope: environment"
|
|
3383
|
+
},
|
|
3384
|
+
app: {
|
|
3385
|
+
type: "string",
|
|
3386
|
+
description: "Scope: app name"
|
|
3387
|
+
},
|
|
3388
|
+
project: {
|
|
3389
|
+
type: "string",
|
|
3390
|
+
description: "Scope: project slug"
|
|
3391
|
+
},
|
|
3392
|
+
expires: {
|
|
3393
|
+
type: "string",
|
|
3394
|
+
description: "Expiry in days (e.g. 30)"
|
|
3395
|
+
}
|
|
3396
|
+
},
|
|
3397
|
+
async run({ args }) {
|
|
3398
|
+
const permissions = args.permissions.split(",").map((p) => p.trim());
|
|
3399
|
+
const scope = {};
|
|
3400
|
+
if (args.project) scope.project = args.project;
|
|
3401
|
+
if (args.env) scope.environment = args.env;
|
|
3402
|
+
if (args.app) scope.app = args.app;
|
|
3403
|
+
const body = {
|
|
3404
|
+
name: args.name,
|
|
3405
|
+
permissions
|
|
3406
|
+
};
|
|
3407
|
+
if (Object.keys(scope).length > 0) body.scope = scope;
|
|
3408
|
+
if (args.expires) {
|
|
3409
|
+
const days = Number.parseInt(args.expires, 10);
|
|
3410
|
+
if (Number.isNaN(days) || days <= 0) logError("--expires must be a positive number of days.");
|
|
3411
|
+
body.expiresInDays = days;
|
|
3412
|
+
}
|
|
3413
|
+
const res = await apiRequest("/v1/env/tokens/create", body);
|
|
3414
|
+
if (!res.ok) {
|
|
3415
|
+
const text = await res.text().catch(() => "");
|
|
3416
|
+
logError(`Failed to create token (${res.status}): ${text}`);
|
|
3417
|
+
}
|
|
3418
|
+
const data = await res.json();
|
|
3419
|
+
console.log(`\nToken created: ${data.id}`);
|
|
3420
|
+
console.log(`Key: ${data.key}`);
|
|
3421
|
+
console.log("\nSave this key — it won't be shown again.");
|
|
3422
|
+
}
|
|
3423
|
+
});
|
|
3424
|
+
|
|
3425
|
+
//#endregion
|
|
3426
|
+
//#region src/commands/token-list.ts
|
|
3427
|
+
function formatScope(scope) {
|
|
3428
|
+
if (!scope) return "-";
|
|
3429
|
+
const parts = [];
|
|
3430
|
+
if (scope.project) parts.push(`prj:${scope.project}`);
|
|
3431
|
+
if (scope.environment) parts.push(`env:${scope.environment}`);
|
|
3432
|
+
if (scope.app) parts.push(`app:${scope.app}`);
|
|
3433
|
+
return parts.length > 0 ? parts.join(",") : "-";
|
|
3434
|
+
}
|
|
3435
|
+
function formatDate(date) {
|
|
3436
|
+
if (!date) return "-";
|
|
3437
|
+
return new Date(date).toLocaleDateString();
|
|
3438
|
+
}
|
|
3439
|
+
const tokenListCommand = defineCommand({
|
|
3440
|
+
meta: {
|
|
3441
|
+
name: "token list",
|
|
3442
|
+
description: "List service tokens"
|
|
3443
|
+
},
|
|
3444
|
+
args: {},
|
|
3445
|
+
async run() {
|
|
3446
|
+
const res = await apiRequest("/v1/env/tokens/list", {});
|
|
3447
|
+
if (!res.ok) {
|
|
3448
|
+
const text = await res.text().catch(() => "");
|
|
3449
|
+
logError(`Failed to list tokens (${res.status}): ${text}`);
|
|
3450
|
+
}
|
|
3451
|
+
const data = await res.json();
|
|
3452
|
+
if (data.tokens.length === 0) {
|
|
3453
|
+
console.log("No service tokens found.");
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
3457
|
+
const idW = 8;
|
|
3458
|
+
const nameW = col(data.tokens.map((t) => t.name), 4);
|
|
3459
|
+
const permW = col(data.tokens.map((t) => t.permissions.join(",")), 11);
|
|
3460
|
+
const scopeW = col(data.tokens.map((t) => formatScope(t.scope)), 5);
|
|
3461
|
+
const expW = col(data.tokens.map((t) => formatDate(t.expiresAt)), 7);
|
|
3462
|
+
const usedW = col(data.tokens.map((t) => formatDate(t.lastUsedAt)), 9);
|
|
3463
|
+
const header = [
|
|
3464
|
+
"ID".padEnd(idW),
|
|
3465
|
+
"NAME".padEnd(nameW),
|
|
3466
|
+
"PERMISSIONS".padEnd(permW),
|
|
3467
|
+
"SCOPE".padEnd(scopeW),
|
|
3468
|
+
"EXPIRES".padEnd(expW),
|
|
3469
|
+
"LAST USED".padEnd(usedW)
|
|
3470
|
+
].join(" ");
|
|
3471
|
+
console.log(header);
|
|
3472
|
+
console.log("-".repeat(header.length));
|
|
3473
|
+
for (const t of data.tokens) {
|
|
3474
|
+
const row = [
|
|
3475
|
+
t.id.slice(0, idW).padEnd(idW),
|
|
3476
|
+
t.name.padEnd(nameW),
|
|
3477
|
+
t.permissions.join(",").padEnd(permW),
|
|
3478
|
+
formatScope(t.scope).padEnd(scopeW),
|
|
3479
|
+
formatDate(t.expiresAt).padEnd(expW),
|
|
3480
|
+
formatDate(t.lastUsedAt).padEnd(usedW)
|
|
3481
|
+
].join(" ");
|
|
3482
|
+
console.log(row);
|
|
3483
|
+
}
|
|
3484
|
+
console.log(`\n${data.tokens.length} tokens`);
|
|
3485
|
+
}
|
|
3486
|
+
});
|
|
3487
|
+
|
|
3488
|
+
//#endregion
|
|
3489
|
+
//#region src/commands/token-revoke.ts
|
|
3490
|
+
const tokenRevokeCommand = defineCommand({
|
|
3491
|
+
meta: {
|
|
3492
|
+
name: "token revoke",
|
|
3493
|
+
description: "Revoke a service token"
|
|
3494
|
+
},
|
|
3495
|
+
args: { id: {
|
|
3496
|
+
type: "positional",
|
|
3497
|
+
description: "Token ID (from `auix env token list`)",
|
|
3498
|
+
required: true
|
|
3499
|
+
} },
|
|
3500
|
+
async run({ args }) {
|
|
3501
|
+
const confirmed = await consola.prompt(`Revoke token ${args.id}?`, { type: "confirm" });
|
|
3502
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
3503
|
+
console.log("Cancelled.");
|
|
3504
|
+
return;
|
|
3505
|
+
}
|
|
3506
|
+
const res = await apiRequest("/v1/env/tokens/revoke", { id: args.id });
|
|
3507
|
+
if (!res.ok) {
|
|
3508
|
+
const text = await res.text().catch(() => "");
|
|
3509
|
+
logError(`Failed to revoke token (${res.status}): ${text}`);
|
|
3510
|
+
}
|
|
3511
|
+
console.log("Revoked.");
|
|
3512
|
+
}
|
|
3513
|
+
});
|
|
3514
|
+
|
|
3515
|
+
//#endregion
|
|
3516
|
+
//#region src/commands/unlock.ts
|
|
3517
|
+
const unlockCommand = defineCommand({
|
|
3518
|
+
meta: {
|
|
3519
|
+
name: "unlock",
|
|
3520
|
+
description: "Unlock a config scope"
|
|
3521
|
+
},
|
|
3522
|
+
args: {
|
|
3523
|
+
env: {
|
|
3524
|
+
type: "string",
|
|
3525
|
+
description: "Environment (development, staging, production)"
|
|
3526
|
+
},
|
|
3527
|
+
app: {
|
|
3528
|
+
type: "string",
|
|
3529
|
+
description: "App name"
|
|
3530
|
+
},
|
|
3531
|
+
project: {
|
|
3532
|
+
type: "string",
|
|
3533
|
+
description: "Project slug"
|
|
3534
|
+
}
|
|
3535
|
+
},
|
|
3536
|
+
async run({ args }) {
|
|
3537
|
+
const confirmed = await consola.prompt("Unlock this config scope?", { type: "confirm" });
|
|
3538
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
3539
|
+
console.log("Cancelled.");
|
|
3540
|
+
return;
|
|
3541
|
+
}
|
|
3542
|
+
const projectConfig = loadProjectConfig();
|
|
3543
|
+
const project = args.project ?? projectConfig.project;
|
|
3544
|
+
const app = args.app ?? resolveApp();
|
|
3545
|
+
const res = await apiRequest("/v1/env/unlock", {
|
|
3546
|
+
project,
|
|
3547
|
+
environment: args.env,
|
|
3548
|
+
app
|
|
3549
|
+
});
|
|
3550
|
+
if (!res.ok) {
|
|
3551
|
+
const text = await res.text().catch(() => "");
|
|
3552
|
+
logError(`Failed to unlock (${res.status}): ${text}`);
|
|
3553
|
+
}
|
|
3554
|
+
console.log("Unlocked.");
|
|
3555
|
+
}
|
|
3556
|
+
});
|
|
3557
|
+
|
|
3558
|
+
//#endregion
|
|
3559
|
+
//#region src/commands/webhook-add.ts
|
|
3560
|
+
const VALID_EVENTS = [
|
|
3561
|
+
"env:created",
|
|
3562
|
+
"env:updated",
|
|
3563
|
+
"env:deleted"
|
|
3564
|
+
];
|
|
3565
|
+
const webhookAddCommand = defineCommand({
|
|
3566
|
+
meta: {
|
|
3567
|
+
name: "add",
|
|
3568
|
+
description: "Register a webhook for env var changes"
|
|
3569
|
+
},
|
|
3570
|
+
args: {
|
|
3571
|
+
url: {
|
|
3572
|
+
type: "positional",
|
|
3573
|
+
description: "Webhook URL",
|
|
3574
|
+
required: true
|
|
3575
|
+
},
|
|
3576
|
+
events: {
|
|
3577
|
+
type: "string",
|
|
3578
|
+
description: "Comma-separated events (env:created,env:updated,env:deleted)",
|
|
3579
|
+
default: "env:created,env:updated,env:deleted"
|
|
3580
|
+
},
|
|
3581
|
+
env: {
|
|
3582
|
+
type: "string",
|
|
3583
|
+
description: "Scope: environment filter"
|
|
3584
|
+
},
|
|
3585
|
+
app: {
|
|
3586
|
+
type: "string",
|
|
3587
|
+
description: "Scope: app filter"
|
|
3588
|
+
},
|
|
3589
|
+
project: {
|
|
3590
|
+
type: "string",
|
|
3591
|
+
description: "Scope: project filter"
|
|
3592
|
+
}
|
|
3593
|
+
},
|
|
3594
|
+
async run({ args }) {
|
|
3595
|
+
const events = args.events.split(",").map((e) => e.trim());
|
|
3596
|
+
for (const e of events) if (!VALID_EVENTS.includes(e)) logError(`Invalid event "${e}". Valid: ${VALID_EVENTS.join(", ")}`);
|
|
3597
|
+
const scope = {};
|
|
3598
|
+
if (args.project) scope.project = args.project;
|
|
3599
|
+
if (args.env) scope.environment = args.env;
|
|
3600
|
+
if (args.app) scope.app = args.app;
|
|
3601
|
+
const body = {
|
|
3602
|
+
url: args.url,
|
|
3603
|
+
events
|
|
3604
|
+
};
|
|
3605
|
+
if (Object.keys(scope).length > 0) body.scope = scope;
|
|
3606
|
+
const res = await apiRequest("/v1/env/webhooks/create", body);
|
|
3607
|
+
if (!res.ok) {
|
|
3608
|
+
const text = await res.text().catch(() => "");
|
|
3609
|
+
logError(`Failed to create webhook (${res.status}): ${text}`);
|
|
3610
|
+
}
|
|
3611
|
+
const data = await res.json();
|
|
3612
|
+
console.log(`Created webhook ${data.id}`);
|
|
3613
|
+
}
|
|
3614
|
+
});
|
|
3615
|
+
|
|
3616
|
+
//#endregion
|
|
3617
|
+
//#region src/commands/webhook-list.ts
|
|
3618
|
+
const webhookListCommand = defineCommand({
|
|
3619
|
+
meta: {
|
|
3620
|
+
name: "list",
|
|
3621
|
+
description: "List registered webhooks"
|
|
3622
|
+
},
|
|
3623
|
+
async run() {
|
|
3624
|
+
const res = await apiRequest("/v1/env/webhooks/list", {});
|
|
3625
|
+
if (!res.ok) {
|
|
3626
|
+
const text = await res.text().catch(() => "");
|
|
3627
|
+
logError(`Failed to list webhooks (${res.status}): ${text}`);
|
|
3628
|
+
}
|
|
3629
|
+
const data = await res.json();
|
|
3630
|
+
if (data.webhooks.length === 0) {
|
|
3631
|
+
console.log("No webhooks found.");
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
const col = (vals, min) => Math.max(min, ...vals.map((v) => v.length));
|
|
3635
|
+
const idW = 8;
|
|
3636
|
+
const urlW = col(data.webhooks.map((w) => w.url), 3);
|
|
3637
|
+
const evtW = col(data.webhooks.map((w) => w.events.join(",")), 6);
|
|
3638
|
+
const enW = 7;
|
|
3639
|
+
const header = [
|
|
3640
|
+
"ID".padEnd(idW),
|
|
3641
|
+
"URL".padEnd(urlW),
|
|
3642
|
+
"EVENTS".padEnd(evtW),
|
|
3643
|
+
"ENABLED".padEnd(enW)
|
|
3644
|
+
].join(" ");
|
|
3645
|
+
console.log(header);
|
|
3646
|
+
console.log("-".repeat(header.length));
|
|
3647
|
+
for (const w of data.webhooks) {
|
|
3648
|
+
const row = [
|
|
3649
|
+
w.id.slice(0, idW).padEnd(idW),
|
|
3650
|
+
w.url.padEnd(urlW),
|
|
3651
|
+
w.events.join(",").padEnd(evtW),
|
|
3652
|
+
String(w.enabled).padEnd(enW)
|
|
3653
|
+
].join(" ");
|
|
3654
|
+
console.log(row);
|
|
3655
|
+
}
|
|
3656
|
+
console.log(`\n${data.webhooks.length} webhooks`);
|
|
3657
|
+
}
|
|
3658
|
+
});
|
|
3659
|
+
|
|
3660
|
+
//#endregion
|
|
3661
|
+
//#region src/commands/webhook-remove.ts
|
|
3662
|
+
const webhookRemoveCommand = defineCommand({
|
|
3663
|
+
meta: {
|
|
3664
|
+
name: "remove",
|
|
3665
|
+
description: "Remove a webhook by ID"
|
|
3666
|
+
},
|
|
3667
|
+
args: { id: {
|
|
3668
|
+
type: "positional",
|
|
3669
|
+
description: "Webhook ID (from `auix env webhook list`)",
|
|
3670
|
+
required: true
|
|
3671
|
+
} },
|
|
3672
|
+
async run({ args }) {
|
|
3673
|
+
const confirmed = await consola.prompt(`Remove webhook ${args.id}?`, { type: "confirm" });
|
|
3674
|
+
if (!confirmed || typeof confirmed === "symbol") {
|
|
3675
|
+
console.log("Cancelled.");
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
const res = await apiRequest("/v1/env/webhooks/delete", { id: args.id });
|
|
3679
|
+
if (!res.ok) {
|
|
3680
|
+
const text = await res.text().catch(() => "");
|
|
3681
|
+
logError(`Failed to remove webhook (${res.status}): ${text}`);
|
|
3682
|
+
}
|
|
3683
|
+
console.log("Removed.");
|
|
3684
|
+
}
|
|
3685
|
+
});
|
|
3686
|
+
|
|
3687
|
+
//#endregion
|
|
3688
|
+
//#region src/commands/whoami.ts
|
|
3689
|
+
const whoamiCommand = defineCommand({
|
|
3690
|
+
meta: {
|
|
3691
|
+
name: "whoami",
|
|
3692
|
+
description: "Show current authentication"
|
|
3693
|
+
},
|
|
3694
|
+
async run() {
|
|
3695
|
+
const config = loadGlobalConfig();
|
|
3696
|
+
if (!config.apiKey) logError("Not logged in. Run `auix login`.");
|
|
3697
|
+
if (config.user) console.log(`User: ${config.user.name} (${config.user.email})`);
|
|
3698
|
+
if (config.organization) console.log(`Org: ${config.organization.name} (${config.organization.slug})`);
|
|
3699
|
+
console.log(`Key: ${config.apiKey.slice(0, 12)}...`);
|
|
3700
|
+
console.log(`API: ${config.baseUrl ?? "https://api.auix.dev"}`);
|
|
3701
|
+
console.log(`App: ${config.appUrl ?? "https://auix.dev"}`);
|
|
1821
3702
|
}
|
|
1822
3703
|
});
|
|
1823
3704
|
|
|
@@ -1826,22 +3707,110 @@ const setCommand = defineCommand({
|
|
|
1826
3707
|
runMain(defineCommand({
|
|
1827
3708
|
meta: {
|
|
1828
3709
|
name: "auix",
|
|
1829
|
-
version: "0.0.
|
|
3710
|
+
version: "0.0.4",
|
|
1830
3711
|
description: "AUIX CLI"
|
|
1831
3712
|
},
|
|
1832
3713
|
subCommands: {
|
|
1833
3714
|
login: loginCommand,
|
|
3715
|
+
logout: logoutCommand,
|
|
3716
|
+
whoami: whoamiCommand,
|
|
3717
|
+
switch: switchCommand,
|
|
1834
3718
|
env: defineCommand({
|
|
1835
3719
|
meta: {
|
|
1836
3720
|
name: "env",
|
|
1837
3721
|
description: "Manage environment variables"
|
|
1838
3722
|
},
|
|
1839
3723
|
subCommands: {
|
|
3724
|
+
init: initCommand,
|
|
3725
|
+
run: runCommand,
|
|
1840
3726
|
pull: pullCommand,
|
|
1841
3727
|
push: pushCommand,
|
|
1842
3728
|
list: listCommand,
|
|
1843
3729
|
set: setCommand,
|
|
1844
|
-
|
|
3730
|
+
delete: deleteCommand,
|
|
3731
|
+
diff: diffCommand,
|
|
3732
|
+
history: historyCommand,
|
|
3733
|
+
rollback: rollbackCommand,
|
|
3734
|
+
lock: lockCommand,
|
|
3735
|
+
unlock: unlockCommand,
|
|
3736
|
+
"lock-status": lockStatusCommand,
|
|
3737
|
+
branch: defineCommand({
|
|
3738
|
+
meta: {
|
|
3739
|
+
name: "branch",
|
|
3740
|
+
description: "Manage env branches"
|
|
3741
|
+
},
|
|
3742
|
+
subCommands: {
|
|
3743
|
+
create: branchCreateCommand,
|
|
3744
|
+
list: branchListCommand,
|
|
3745
|
+
delete: branchDeleteCommand
|
|
3746
|
+
}
|
|
3747
|
+
}),
|
|
3748
|
+
token: defineCommand({
|
|
3749
|
+
meta: {
|
|
3750
|
+
name: "token",
|
|
3751
|
+
description: "Manage service tokens"
|
|
3752
|
+
},
|
|
3753
|
+
subCommands: {
|
|
3754
|
+
create: tokenCreateCommand,
|
|
3755
|
+
list: tokenListCommand,
|
|
3756
|
+
revoke: tokenRevokeCommand
|
|
3757
|
+
}
|
|
3758
|
+
}),
|
|
3759
|
+
webhook: defineCommand({
|
|
3760
|
+
meta: {
|
|
3761
|
+
name: "webhook",
|
|
3762
|
+
description: "Manage env change webhooks"
|
|
3763
|
+
},
|
|
3764
|
+
subCommands: {
|
|
3765
|
+
add: webhookAddCommand,
|
|
3766
|
+
list: webhookListCommand,
|
|
3767
|
+
remove: webhookRemoveCommand
|
|
3768
|
+
}
|
|
3769
|
+
}),
|
|
3770
|
+
share: defineCommand({
|
|
3771
|
+
meta: {
|
|
3772
|
+
name: "share",
|
|
3773
|
+
description: "One-time encrypted share links"
|
|
3774
|
+
},
|
|
3775
|
+
subCommands: {
|
|
3776
|
+
create: shareCreateCommand,
|
|
3777
|
+
access: shareAccessCommand
|
|
3778
|
+
}
|
|
3779
|
+
}),
|
|
3780
|
+
sync: defineCommand({
|
|
3781
|
+
meta: {
|
|
3782
|
+
name: "sync",
|
|
3783
|
+
description: "Sync env vars to external providers"
|
|
3784
|
+
},
|
|
3785
|
+
subCommands: {
|
|
3786
|
+
add: syncAddCommand,
|
|
3787
|
+
list: syncListCommand,
|
|
3788
|
+
push: syncPushCommand,
|
|
3789
|
+
remove: syncRemoveCommand
|
|
3790
|
+
}
|
|
3791
|
+
}),
|
|
3792
|
+
rotation: defineCommand({
|
|
3793
|
+
meta: {
|
|
3794
|
+
name: "rotation",
|
|
3795
|
+
description: "Manage credential rotation policies"
|
|
3796
|
+
},
|
|
3797
|
+
subCommands: {
|
|
3798
|
+
create: rotationCreateCommand,
|
|
3799
|
+
list: rotationListCommand,
|
|
3800
|
+
rotate: rotationRotateCommand
|
|
3801
|
+
}
|
|
3802
|
+
}),
|
|
3803
|
+
dynamic: defineCommand({
|
|
3804
|
+
meta: {
|
|
3805
|
+
name: "dynamic",
|
|
3806
|
+
description: "Manage dynamic (ephemeral) secrets"
|
|
3807
|
+
},
|
|
3808
|
+
subCommands: {
|
|
3809
|
+
create: dynamicCreateCommand,
|
|
3810
|
+
list: dynamicListCommand,
|
|
3811
|
+
lease: dynamicLeaseCommand
|
|
3812
|
+
}
|
|
3813
|
+
})
|
|
1845
3814
|
}
|
|
1846
3815
|
})
|
|
1847
3816
|
}
|