veylix-cli 1.2.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/package.json +1 -1
- package/veylix.mjs +92 -4
package/README.md
CHANGED
|
@@ -93,9 +93,10 @@ veylix pull > .env # or write a .env file
|
|
|
93
93
|
| `veylix envs` | List the environments of the current project |
|
|
94
94
|
| `veylix pull [--format dotenv\|json\|shell] [--output <file>]` | Print or save variables |
|
|
95
95
|
| `veylix run -- <command> [args…]` | Run a command with the variables injected |
|
|
96
|
-
| `veylix push [--file .env] [--replace]` | Upload a `.env` file (needs a read-write token) |
|
|
96
|
+
| `veylix push [--file .env] [--replace] [--dry-run] [--yes]` | Upload a `.env` file (needs a read-write token). Shows the changes first and asks before deleting anything |
|
|
97
97
|
| `veylix set KEY=value [KEY2=value…]` | Create or update variables |
|
|
98
98
|
| `veylix unset KEY` | Delete a variable |
|
|
99
|
+
| `veylix diff <env> <other-env> [--all] [--fail-on-missing]` | Compare two environments: keys missing on either side (names only) |
|
|
99
100
|
| `veylix help` / `veylix --version` | Show help or the version |
|
|
100
101
|
|
|
101
102
|
Every command accepts `--project <name|id>` and `--env <name>` to override the folder link.
|
|
@@ -116,7 +117,11 @@ eval "$(veylix pull --format shell)"
|
|
|
116
117
|
veylix pull --output .env.local
|
|
117
118
|
|
|
118
119
|
# Sync a local .env up to Veylix; --replace also deletes keys missing from the file
|
|
119
|
-
veylix push --file .env.production --env production --replace
|
|
120
|
+
veylix push --file .env.production --env production --replace --dry-run # preview only
|
|
121
|
+
veylix push --file .env.production --env production --replace # asks before deleting
|
|
122
|
+
|
|
123
|
+
# Find keys missing from production (exit 1 in CI if any)
|
|
124
|
+
veylix diff development production --fail-on-missing
|
|
120
125
|
|
|
121
126
|
# Quick edits
|
|
122
127
|
veylix set STRIPE_KEY=sk_live_… FEATURE_FLAG=true
|
package/package.json
CHANGED
package/veylix.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { homedir } from "node:os";
|
|
|
8
8
|
import { dirname, join, relative } from "node:path";
|
|
9
9
|
import { createInterface } from "node:readline/promises";
|
|
10
10
|
|
|
11
|
-
const VERSION = "1.
|
|
11
|
+
const VERSION = "1.4.0";
|
|
12
12
|
const DEFAULT_URL = "https://veylix-omega.vercel.app";
|
|
13
13
|
const CONFIG_PATH = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "veylix", "config.json");
|
|
14
14
|
// Per-folder project/environment choice (no secrets, safe to commit).
|
|
@@ -27,10 +27,14 @@ Usage:
|
|
|
27
27
|
veylix envs List environments of the project with variable counts
|
|
28
28
|
veylix pull [--format dotenv|json|shell] [--output <file>]
|
|
29
29
|
veylix run -- <command> [args…] Run a command with the variables injected
|
|
30
|
-
veylix push [--file .env] [--replace]
|
|
31
|
-
Upload a .env file (read-write token)
|
|
30
|
+
veylix push [--file .env] [--replace] [--dry-run] [--yes]
|
|
31
|
+
Upload a .env file (read-write token). Shows what will
|
|
32
|
+
change first; --replace also deletes keys missing from
|
|
33
|
+
the file and asks before deleting (--yes skips the question)
|
|
32
34
|
veylix set KEY=value [KEY2=value…]
|
|
33
35
|
veylix unset KEY
|
|
36
|
+
veylix diff <env> <other-env> [--all] [--fail-on-missing]
|
|
37
|
+
Compare two environments (key names only, never values)
|
|
34
38
|
|
|
35
39
|
Every command accepts --project <name> and --env <name> to override the link.
|
|
36
40
|
|
|
@@ -65,6 +69,7 @@ function parseArgs(argv) {
|
|
|
65
69
|
else if (argv[i + 1] && !argv[i + 1].startsWith("--")) flags[key] = argv[++i];
|
|
66
70
|
else flags[key] = true;
|
|
67
71
|
} else if (a === "-h") flags.help = true;
|
|
72
|
+
else if (a === "-y") flags.yes = true;
|
|
68
73
|
else positional.push(a);
|
|
69
74
|
}
|
|
70
75
|
return { command: positional[0], args: positional.slice(1), flags, rest };
|
|
@@ -213,6 +218,41 @@ function fail(msg) {
|
|
|
213
218
|
process.exit(1);
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
// ---------- push preview ----------
|
|
222
|
+
|
|
223
|
+
/** What a push would do: keys added, changed, deleted (only with replace) and left unchanged. */
|
|
224
|
+
function diffSecrets(current, incoming, replace) {
|
|
225
|
+
const plan = { added: [], changed: [], deleted: [], unchanged: 0 };
|
|
226
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
227
|
+
if (!(key in current)) plan.added.push(key);
|
|
228
|
+
else if (current[key] !== value) plan.changed.push(key);
|
|
229
|
+
else plan.unchanged++;
|
|
230
|
+
}
|
|
231
|
+
if (replace) plan.deleted = Object.keys(current).filter((key) => !(key in incoming));
|
|
232
|
+
return plan;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function printPlan(plan) {
|
|
236
|
+
const width = Math.max(0, ...[...plan.added, ...plan.changed, ...plan.deleted].map((k) => k.length));
|
|
237
|
+
for (const key of plan.added) info(` ${c(32, "+")} ${key.padEnd(width)} ${c(2, "new")}`);
|
|
238
|
+
for (const key of plan.changed) info(` ${c(33, "~")} ${key.padEnd(width)} ${c(2, "changed")}`);
|
|
239
|
+
for (const key of plan.deleted) info(` ${c(31, "-")} ${key.padEnd(width)} ${c(31, "will be DELETED")}`);
|
|
240
|
+
if (plan.unchanged) info(c(2, ` ${plan.unchanged} unchanged`));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Yes/no question on stderr; anything but y/yes counts as no. */
|
|
244
|
+
async function confirm(question) {
|
|
245
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
246
|
+
try {
|
|
247
|
+
return /^y(es)?$/i.test((await rl.question(`${question} [y/N] `)).trim());
|
|
248
|
+
} catch (e) {
|
|
249
|
+
if (e.name === "AbortError") return false;
|
|
250
|
+
throw e;
|
|
251
|
+
} finally {
|
|
252
|
+
rl.close();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
216
256
|
// ---------- commands ----------
|
|
217
257
|
|
|
218
258
|
const commands = {
|
|
@@ -328,10 +368,58 @@ const commands = {
|
|
|
328
368
|
if (!existsSync(file)) fail(`File not found: ${file}`);
|
|
329
369
|
const secrets = parseDotenv(readFileSync(file, "utf8"));
|
|
330
370
|
if (!Object.keys(secrets).length) fail(`No KEY=value lines found in ${file}`);
|
|
331
|
-
const
|
|
371
|
+
const replace = !!flags.replace;
|
|
372
|
+
|
|
373
|
+
// Preview what will change before writing anything (key names only, never values).
|
|
374
|
+
const current = await fetchSecrets(flags);
|
|
375
|
+
const plan = diffSecrets(current.secrets, secrets, replace);
|
|
376
|
+
info(`${c(1, where(current))} ${c(2, `(from ${file})`)}`);
|
|
377
|
+
printPlan(plan);
|
|
378
|
+
if (!plan.added.length && !plan.changed.length && !plan.deleted.length) return success("Already up to date. Nothing to push.");
|
|
379
|
+
if (flags["dry-run"]) return info(c(2, "Dry run: nothing was changed."));
|
|
380
|
+
|
|
381
|
+
if (plan.deleted.length && !flags.yes) {
|
|
382
|
+
const n = plan.deleted.length;
|
|
383
|
+
const warning = `${n} key${n === 1 ? "" : "s"} will be permanently deleted.`;
|
|
384
|
+
if (!process.stdin.isTTY) fail(`${warning} Re-run with --yes to confirm (or --dry-run to preview).`);
|
|
385
|
+
info(c(33, `⚠ ${warning}`));
|
|
386
|
+
if (!(await confirm("Continue?"))) fail("Cancelled. Nothing was changed.");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const res = JSON.parse(await api(flags, "/api/v1/secrets", { method: "PUT", body: JSON.stringify({ ...target(flags), secrets, replace }) }));
|
|
332
390
|
success(`${where(res)}: ${res.created} created, ${res.updated} updated${res.deleted ? `, ${res.deleted} deleted` : ""}`);
|
|
333
391
|
},
|
|
334
392
|
|
|
393
|
+
async diff(args, flags) {
|
|
394
|
+
const linked = target(flags).env;
|
|
395
|
+
const [envA, envB] = args.length >= 2 ? args : [linked, args[0]];
|
|
396
|
+
if (!envA || !envB) fail("Usage: veylix diff <env> <other-env> (or `veylix diff <other-env>` in a linked folder)");
|
|
397
|
+
const [a, b] = await Promise.all([fetchSecrets({ ...flags, env: envA }), fetchSecrets({ ...flags, env: envB })]);
|
|
398
|
+
const keys = [...new Set([...Object.keys(a.secrets), ...Object.keys(b.secrets)])].sort();
|
|
399
|
+
const missing = [];
|
|
400
|
+
let different = 0;
|
|
401
|
+
let same = 0;
|
|
402
|
+
const width = Math.max(0, ...keys.map((k) => k.length));
|
|
403
|
+
info(`${c(1, `${a.project} › ${a.environment}`)} ↔ ${c(1, b.environment)}`);
|
|
404
|
+
for (const key of keys) {
|
|
405
|
+
if (!(key in b.secrets)) {
|
|
406
|
+
missing.push(key);
|
|
407
|
+
info(` ${c(31, "-")} ${key.padEnd(width)} ${c(31, `missing in ${b.environment}`)}`);
|
|
408
|
+
} else if (!(key in a.secrets)) {
|
|
409
|
+
missing.push(key);
|
|
410
|
+
info(` ${c(31, "-")} ${key.padEnd(width)} ${c(31, `missing in ${a.environment}`)}`);
|
|
411
|
+
} else if (a.secrets[key] !== b.secrets[key]) {
|
|
412
|
+
different++;
|
|
413
|
+
if (flags.all) info(` ${c(33, "~")} ${key.padEnd(width)} ${c(2, "different")}`);
|
|
414
|
+
} else {
|
|
415
|
+
same++;
|
|
416
|
+
if (flags.all) info(` ${c(2, "=")} ${key.padEnd(width)} ${c(2, "same value")}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
info(c(2, ` ${missing.length} missing · ${different} different · ${same} same${flags.all ? "" : " (--all lists every key)"}`));
|
|
420
|
+
if (missing.length && flags["fail-on-missing"]) process.exit(1);
|
|
421
|
+
},
|
|
422
|
+
|
|
335
423
|
async set(args, flags) {
|
|
336
424
|
const pairs = args.map((a) => a.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s));
|
|
337
425
|
if (!pairs.length || pairs.some((p) => !p)) fail("Usage: veylix set KEY=value [KEY2=value…]");
|