auix 0.0.1 → 0.0.3

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