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.
Files changed (2) hide show
  1. package/dist/index.js +2111 -142
  2. 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 { createInterface } from "node:readline/promises";
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://app.auix.dev";
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
- throw new Error("No API key found. Run `auix login` or set AUIX_API_KEY.");
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 ${getApiKey()}`
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/list.ts
1463
- const listCommand = defineCommand({
1689
+ //#region src/commands/dynamic-create.ts
1690
+ const dynamicCreateCommand = defineCommand({
1464
1691
  meta: {
1465
- name: "list",
1466
- description: "List environment variables (values masked)"
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/resolve", {
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 resolve env (${res.status}): ${text}`);
1858
+ logError(`Failed to get history (${res.status}): ${text}`);
1500
1859
  }
1501
1860
  const data = await res.json();
1502
- const entries = Object.entries(data.variables);
1503
- if (entries.length === 0) {
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 (const [key, value] of entries) {
1509
- const display = showValues ? value : maskValue(value);
1510
- console.log(`${key}=${display}`);
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${entries.length} variables`);
1873
+ console.log(`\n${data.versions.length} versions`);
1513
1874
  }
1514
1875
  });
1515
1876
 
1516
1877
  //#endregion
1517
- //#region src/commands/login.ts
1518
- const CLIENT_ID = "auix-cli";
1519
- const POLL_INTERVAL_MS = 5e3;
1520
- function openBrowser(url) {
1521
- try {
1522
- execSync(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} ${JSON.stringify(url)}`, { stdio: "ignore" });
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
- async function deviceFlow(appUrl) {
1526
- const base = appUrl.replace(/\/$/, "");
1527
- const codeRes = await fetch(`${base}/api/auth/device/code`, {
1528
- method: "POST",
1529
- headers: { "Content-Type": "application/json" },
1530
- body: JSON.stringify({ client_id: CLIENT_ID })
1531
- });
1532
- if (!codeRes.ok) {
1533
- const text = await codeRes.text().catch(() => "");
1534
- logError(`Failed to start device flow (${codeRes.status}): ${text}`);
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
- const codeData = await codeRes.json();
1537
- console.log("Opening browser for authentication...");
1538
- console.log(`If the browser doesn't open, visit: ${codeData.verification_uri_complete}`);
1539
- console.log(`Enter code: ${codeData.user_code}\n`);
1540
- openBrowser(codeData.verification_uri_complete);
1541
- const deadline = Date.now() + codeData.expires_in * 1e3;
1542
- const interval = Math.max((codeData.interval || 5) * 1e3, POLL_INTERVAL_MS);
1543
- while (Date.now() < deadline) {
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 (tokenRes.ok) return (await tokenRes.json()).access_token;
1555
- const err = await tokenRes.json().catch(() => ({}));
1556
- if (err.error === "authorization_pending") continue;
1557
- if (err.error === "slow_down") continue;
1558
- if (err.error === "expired_token") logError("Device code expired.");
1559
- if (err.error === "access_denied") logError("Access denied.");
1560
- logError(`Unexpected error: ${err.error ?? tokenRes.status}`);
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
- logError("Device code expired. Please try again.");
1563
- return "";
2039
+ return false;
1564
2040
  }
1565
- async function selectOrganization(appUrl, token) {
1566
- const base = appUrl.replace(/\/$/, "");
1567
- const res = await fetch(`${base}/api/auth/organization/list`, { headers: { Authorization: `Bearer ${token}` } });
1568
- if (!res.ok) logError(`Failed to list organizations (${res.status}).`);
1569
- const orgs = await res.json();
1570
- if (orgs.length === 0) logError("No organizations found. Create one at the web dashboard.");
1571
- if (orgs.length === 1) {
1572
- console.log(`Organization: ${orgs[0].name} (${orgs[0].slug})`);
1573
- return orgs[0];
1574
- }
1575
- console.log("Select organization:");
1576
- for (let i = 0; i < orgs.length; i++) console.log(` ${i + 1}) ${orgs[i].name} (${orgs[i].slug})`);
1577
- const rl = createInterface({
1578
- input: process.stdin,
1579
- output: process.stdout
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
- const answer = await rl.question("Choice: ");
1582
- rl.close();
1583
- const idx = Number.parseInt(answer, 10) - 1;
1584
- if (Number.isNaN(idx) || idx < 0 || idx >= orgs.length) logError("Invalid selection.");
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
- async function createApiKey(appUrl, token, organizationId) {
1588
- const base = appUrl.replace(/\/$/, "");
1589
- const res = await fetch(`${base}/api/cli/create-key`, {
1590
- method: "POST",
1591
- headers: {
1592
- "Content-Type": "application/json",
1593
- Authorization: `Bearer ${token}`
1594
- },
1595
- body: JSON.stringify({ organizationId })
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
- console.log("Authenticated.\n");
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
- const data = await res.json();
1697
- const count = Object.keys(data.variables).length;
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
- const content = formatEnvString(data.variables);
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
- content,
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/set.ts
1769
- const setCommand = defineCommand({
2553
+ //#region src/commands/rollback.ts
2554
+ const rollbackCommand = defineCommand({
1770
2555
  meta: {
1771
- name: "set",
1772
- description: "Set a single environment variable"
2556
+ name: "rollback",
2557
+ description: "Rollback a variable to a previous version"
1773
2558
  },
1774
2559
  args: {
1775
- pair: {
2560
+ key: {
1776
2561
  type: "positional",
1777
- description: "KEY=VALUE pair",
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
- secret: {
1794
- type: "boolean",
1795
- description: "Mark as secret",
1796
- default: true
2582
+ branch: {
2583
+ type: "string",
2584
+ description: "Env branch name"
1797
2585
  }
1798
2586
  },
1799
2587
  async run({ args }) {
1800
- const eqIndex = args.pair.indexOf("=");
1801
- if (eqIndex === -1) logError("Expected KEY=VALUE format.");
1802
- const key = args.pair.slice(0, eqIndex);
1803
- const value = args.pair.slice(eqIndex + 1);
1804
- if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) logError("Invalid key. Use uppercase letters, digits, and underscores.");
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/set", {
1809
- key,
1810
- value,
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
- isSecret: args.secret
2604
+ branch: args.branch
1815
2605
  });
1816
2606
  if (!res.ok) {
1817
2607
  const text = await res.text().catch(() => "");
1818
- logError(`Failed to set variable (${res.status}): ${text}`);
2608
+ logError(`Failed to rollback (${res.status}): ${text}`);
1819
2609
  }
1820
- console.log(`Set ${key}`);
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.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
- diff: diffCommand
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
  }