veylix-cli 1.2.1 → 1.3.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.
Files changed (3) hide show
  1. package/README.md +5 -2
  2. package/package.json +1 -1
  3. package/veylix.mjs +61 -5
package/README.md CHANGED
@@ -7,6 +7,8 @@
7
7
 
8
8
  The official command-line tool for **[Veylix](https://veylix-omega.vercel.app)**. It pulls your environment variables into local development, shell sessions and CI/CD pipelines, so you don't have to copy `.env` files around.
9
9
 
10
+ 📚 **Full documentation: [veylix-omega.vercel.app/docs](https://veylix-omega.vercel.app/docs)**
11
+
10
12
  ```bash
11
13
  npm install -g veylix-cli
12
14
  veylix login --token vlx_…
@@ -91,7 +93,7 @@ veylix pull > .env # or write a .env file
91
93
  | `veylix envs` | List the environments of the current project |
92
94
  | `veylix pull [--format dotenv\|json\|shell] [--output <file>]` | Print or save variables |
93
95
  | `veylix run -- <command> [args…]` | Run a command with the variables injected |
94
- | `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 |
95
97
  | `veylix set KEY=value [KEY2=value…]` | Create or update variables |
96
98
  | `veylix unset KEY` | Delete a variable |
97
99
  | `veylix help` / `veylix --version` | Show help or the version |
@@ -114,7 +116,8 @@ eval "$(veylix pull --format shell)"
114
116
  veylix pull --output .env.local
115
117
 
116
118
  # Sync a local .env up to Veylix; --replace also deletes keys missing from the file
117
- veylix push --file .env.production --env production --replace
119
+ veylix push --file .env.production --env production --replace --dry-run # preview only
120
+ veylix push --file .env.production --env production --replace # asks before deleting
118
121
 
119
122
  # Quick edits
120
123
  veylix set STRIPE_KEY=sk_live_… FEATURE_FLAG=true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veylix-cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Veylix CLI: pull, inject and sync environment variables from Veylix into your apps, shells and CI pipelines. Zero dependencies.",
5
5
  "keywords": [
6
6
  "veylix",
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.2.1";
11
+ const VERSION = "1.3.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,8 +27,10 @@ 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
34
36
 
@@ -45,7 +47,7 @@ Examples:
45
47
  veylix pull --project api --env production --format json
46
48
  VEYLIX_TOKEN=vlx_… VEYLIX_PROJECT=api VEYLIX_ENV=production veylix run -- node server.js # CI
47
49
 
48
- Create a token at ${DEFAULT_URL}/settings`;
50
+ Create a token at ${DEFAULT_URL}/settings · Docs: ${DEFAULT_URL}/docs`;
49
51
 
50
52
  // ---------- args ----------
51
53
 
@@ -65,6 +67,7 @@ function parseArgs(argv) {
65
67
  else if (argv[i + 1] && !argv[i + 1].startsWith("--")) flags[key] = argv[++i];
66
68
  else flags[key] = true;
67
69
  } else if (a === "-h") flags.help = true;
70
+ else if (a === "-y") flags.yes = true;
68
71
  else positional.push(a);
69
72
  }
70
73
  return { command: positional[0], args: positional.slice(1), flags, rest };
@@ -213,6 +216,41 @@ function fail(msg) {
213
216
  process.exit(1);
214
217
  }
215
218
 
219
+ // ---------- push preview ----------
220
+
221
+ /** What a push would do: keys added, changed, deleted (only with replace) and left unchanged. */
222
+ function diffSecrets(current, incoming, replace) {
223
+ const plan = { added: [], changed: [], deleted: [], unchanged: 0 };
224
+ for (const [key, value] of Object.entries(incoming)) {
225
+ if (!(key in current)) plan.added.push(key);
226
+ else if (current[key] !== value) plan.changed.push(key);
227
+ else plan.unchanged++;
228
+ }
229
+ if (replace) plan.deleted = Object.keys(current).filter((key) => !(key in incoming));
230
+ return plan;
231
+ }
232
+
233
+ function printPlan(plan) {
234
+ const width = Math.max(0, ...[...plan.added, ...plan.changed, ...plan.deleted].map((k) => k.length));
235
+ for (const key of plan.added) info(` ${c(32, "+")} ${key.padEnd(width)} ${c(2, "new")}`);
236
+ for (const key of plan.changed) info(` ${c(33, "~")} ${key.padEnd(width)} ${c(2, "changed")}`);
237
+ for (const key of plan.deleted) info(` ${c(31, "-")} ${key.padEnd(width)} ${c(31, "will be DELETED")}`);
238
+ if (plan.unchanged) info(c(2, ` ${plan.unchanged} unchanged`));
239
+ }
240
+
241
+ /** Yes/no question on stderr; anything but y/yes counts as no. */
242
+ async function confirm(question) {
243
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
244
+ try {
245
+ return /^y(es)?$/i.test((await rl.question(`${question} [y/N] `)).trim());
246
+ } catch (e) {
247
+ if (e.name === "AbortError") return false;
248
+ throw e;
249
+ } finally {
250
+ rl.close();
251
+ }
252
+ }
253
+
216
254
  // ---------- commands ----------
217
255
 
218
256
  const commands = {
@@ -328,7 +366,25 @@ const commands = {
328
366
  if (!existsSync(file)) fail(`File not found: ${file}`);
329
367
  const secrets = parseDotenv(readFileSync(file, "utf8"));
330
368
  if (!Object.keys(secrets).length) fail(`No KEY=value lines found in ${file}`);
331
- const res = JSON.parse(await api(flags, "/api/v1/secrets", { method: "PUT", body: JSON.stringify({ ...target(flags), secrets, replace: !!flags.replace }) }));
369
+ const replace = !!flags.replace;
370
+
371
+ // Preview what will change before writing anything (key names only, never values).
372
+ const current = await fetchSecrets(flags);
373
+ const plan = diffSecrets(current.secrets, secrets, replace);
374
+ info(`${c(1, where(current))} ${c(2, `(from ${file})`)}`);
375
+ printPlan(plan);
376
+ if (!plan.added.length && !plan.changed.length && !plan.deleted.length) return success("Already up to date. Nothing to push.");
377
+ if (flags["dry-run"]) return info(c(2, "Dry run: nothing was changed."));
378
+
379
+ if (plan.deleted.length && !flags.yes) {
380
+ const n = plan.deleted.length;
381
+ const warning = `${n} key${n === 1 ? "" : "s"} will be permanently deleted.`;
382
+ if (!process.stdin.isTTY) fail(`${warning} Re-run with --yes to confirm (or --dry-run to preview).`);
383
+ info(c(33, `⚠ ${warning}`));
384
+ if (!(await confirm("Continue?"))) fail("Cancelled. Nothing was changed.");
385
+ }
386
+
387
+ const res = JSON.parse(await api(flags, "/api/v1/secrets", { method: "PUT", body: JSON.stringify({ ...target(flags), secrets, replace }) }));
332
388
  success(`${where(res)}: ${res.created} created, ${res.updated} updated${res.deleted ? `, ${res.deleted} deleted` : ""}`);
333
389
  },
334
390