vaultsy-cli 0.1.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 +338 -0
  2. package/dist/index.js +1351 -0
  3. package/package.json +53 -0
package/dist/index.js ADDED
@@ -0,0 +1,1351 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/lib/config.ts
13
+ var config_exports = {};
14
+ __export(config_exports, {
15
+ clearConfig: () => clearConfig,
16
+ configExists: () => configExists,
17
+ readConfig: () => readConfig,
18
+ writeConfig: () => writeConfig
19
+ });
20
+ import { homedir } from "os";
21
+ import { join } from "path";
22
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
23
+ function configExists() {
24
+ return existsSync(CONFIG_FILE);
25
+ }
26
+ function readConfig() {
27
+ if (!existsSync(CONFIG_FILE)) {
28
+ throw new Error("Not logged in. Run `vaultsy login` first.");
29
+ }
30
+ try {
31
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
32
+ const parsed = JSON.parse(raw);
33
+ if (!parsed.token || !parsed.baseUrl) {
34
+ throw new Error("Config file is corrupted. Run `vaultsy login` to re-authenticate.");
35
+ }
36
+ return parsed;
37
+ } catch (err) {
38
+ if (err instanceof SyntaxError) {
39
+ throw new Error("Config file is corrupted. Run `vaultsy login` to re-authenticate.");
40
+ }
41
+ throw err;
42
+ }
43
+ }
44
+ function writeConfig(config) {
45
+ if (!existsSync(CONFIG_DIR)) {
46
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
47
+ }
48
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
49
+ encoding: "utf-8",
50
+ mode: 384
51
+ // owner read/write only — never group or world readable
52
+ });
53
+ }
54
+ function clearConfig() {
55
+ if (existsSync(CONFIG_FILE)) {
56
+ writeFileSync(CONFIG_FILE, "", { mode: 384 });
57
+ unlinkSync(CONFIG_FILE);
58
+ }
59
+ }
60
+ var CONFIG_DIR, CONFIG_FILE;
61
+ var init_config = __esm({
62
+ "src/lib/config.ts"() {
63
+ "use strict";
64
+ CONFIG_DIR = join(homedir(), ".vaultsy");
65
+ CONFIG_FILE = join(CONFIG_DIR, "config.json");
66
+ }
67
+ });
68
+
69
+ // src/lib/api.ts
70
+ var api_exports = {};
71
+ __export(api_exports, {
72
+ ApiError: () => ApiError,
73
+ getMe: () => getMe,
74
+ listProjects: () => listProjects,
75
+ listVersions: () => listVersions,
76
+ pullSecrets: () => pullSecrets,
77
+ pushSecrets: () => pushSecrets,
78
+ rollback: () => rollback
79
+ });
80
+ async function apiFetch(path, options = {}) {
81
+ const { baseUrl, token, ...fetchOptions } = options;
82
+ let resolvedBase = baseUrl;
83
+ let resolvedToken = token;
84
+ if (!resolvedBase || !resolvedToken) {
85
+ const cfg = readConfig();
86
+ resolvedBase ??= cfg.baseUrl;
87
+ resolvedToken ??= cfg.token;
88
+ }
89
+ const url = `${resolvedBase.replace(/\/$/, "")}${path}`;
90
+ const headers = {
91
+ "Content-Type": "application/json",
92
+ Authorization: `Bearer ${resolvedToken}`,
93
+ ...fetchOptions.headers
94
+ };
95
+ const res = await fetch(url, { ...fetchOptions, headers });
96
+ if (!res.ok) {
97
+ let message = `HTTP ${res.status}`;
98
+ try {
99
+ const body = await res.json();
100
+ if (body?.message) message = body.message;
101
+ } catch {
102
+ }
103
+ throw new ApiError(res.status, message);
104
+ }
105
+ return res.json();
106
+ }
107
+ async function getMe(opts) {
108
+ return apiFetch("/api/v1/me", opts);
109
+ }
110
+ async function listProjects() {
111
+ const res = await apiFetch("/api/v1/projects");
112
+ return res.projects;
113
+ }
114
+ async function pullSecrets(projectId, env) {
115
+ return apiFetch(`/api/v1/projects/${projectId}/envs/${env}`);
116
+ }
117
+ async function pushSecrets(projectId, env, secrets) {
118
+ return apiFetch(`/api/v1/projects/${projectId}/envs/${env}`, {
119
+ method: "POST",
120
+ body: JSON.stringify({ secrets })
121
+ });
122
+ }
123
+ async function listVersions(projectId, env) {
124
+ return apiFetch(`/api/v1/projects/${projectId}/envs/${env}/versions`);
125
+ }
126
+ async function rollback(projectId, env, versionId) {
127
+ return apiFetch(`/api/v1/projects/${projectId}/envs/${env}/rollback`, {
128
+ method: "POST",
129
+ body: JSON.stringify({ versionId })
130
+ });
131
+ }
132
+ var ApiError;
133
+ var init_api = __esm({
134
+ "src/lib/api.ts"() {
135
+ "use strict";
136
+ init_config();
137
+ ApiError = class extends Error {
138
+ constructor(status, message) {
139
+ super(message);
140
+ this.status = status;
141
+ this.name = "ApiError";
142
+ }
143
+ };
144
+ }
145
+ });
146
+
147
+ // src/lib/env.ts
148
+ var env_exports = {};
149
+ __export(env_exports, {
150
+ envFileName: () => envFileName,
151
+ findProjectConfig: () => findProjectConfig,
152
+ isGitIgnored: () => isGitIgnored,
153
+ parseEnvText: () => parseEnvText,
154
+ readEnvFile: () => readEnvFile,
155
+ serializeEnvText: () => serializeEnvText,
156
+ writeEnvFile: () => writeEnvFile,
157
+ writeProjectConfig: () => writeProjectConfig
158
+ });
159
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
160
+ import { join as join2, resolve } from "path";
161
+ function parseEnvText(text2) {
162
+ const lines = text2.split(/\r?\n/);
163
+ const result = [];
164
+ for (const rawLine of lines) {
165
+ let line = rawLine.trim();
166
+ if (!line) continue;
167
+ if (line.startsWith("#")) continue;
168
+ if (line.startsWith("export ")) {
169
+ line = line.replace(/^export\s+/, "");
170
+ }
171
+ const equalIndex = line.indexOf("=");
172
+ if (equalIndex === -1) continue;
173
+ const key = line.slice(0, equalIndex).trim();
174
+ let value = line.slice(equalIndex + 1).trim();
175
+ const isDoubleQuoted = value.startsWith('"') && value.endsWith('"');
176
+ const isSingleQuoted = value.startsWith("'") && value.endsWith("'");
177
+ if (!isDoubleQuoted && !isSingleQuoted) {
178
+ const commentIndex = value.indexOf(" #");
179
+ if (commentIndex !== -1) {
180
+ value = value.slice(0, commentIndex).trim();
181
+ }
182
+ }
183
+ if (isDoubleQuoted || isSingleQuoted) {
184
+ value = value.slice(1, -1);
185
+ }
186
+ if (key) {
187
+ result.push({ key, value });
188
+ }
189
+ }
190
+ return result;
191
+ }
192
+ function serializeEnvText(rows) {
193
+ const lines = rows.map(({ key, value }) => {
194
+ const needsQuoting = value === "" || /[\s#$"'\\`]/.test(value);
195
+ if (needsQuoting) {
196
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
197
+ return `${key}="${escaped}"`;
198
+ }
199
+ return `${key}=${value}`;
200
+ });
201
+ return lines.join("\n") + "\n";
202
+ }
203
+ function readEnvFile(filePath) {
204
+ if (!existsSync2(filePath)) return [];
205
+ const raw = readFileSync2(filePath, "utf-8");
206
+ return parseEnvText(raw);
207
+ }
208
+ function writeEnvFile(filePath, rows) {
209
+ writeFileSync2(filePath, serializeEnvText(rows), { encoding: "utf-8" });
210
+ }
211
+ function findProjectConfig(dir = process.cwd()) {
212
+ let current = resolve(dir);
213
+ while (true) {
214
+ const candidate = join2(current, VAULTSY_JSON);
215
+ if (existsSync2(candidate)) {
216
+ try {
217
+ const raw = readFileSync2(candidate, "utf-8");
218
+ const parsed = JSON.parse(raw);
219
+ if (parsed.project && typeof parsed.project === "string") {
220
+ return { config: parsed, dir: current };
221
+ }
222
+ } catch {
223
+ }
224
+ }
225
+ const parent = resolve(current, "..");
226
+ if (parent === current) break;
227
+ current = parent;
228
+ }
229
+ return null;
230
+ }
231
+ function writeProjectConfig(config, dir = process.cwd()) {
232
+ writeFileSync2(join2(dir, VAULTSY_JSON), JSON.stringify(config, null, 2) + "\n", {
233
+ encoding: "utf-8"
234
+ });
235
+ }
236
+ function isGitIgnored(filename, dir = process.cwd()) {
237
+ const gitignorePath = join2(dir, ".gitignore");
238
+ if (!existsSync2(gitignorePath)) return false;
239
+ const lines = readFileSync2(gitignorePath, "utf-8").split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
240
+ return lines.some((pattern) => {
241
+ if (pattern === filename) return true;
242
+ if (pattern.startsWith("*.") && filename.endsWith(pattern.slice(1))) return true;
243
+ if (pattern === ".env*" && filename.startsWith(".env")) return true;
244
+ if (pattern === ".env.*" && /^\.env\..+/.test(filename)) return true;
245
+ return false;
246
+ });
247
+ }
248
+ function envFileName(env) {
249
+ return env === "development" ? ".env" : `.env.${env}`;
250
+ }
251
+ var VAULTSY_JSON;
252
+ var init_env = __esm({
253
+ "src/lib/env.ts"() {
254
+ "use strict";
255
+ VAULTSY_JSON = "vaultsy.json";
256
+ }
257
+ });
258
+
259
+ // src/index.ts
260
+ import { Command } from "commander";
261
+
262
+ // ../shared/dist/enums.js
263
+ var EnvironmentType = ["development", "staging", "preview", "production"];
264
+
265
+ // src/commands/login.ts
266
+ init_config();
267
+ init_api();
268
+ init_api();
269
+ import * as p from "@clack/prompts";
270
+ import chalk from "chalk";
271
+ async function loginCommand(opts) {
272
+ p.intro(chalk.bold.cyan("vaultsy login"));
273
+ let baseUrl;
274
+ if (opts.baseUrl) {
275
+ baseUrl = opts.baseUrl;
276
+ } else if (configExists()) {
277
+ try {
278
+ baseUrl = readConfig().baseUrl;
279
+ } catch {
280
+ baseUrl = "https://vaultsy.app";
281
+ }
282
+ } else {
283
+ baseUrl = "https://vaultsy.app";
284
+ }
285
+ const resolvedBaseUrl = await p.text({
286
+ message: "Vaultsy base URL",
287
+ placeholder: "https://vaultsy.app",
288
+ initialValue: baseUrl,
289
+ validate(value) {
290
+ if (!value.trim()) return "Base URL is required.";
291
+ try {
292
+ new URL(value.trim());
293
+ } catch {
294
+ return "Enter a valid URL (e.g. https://vaultsy.app).";
295
+ }
296
+ }
297
+ });
298
+ if (p.isCancel(resolvedBaseUrl)) {
299
+ p.cancel("Login cancelled.");
300
+ process.exit(0);
301
+ }
302
+ let token;
303
+ if (opts.token) {
304
+ token = opts.token;
305
+ } else {
306
+ p.log.info(
307
+ `Create a token at ${chalk.cyan(resolvedBaseUrl.replace(/\/$/, "") + "/dashboard/settings")}`
308
+ );
309
+ const input = await p.password({
310
+ message: "Paste your API token",
311
+ validate(value) {
312
+ if (!value.trim()) return "Token is required.";
313
+ }
314
+ });
315
+ if (p.isCancel(input)) {
316
+ p.cancel("Login cancelled.");
317
+ process.exit(0);
318
+ }
319
+ token = input.trim();
320
+ }
321
+ const spinner7 = p.spinner();
322
+ spinner7.start("Verifying token\u2026");
323
+ let userName;
324
+ let userEmail;
325
+ try {
326
+ const me = await getMe({
327
+ baseUrl: resolvedBaseUrl.trim().replace(/\/$/, ""),
328
+ token
329
+ });
330
+ userName = me.name;
331
+ userEmail = me.email;
332
+ spinner7.stop("Token verified.");
333
+ } catch (err) {
334
+ spinner7.stop("Verification failed.");
335
+ if (err instanceof ApiError) {
336
+ if (err.status === 401) {
337
+ p.log.error("Invalid or expired token. Generate a new one and try again.");
338
+ } else {
339
+ p.log.error(`Server responded with ${err.status}: ${err.message}`);
340
+ }
341
+ } else {
342
+ p.log.error(
343
+ `Could not reach ${resolvedBaseUrl}. Check the URL and your network connection.`
344
+ );
345
+ }
346
+ process.exit(1);
347
+ }
348
+ writeConfig({
349
+ token,
350
+ baseUrl: resolvedBaseUrl.trim().replace(/\/$/, "")
351
+ });
352
+ p.outro(
353
+ `${chalk.green("\u2713")} Logged in as ${chalk.bold(userName)} ${chalk.dim(`<${userEmail}>`)}
354
+ Config saved to ${chalk.dim("~/.vaultsy/config.json")}`
355
+ );
356
+ }
357
+
358
+ // src/commands/pull.ts
359
+ import * as p2 from "@clack/prompts";
360
+ import chalk2 from "chalk";
361
+ import { resolve as resolve2 } from "path";
362
+ init_api();
363
+ init_env();
364
+ async function pullCommand(projectArg, envArg, opts) {
365
+ p2.intro(chalk2.bold.cyan("vaultsy pull"));
366
+ let projectId;
367
+ let projectTitle;
368
+ if (projectArg) {
369
+ projectId = projectArg;
370
+ } else {
371
+ const found = findProjectConfig();
372
+ if (found) {
373
+ projectId = found.config.project;
374
+ p2.log.info(`Using project ${chalk2.cyan(projectId)} from ${chalk2.dim("vaultsy.json")}`);
375
+ } else {
376
+ const spinner8 = p2.spinner();
377
+ spinner8.start("Fetching projects\u2026");
378
+ let projects;
379
+ try {
380
+ projects = await listProjects();
381
+ spinner8.stop(`Found ${projects.length} project${projects.length !== 1 ? "s" : ""}.`);
382
+ } catch (err) {
383
+ spinner8.stop("Failed to fetch projects.");
384
+ printApiError(err);
385
+ process.exit(1);
386
+ }
387
+ if (projects.length === 0) {
388
+ p2.log.error("No projects found. Create one at your Vaultsy dashboard first.");
389
+ process.exit(1);
390
+ }
391
+ const selected = await p2.select({
392
+ message: "Select a project",
393
+ options: projects.map((proj) => ({
394
+ value: proj.id,
395
+ label: proj.title,
396
+ hint: proj.id
397
+ }))
398
+ });
399
+ if (p2.isCancel(selected)) {
400
+ p2.cancel("Pull cancelled.");
401
+ process.exit(0);
402
+ }
403
+ projectId = selected;
404
+ projectTitle = projects.find((proj) => proj.id === selected)?.title;
405
+ }
406
+ }
407
+ let env;
408
+ if (envArg) {
409
+ if (!EnvironmentType.includes(envArg)) {
410
+ p2.log.error(
411
+ `Invalid environment "${envArg}". Must be one of: ${EnvironmentType.join(", ")}.`
412
+ );
413
+ process.exit(1);
414
+ }
415
+ env = envArg;
416
+ } else {
417
+ const found = findProjectConfig();
418
+ const defaultEnv = found?.config.defaultEnv;
419
+ const selected = await p2.select({
420
+ message: "Select an environment",
421
+ options: EnvironmentType.map((e) => ({
422
+ value: e,
423
+ label: e,
424
+ hint: e === defaultEnv ? "default" : void 0
425
+ })),
426
+ initialValue: defaultEnv ?? "development"
427
+ });
428
+ if (p2.isCancel(selected)) {
429
+ p2.cancel("Pull cancelled.");
430
+ process.exit(0);
431
+ }
432
+ env = selected;
433
+ }
434
+ const filename = opts.output ?? envFileName(env);
435
+ const outputPath = resolve2(process.cwd(), filename);
436
+ if (!opts.yes && !isGitIgnored(filename)) {
437
+ p2.log.warn(
438
+ `${chalk2.yellow(filename)} does not appear to be in ${chalk2.dim(".gitignore")}.
439
+ Make sure you don't accidentally commit secrets to version control.`
440
+ );
441
+ const confirmed = await p2.confirm({
442
+ message: "Continue anyway?",
443
+ initialValue: false
444
+ });
445
+ if (p2.isCancel(confirmed) || !confirmed) {
446
+ p2.cancel("Pull cancelled.");
447
+ process.exit(0);
448
+ }
449
+ }
450
+ const spinner7 = p2.spinner();
451
+ spinner7.start(`Pulling ${chalk2.cyan(env)} secrets\u2026`);
452
+ let result;
453
+ try {
454
+ result = await pullSecrets(projectId, env);
455
+ spinner7.stop(
456
+ `Pulled ${result.secrets.length} secret${result.secrets.length !== 1 ? "s" : ""} from ${chalk2.bold(projectTitle ?? result.project.title)} / ${chalk2.cyan(env)}.`
457
+ );
458
+ } catch (err) {
459
+ spinner7.stop("Pull failed.");
460
+ printApiError(err);
461
+ process.exit(1);
462
+ }
463
+ if (result.secrets.length === 0) {
464
+ p2.log.warn(`No secrets found for the ${chalk2.cyan(env)} environment.`);
465
+ p2.outro(chalk2.dim("Nothing written."));
466
+ return;
467
+ }
468
+ writeEnvFile(outputPath, result.secrets);
469
+ p2.outro(
470
+ `${chalk2.green("\u2713")} Written to ${chalk2.bold(filename)}
471
+ ${chalk2.dim(outputPath)}`
472
+ );
473
+ }
474
+ function printApiError(err) {
475
+ if (err instanceof ApiError) {
476
+ if (err.status === 401) {
477
+ p2.log.error("Unauthorized. Run `vaultsy login` to re-authenticate.");
478
+ } else if (err.status === 404) {
479
+ p2.log.error("Project or environment not found. Check the project ID and environment name.");
480
+ } else {
481
+ p2.log.error(`API error ${err.status}: ${err.message}`);
482
+ }
483
+ } else if (err instanceof Error) {
484
+ p2.log.error(err.message);
485
+ } else {
486
+ p2.log.error("An unexpected error occurred.");
487
+ }
488
+ }
489
+
490
+ // src/commands/push.ts
491
+ import * as p3 from "@clack/prompts";
492
+ import chalk3 from "chalk";
493
+ import { resolve as resolve3 } from "path";
494
+ import { existsSync as existsSync3 } from "fs";
495
+ init_api();
496
+ init_env();
497
+ async function pushCommand(projectArg, envArg, opts) {
498
+ p3.intro(chalk3.bold.cyan("vaultsy push"));
499
+ let projectId;
500
+ let projectTitle;
501
+ if (projectArg) {
502
+ projectId = projectArg;
503
+ } else {
504
+ const found = findProjectConfig();
505
+ if (found) {
506
+ projectId = found.config.project;
507
+ p3.log.info(`Using project ${chalk3.cyan(projectId)} from ${chalk3.dim("vaultsy.json")}`);
508
+ } else {
509
+ const spinner7 = p3.spinner();
510
+ spinner7.start("Fetching projects\u2026");
511
+ let projects;
512
+ try {
513
+ projects = await listProjects();
514
+ spinner7.stop(`Found ${projects.length} project${projects.length !== 1 ? "s" : ""}.`);
515
+ } catch (err) {
516
+ spinner7.stop("Failed to fetch projects.");
517
+ printApiError2(err);
518
+ process.exit(1);
519
+ }
520
+ if (projects.length === 0) {
521
+ p3.log.error("No projects found. Create one at your Vaultsy dashboard first.");
522
+ process.exit(1);
523
+ }
524
+ const selected = await p3.select({
525
+ message: "Select a project",
526
+ options: projects.map((proj) => ({
527
+ value: proj.id,
528
+ label: proj.title,
529
+ hint: proj.id
530
+ }))
531
+ });
532
+ if (p3.isCancel(selected)) {
533
+ p3.cancel("Push cancelled.");
534
+ process.exit(0);
535
+ }
536
+ projectId = selected;
537
+ projectTitle = projects.find((proj) => proj.id === selected)?.title;
538
+ }
539
+ }
540
+ let env;
541
+ if (envArg) {
542
+ if (!EnvironmentType.includes(envArg)) {
543
+ p3.log.error(
544
+ `Invalid environment "${envArg}". Must be one of: ${EnvironmentType.join(", ")}.`
545
+ );
546
+ process.exit(1);
547
+ }
548
+ env = envArg;
549
+ } else {
550
+ const found = findProjectConfig();
551
+ const defaultEnv = found?.config.defaultEnv;
552
+ const selected = await p3.select({
553
+ message: "Select an environment",
554
+ options: EnvironmentType.map((e) => ({
555
+ value: e,
556
+ label: e,
557
+ hint: e === defaultEnv ? "default" : void 0
558
+ })),
559
+ initialValue: defaultEnv ?? "development"
560
+ });
561
+ if (p3.isCancel(selected)) {
562
+ p3.cancel("Push cancelled.");
563
+ process.exit(0);
564
+ }
565
+ env = selected;
566
+ }
567
+ const filename = opts.input ?? envFileName(env);
568
+ const inputPath = resolve3(process.cwd(), filename);
569
+ if (!existsSync3(inputPath)) {
570
+ p3.log.error(
571
+ `File ${chalk3.bold(filename)} not found.
572
+ Run ${chalk3.cyan(`vaultsy pull ${projectId} ${env}`)} first, or specify a file with ${chalk3.dim("--input <file>")}.`
573
+ );
574
+ process.exit(1);
575
+ }
576
+ const localSecrets = readEnvFile(inputPath).filter((r) => r.key && r.value);
577
+ if (localSecrets.length === 0) {
578
+ p3.log.warn(`${chalk3.bold(filename)} is empty or contains no valid KEY=VALUE pairs.`);
579
+ p3.outro(chalk3.dim("Nothing pushed."));
580
+ return;
581
+ }
582
+ p3.log.info(
583
+ `Read ${chalk3.bold(String(localSecrets.length))} secret${localSecrets.length !== 1 ? "s" : ""} from ${chalk3.bold(filename)}.`
584
+ );
585
+ const diffSpinner = p3.spinner();
586
+ diffSpinner.start("Computing diff against remote\u2026");
587
+ let remoteSecrets;
588
+ let resolvedTitle;
589
+ try {
590
+ const remote = await pullSecrets(projectId, env);
591
+ remoteSecrets = remote.secrets;
592
+ resolvedTitle = projectTitle ?? remote.project.title;
593
+ diffSpinner.stop("Diff computed.");
594
+ } catch (err) {
595
+ diffSpinner.stop("Failed to fetch remote secrets.");
596
+ printApiError2(err);
597
+ process.exit(1);
598
+ }
599
+ const diff = computeDiff(remoteSecrets, localSecrets);
600
+ printDiff(diff);
601
+ const hasChanges = diff.added.length > 0 || diff.modified.length > 0 || diff.removed.length > 0;
602
+ if (!hasChanges) {
603
+ p3.outro(`${chalk3.dim("No changes.")} Remote ${chalk3.cyan(env)} is already up to date.`);
604
+ return;
605
+ }
606
+ if (!opts.yes) {
607
+ const confirmed = await p3.confirm({
608
+ message: `Push these changes to ${chalk3.bold(resolvedTitle)} / ${chalk3.cyan(env)}?`,
609
+ initialValue: true
610
+ });
611
+ if (p3.isCancel(confirmed) || !confirmed) {
612
+ p3.cancel("Push cancelled.");
613
+ process.exit(0);
614
+ }
615
+ }
616
+ const pushSpinner = p3.spinner();
617
+ pushSpinner.start(`Pushing to ${chalk3.cyan(env)}\u2026`);
618
+ try {
619
+ const result = await pushSecrets(projectId, env, localSecrets);
620
+ const { added, modified, removed, unchanged } = result.changes;
621
+ pushSpinner.stop(
622
+ `Done. ${chalk3.green(`+${added}`)} added, ${chalk3.yellow(`~${modified}`)} modified, ${chalk3.red(`-${removed}`)} removed, ${chalk3.dim(`${unchanged} unchanged`)}.`
623
+ );
624
+ } catch (err) {
625
+ pushSpinner.stop("Push failed.");
626
+ printApiError2(err);
627
+ process.exit(1);
628
+ }
629
+ p3.outro(
630
+ `${chalk3.green("\u2713")} ${chalk3.bold(resolvedTitle)} / ${chalk3.cyan(env)} updated successfully.`
631
+ );
632
+ }
633
+ function computeDiff(remote, local) {
634
+ const remoteMap = new Map(remote.map((r) => [r.key, r.value]));
635
+ const localMap = new Map(local.map((r) => [r.key, r.value]));
636
+ const added = [];
637
+ const modified = [];
638
+ const removed = [];
639
+ const unchanged = [];
640
+ for (const [key, value] of localMap) {
641
+ if (!remoteMap.has(key)) {
642
+ added.push(key);
643
+ } else if (remoteMap.get(key) !== value) {
644
+ modified.push(key);
645
+ } else {
646
+ unchanged.push(key);
647
+ }
648
+ }
649
+ for (const key of remoteMap.keys()) {
650
+ if (!localMap.has(key)) {
651
+ removed.push(key);
652
+ }
653
+ }
654
+ added.sort();
655
+ modified.sort();
656
+ removed.sort();
657
+ unchanged.sort();
658
+ return { added, modified, removed, unchanged };
659
+ }
660
+ function printDiff(diff) {
661
+ const total = diff.added.length + diff.modified.length + diff.removed.length + diff.unchanged.length;
662
+ if (total === 0) {
663
+ p3.log.info(chalk3.dim("No secrets on remote or local."));
664
+ return;
665
+ }
666
+ const lines = [];
667
+ for (const key of diff.added) {
668
+ lines.push(` ${chalk3.green("+")} ${chalk3.green(key)}`);
669
+ }
670
+ for (const key of diff.modified) {
671
+ lines.push(` ${chalk3.yellow("~")} ${chalk3.yellow(key)}`);
672
+ }
673
+ for (const key of diff.removed) {
674
+ lines.push(` ${chalk3.red("-")} ${chalk3.red(key)}`);
675
+ }
676
+ for (const key of diff.unchanged) {
677
+ lines.push(` ${chalk3.dim("\xB7")} ${chalk3.dim(key)}`);
678
+ }
679
+ p3.log.message(lines.join("\n"));
680
+ const summary = [
681
+ diff.added.length > 0 ? chalk3.green(`+${diff.added.length} to add`) : null,
682
+ diff.modified.length > 0 ? chalk3.yellow(`~${diff.modified.length} to modify`) : null,
683
+ diff.removed.length > 0 ? chalk3.red(`-${diff.removed.length} to remove`) : null,
684
+ diff.unchanged.length > 0 ? chalk3.dim(`${diff.unchanged.length} unchanged`) : null
685
+ ].filter(Boolean).join(chalk3.dim(", "));
686
+ p3.log.info(summary);
687
+ }
688
+ function printApiError2(err) {
689
+ if (err instanceof ApiError) {
690
+ if (err.status === 401) {
691
+ p3.log.error("Unauthorized. Run `vaultsy login` to re-authenticate.");
692
+ } else if (err.status === 404) {
693
+ p3.log.error("Project or environment not found. Check the project ID and environment name.");
694
+ } else {
695
+ p3.log.error(`API error ${err.status}: ${err.message}`);
696
+ }
697
+ } else if (err instanceof Error) {
698
+ p3.log.error(err.message);
699
+ } else {
700
+ p3.log.error("An unexpected error occurred.");
701
+ }
702
+ }
703
+
704
+ // src/commands/history.ts
705
+ import * as p4 from "@clack/prompts";
706
+ import chalk4 from "chalk";
707
+ init_api();
708
+ init_env();
709
+ async function historyCommand(projectArg, envArg) {
710
+ p4.intro(chalk4.bold.cyan("vaultsy history"));
711
+ let projectId;
712
+ let projectTitle;
713
+ if (projectArg) {
714
+ projectId = projectArg;
715
+ } else {
716
+ const found = findProjectConfig();
717
+ if (found) {
718
+ projectId = found.config.project;
719
+ p4.log.info(`Using project ${chalk4.cyan(projectId)} from ${chalk4.dim("vaultsy.json")}`);
720
+ } else {
721
+ const spinner8 = p4.spinner();
722
+ spinner8.start("Fetching projects\u2026");
723
+ let projects;
724
+ try {
725
+ projects = await listProjects();
726
+ spinner8.stop(`Found ${projects.length} project${projects.length !== 1 ? "s" : ""}.`);
727
+ } catch (err) {
728
+ spinner8.stop("Failed to fetch projects.");
729
+ printApiError3(err);
730
+ process.exit(1);
731
+ }
732
+ if (projects.length === 0) {
733
+ p4.log.error("No projects found. Create one at your Vaultsy dashboard first.");
734
+ process.exit(1);
735
+ }
736
+ const selected = await p4.select({
737
+ message: "Select a project",
738
+ options: projects.map((proj) => ({
739
+ value: proj.id,
740
+ label: proj.title,
741
+ hint: proj.id
742
+ }))
743
+ });
744
+ if (p4.isCancel(selected)) {
745
+ p4.cancel("Cancelled.");
746
+ process.exit(0);
747
+ }
748
+ projectId = selected;
749
+ projectTitle = projects.find((proj) => proj.id === selected)?.title;
750
+ }
751
+ }
752
+ let env;
753
+ if (envArg) {
754
+ if (!EnvironmentType.includes(envArg)) {
755
+ p4.log.error(
756
+ `Invalid environment "${envArg}". Must be one of: ${EnvironmentType.join(", ")}.`
757
+ );
758
+ process.exit(1);
759
+ }
760
+ env = envArg;
761
+ } else {
762
+ const found = findProjectConfig();
763
+ const defaultEnv = found?.config.defaultEnv;
764
+ const selected = await p4.select({
765
+ message: "Select an environment",
766
+ options: EnvironmentType.map((e) => ({
767
+ value: e,
768
+ label: e,
769
+ hint: e === defaultEnv ? "default" : void 0
770
+ })),
771
+ initialValue: defaultEnv ?? "development"
772
+ });
773
+ if (p4.isCancel(selected)) {
774
+ p4.cancel("Cancelled.");
775
+ process.exit(0);
776
+ }
777
+ env = selected;
778
+ }
779
+ const spinner7 = p4.spinner();
780
+ spinner7.start(`Fetching history for ${chalk4.cyan(env)}\u2026`);
781
+ let result;
782
+ try {
783
+ result = await listVersions(projectId, env);
784
+ spinner7.stop(
785
+ `${result.versions.length} snapshot${result.versions.length !== 1 ? "s" : ""} for ${chalk4.bold(projectTitle ?? result.project.title)} / ${chalk4.cyan(env)}.`
786
+ );
787
+ } catch (err) {
788
+ spinner7.stop("Failed to fetch history.");
789
+ printApiError3(err);
790
+ process.exit(1);
791
+ }
792
+ if (result.versions.length === 0) {
793
+ p4.log.warn(`No version history found for the ${chalk4.cyan(env)} environment.`);
794
+ p4.outro(chalk4.dim("Nothing to show."));
795
+ return;
796
+ }
797
+ const COL_VER = 7;
798
+ const COL_SECRETS = 7;
799
+ const COL_BY = 20;
800
+ const COL_DATE = 22;
801
+ const header = chalk4.bold(padEnd("#", COL_VER)) + chalk4.dim(" \u2502 ") + chalk4.bold(padEnd("VERSION ID", 26)) + chalk4.dim(" \u2502 ") + chalk4.bold(padEnd("KEYS", COL_SECRETS)) + chalk4.dim(" \u2502 ") + chalk4.bold(padEnd("CREATED BY", COL_BY)) + chalk4.dim(" \u2502 ") + chalk4.bold(padEnd("DATE", COL_DATE));
802
+ const divider = chalk4.dim(
803
+ "\u2500".repeat(COL_VER) + "\u2500\u253C\u2500" + "\u2500".repeat(26) + "\u2500\u253C\u2500" + "\u2500".repeat(COL_SECRETS) + "\u2500\u253C\u2500" + "\u2500".repeat(COL_BY) + "\u2500\u253C\u2500" + "\u2500".repeat(COL_DATE)
804
+ );
805
+ const rows = result.versions.map((v, i) => {
806
+ const isLatest = i === 0;
807
+ const vNum = isLatest ? chalk4.green(padEnd(`v${v.versionNumber}`, COL_VER)) : chalk4.dim(padEnd(`v${v.versionNumber}`, COL_VER));
808
+ const vId = chalk4.dim(padEnd(v.id, 26));
809
+ const secrets = padEnd(String(v.secretCount), COL_SECRETS);
810
+ const by = padEnd(v.createdBy?.name ?? chalk4.italic("system"), COL_BY);
811
+ const date = padEnd(formatDate(v.createdAt), COL_DATE);
812
+ const latestBadge = isLatest ? chalk4.green(" \u2190 latest") : "";
813
+ return vNum + chalk4.dim(" \u2502 ") + vId + chalk4.dim(" \u2502 ") + secrets + chalk4.dim(" \u2502 ") + by + chalk4.dim(" \u2502 ") + date + latestBadge;
814
+ });
815
+ const lines = [header, divider, ...rows];
816
+ p4.log.message(lines.join("\n"));
817
+ p4.log.info(
818
+ `To rollback, run: ${chalk4.cyan(`vaultsy rollback ${projectId} ${env} <VERSION_ID>`)}`
819
+ );
820
+ p4.outro(chalk4.dim("Done."));
821
+ }
822
+ var ANSI_REGEX = new RegExp("\x1B\\[[0-9;]*m", "g");
823
+ function padEnd(str, length) {
824
+ const visible = str.replace(ANSI_REGEX, "");
825
+ const pad = Math.max(0, length - visible.length);
826
+ return str + " ".repeat(pad);
827
+ }
828
+ function formatDate(iso) {
829
+ const d = new Date(iso);
830
+ const date = d.toLocaleDateString("en-GB", {
831
+ day: "2-digit",
832
+ month: "short",
833
+ year: "numeric"
834
+ });
835
+ const time = d.toLocaleTimeString("en-GB", {
836
+ hour: "2-digit",
837
+ minute: "2-digit"
838
+ });
839
+ return `${date}, ${time}`;
840
+ }
841
+ function printApiError3(err) {
842
+ if (err instanceof ApiError) {
843
+ if (err.status === 401) {
844
+ p4.log.error("Unauthorized. Run `vaultsy login` to re-authenticate.");
845
+ } else if (err.status === 404) {
846
+ p4.log.error("Project or environment not found. Check the project ID and environment name.");
847
+ } else {
848
+ p4.log.error(`API error ${err.status}: ${err.message}`);
849
+ }
850
+ } else if (err instanceof Error) {
851
+ p4.log.error(err.message);
852
+ } else {
853
+ p4.log.error("An unexpected error occurred.");
854
+ }
855
+ }
856
+
857
+ // src/commands/rollback.ts
858
+ import * as p5 from "@clack/prompts";
859
+ import chalk5 from "chalk";
860
+ init_api();
861
+ init_env();
862
+ async function rollbackCommand(projectArg, envArg, versionIdArg, opts) {
863
+ p5.intro(chalk5.bold.cyan("vaultsy rollback"));
864
+ let projectId;
865
+ let projectTitle;
866
+ if (projectArg) {
867
+ projectId = projectArg;
868
+ } else {
869
+ const found = findProjectConfig();
870
+ if (found) {
871
+ projectId = found.config.project;
872
+ p5.log.info(`Using project ${chalk5.cyan(projectId)} from ${chalk5.dim("vaultsy.json")}`);
873
+ } else {
874
+ const spinner8 = p5.spinner();
875
+ spinner8.start("Fetching projects\u2026");
876
+ let projects;
877
+ try {
878
+ projects = await listProjects();
879
+ spinner8.stop(`Found ${projects.length} project${projects.length !== 1 ? "s" : ""}.`);
880
+ } catch (err) {
881
+ spinner8.stop("Failed to fetch projects.");
882
+ printApiError4(err);
883
+ process.exit(1);
884
+ }
885
+ if (projects.length === 0) {
886
+ p5.log.error("No projects found. Create one at your Vaultsy dashboard first.");
887
+ process.exit(1);
888
+ }
889
+ const selected = await p5.select({
890
+ message: "Select a project",
891
+ options: projects.map((proj) => ({
892
+ value: proj.id,
893
+ label: proj.title,
894
+ hint: proj.id
895
+ }))
896
+ });
897
+ if (p5.isCancel(selected)) {
898
+ p5.cancel("Rollback cancelled.");
899
+ process.exit(0);
900
+ }
901
+ projectId = selected;
902
+ projectTitle = projects.find((proj) => proj.id === selected)?.title;
903
+ }
904
+ }
905
+ let env;
906
+ if (envArg) {
907
+ if (!EnvironmentType.includes(envArg)) {
908
+ p5.log.error(
909
+ `Invalid environment "${envArg}". Must be one of: ${EnvironmentType.join(", ")}.`
910
+ );
911
+ process.exit(1);
912
+ }
913
+ env = envArg;
914
+ } else {
915
+ const found = findProjectConfig();
916
+ const defaultEnv = found?.config.defaultEnv;
917
+ const selected = await p5.select({
918
+ message: "Select an environment",
919
+ options: EnvironmentType.map((e) => ({
920
+ value: e,
921
+ label: e,
922
+ hint: e === defaultEnv ? "default" : void 0
923
+ })),
924
+ initialValue: defaultEnv ?? "development"
925
+ });
926
+ if (p5.isCancel(selected)) {
927
+ p5.cancel("Rollback cancelled.");
928
+ process.exit(0);
929
+ }
930
+ env = selected;
931
+ }
932
+ let versionId;
933
+ let versionNumber;
934
+ if (versionIdArg) {
935
+ versionId = versionIdArg;
936
+ } else {
937
+ const spinner8 = p5.spinner();
938
+ spinner8.start(`Fetching version history for ${chalk5.cyan(env)}\u2026`);
939
+ let versionsResult;
940
+ try {
941
+ versionsResult = await listVersions(projectId, env);
942
+ spinner8.stop(
943
+ `Found ${versionsResult.versions.length} snapshot${versionsResult.versions.length !== 1 ? "s" : ""}.`
944
+ );
945
+ } catch (err) {
946
+ spinner8.stop("Failed to fetch version history.");
947
+ printApiError4(err);
948
+ process.exit(1);
949
+ }
950
+ if (versionsResult.versions.length === 0) {
951
+ p5.log.error(`No version history found for the ${chalk5.cyan(env)} environment.`);
952
+ process.exit(1);
953
+ }
954
+ const pickable = versionsResult.versions;
955
+ const selected = await p5.select({
956
+ message: "Select a version to roll back to",
957
+ options: pickable.map((v, i) => ({
958
+ value: v.id,
959
+ label: `v${v.versionNumber} \u2014 ${v.secretCount} key${v.secretCount !== 1 ? "s" : ""} \u2014 ${formatDate2(v.createdAt)}`,
960
+ hint: i === 0 ? "current" : v.createdBy?.name ? `by ${v.createdBy.name}` : void 0
961
+ }))
962
+ });
963
+ if (p5.isCancel(selected)) {
964
+ p5.cancel("Rollback cancelled.");
965
+ process.exit(0);
966
+ }
967
+ versionId = selected;
968
+ versionNumber = versionsResult.versions.find((v) => v.id === selected)?.versionNumber;
969
+ projectTitle ??= versionsResult.project.title;
970
+ }
971
+ if (!opts.yes) {
972
+ const label = versionNumber !== void 0 ? `v${versionNumber} (${chalk5.dim(versionId)})` : chalk5.dim(versionId);
973
+ p5.log.warn(
974
+ `This will overwrite all ${chalk5.bold(env)} secrets with the state from snapshot ${label}.
975
+ A new snapshot will be created automatically so you can undo this rollback too.`
976
+ );
977
+ const confirmed = await p5.confirm({
978
+ message: `Roll back ${chalk5.bold(projectTitle ?? projectId)} / ${chalk5.cyan(env)} to ${label}?`,
979
+ initialValue: false
980
+ });
981
+ if (p5.isCancel(confirmed) || !confirmed) {
982
+ p5.cancel("Rollback cancelled.");
983
+ process.exit(0);
984
+ }
985
+ }
986
+ const spinner7 = p5.spinner();
987
+ spinner7.start("Rolling back\u2026");
988
+ try {
989
+ const result = await rollback(projectId, env, versionId);
990
+ const { added, modified, removed, unchanged } = result.changes;
991
+ spinner7.stop(
992
+ `Rolled back to v${result.rolledBackTo.versionNumber}. ${chalk5.green(`+${added}`)} added, ${chalk5.yellow(`~${modified}`)} modified, ${chalk5.red(`-${removed}`)} removed, ${chalk5.dim(`${unchanged} unchanged`)}.`
993
+ );
994
+ p5.outro(
995
+ `${chalk5.green("\u2713")} ${chalk5.bold(projectTitle ?? result.project.title)} / ${chalk5.cyan(env)} rolled back to ${chalk5.bold(`v${result.rolledBackTo.versionNumber}`)}.`
996
+ );
997
+ } catch (err) {
998
+ spinner7.stop("Rollback failed.");
999
+ printApiError4(err);
1000
+ process.exit(1);
1001
+ }
1002
+ }
1003
+ function formatDate2(iso) {
1004
+ const d = new Date(iso);
1005
+ return d.toLocaleDateString("en-GB", {
1006
+ day: "2-digit",
1007
+ month: "short",
1008
+ year: "numeric"
1009
+ }) + ", " + d.toLocaleTimeString("en-GB", {
1010
+ hour: "2-digit",
1011
+ minute: "2-digit"
1012
+ });
1013
+ }
1014
+ function printApiError4(err) {
1015
+ if (err instanceof ApiError) {
1016
+ if (err.status === 401) {
1017
+ p5.log.error("Unauthorized. Run `vaultsy login` to re-authenticate.");
1018
+ } else if (err.status === 404) {
1019
+ p5.log.error("Project or environment not found. Check the project ID and environment name.");
1020
+ } else {
1021
+ p5.log.error(`API error ${err.status}: ${err.message}`);
1022
+ }
1023
+ } else if (err instanceof Error) {
1024
+ p5.log.error(err.message);
1025
+ } else {
1026
+ p5.log.error("An unexpected error occurred.");
1027
+ }
1028
+ }
1029
+
1030
+ // src/commands/run.ts
1031
+ import * as p6 from "@clack/prompts";
1032
+ import chalk6 from "chalk";
1033
+ import { spawn } from "child_process";
1034
+ init_api();
1035
+ init_env();
1036
+ async function runCommand(projectArg, envArg, commandArgs, _opts) {
1037
+ if (commandArgs.length === 0) {
1038
+ p6.log.error(
1039
+ `No command specified.
1040
+ Usage: ${chalk6.cyan("vaultsy run <project> <env> -- <command> [args...]")}
1041
+ Example: ${chalk6.dim("vaultsy run my-app production -- node server.js")}`
1042
+ );
1043
+ process.exit(1);
1044
+ }
1045
+ p6.intro(chalk6.bold.cyan("vaultsy run"));
1046
+ let projectId;
1047
+ let projectTitle;
1048
+ if (projectArg) {
1049
+ projectId = projectArg;
1050
+ } else {
1051
+ const found = findProjectConfig();
1052
+ if (found) {
1053
+ projectId = found.config.project;
1054
+ p6.log.info(`Using project ${chalk6.cyan(projectId)} from ${chalk6.dim("vaultsy.json")}`);
1055
+ } else {
1056
+ const spinner8 = p6.spinner();
1057
+ spinner8.start("Fetching projects\u2026");
1058
+ let projects;
1059
+ try {
1060
+ projects = await listProjects();
1061
+ spinner8.stop(`Found ${projects.length} project${projects.length !== 1 ? "s" : ""}.`);
1062
+ } catch (err) {
1063
+ spinner8.stop("Failed to fetch projects.");
1064
+ printApiError5(err);
1065
+ process.exit(1);
1066
+ }
1067
+ if (projects.length === 0) {
1068
+ p6.log.error("No projects found. Create one at your Vaultsy dashboard first.");
1069
+ process.exit(1);
1070
+ }
1071
+ const selected = await p6.select({
1072
+ message: "Select a project",
1073
+ options: projects.map((proj) => ({
1074
+ value: proj.id,
1075
+ label: proj.title,
1076
+ hint: proj.id
1077
+ }))
1078
+ });
1079
+ if (p6.isCancel(selected)) {
1080
+ p6.cancel("Run cancelled.");
1081
+ process.exit(0);
1082
+ }
1083
+ projectId = selected;
1084
+ projectTitle = projects.find((proj) => proj.id === selected)?.title;
1085
+ }
1086
+ }
1087
+ let env;
1088
+ if (envArg) {
1089
+ if (!EnvironmentType.includes(envArg)) {
1090
+ p6.log.error(
1091
+ `Invalid environment "${envArg}". Must be one of: ${EnvironmentType.join(", ")}.`
1092
+ );
1093
+ process.exit(1);
1094
+ }
1095
+ env = envArg;
1096
+ } else {
1097
+ const found = findProjectConfig();
1098
+ const defaultEnv = found?.config.defaultEnv;
1099
+ const selected = await p6.select({
1100
+ message: "Select an environment",
1101
+ options: EnvironmentType.map((e) => ({
1102
+ value: e,
1103
+ label: e,
1104
+ hint: e === defaultEnv ? "default" : void 0
1105
+ })),
1106
+ initialValue: defaultEnv ?? "development"
1107
+ });
1108
+ if (p6.isCancel(selected)) {
1109
+ p6.cancel("Run cancelled.");
1110
+ process.exit(0);
1111
+ }
1112
+ env = selected;
1113
+ }
1114
+ const spinner7 = p6.spinner();
1115
+ spinner7.start(`Pulling ${chalk6.cyan(env)} secrets\u2026`);
1116
+ let secrets;
1117
+ try {
1118
+ const result = await pullSecrets(projectId, env);
1119
+ secrets = result.secrets;
1120
+ projectTitle ??= result.project.title;
1121
+ spinner7.stop(
1122
+ `Injecting ${secrets.length} secret${secrets.length !== 1 ? "s" : ""} from ${chalk6.bold(projectTitle)} / ${chalk6.cyan(env)}.`
1123
+ );
1124
+ } catch (err) {
1125
+ spinner7.stop("Failed to pull secrets.");
1126
+ printApiError5(err);
1127
+ process.exit(1);
1128
+ }
1129
+ const injectedEnv = {
1130
+ ...secretsToRecord(secrets),
1131
+ // lower precedence
1132
+ ...filterStringRecord(process.env)
1133
+ // shell env wins
1134
+ };
1135
+ if (secrets.length > 0) {
1136
+ const keyList = secrets.map((s) => chalk6.dim(s.key)).join(", ");
1137
+ p6.log.info(`Injecting: ${keyList}`);
1138
+ } else {
1139
+ p6.log.warn("No secrets found \u2014 running with current environment only.");
1140
+ }
1141
+ const [bin, ...args] = commandArgs;
1142
+ p6.log.step(`${chalk6.bold("$")} ${chalk6.white([bin, ...args].join(" "))}`);
1143
+ process.stdout.write("");
1144
+ const child = spawn(bin, args, {
1145
+ env: injectedEnv,
1146
+ stdio: "inherit",
1147
+ // child shares stdin/stdout/stderr with us
1148
+ shell: false
1149
+ // do NOT use shell — avoids leaking env in ps output
1150
+ });
1151
+ const forwardSignal = (signal) => {
1152
+ if (!child.killed) {
1153
+ child.kill(signal);
1154
+ }
1155
+ };
1156
+ process.on("SIGINT", () => forwardSignal("SIGINT"));
1157
+ process.on("SIGTERM", () => forwardSignal("SIGTERM"));
1158
+ process.on("SIGHUP", () => forwardSignal("SIGHUP"));
1159
+ child.on("error", (err) => {
1160
+ if (err.code === "ENOENT") {
1161
+ p6.log.error(
1162
+ `Command not found: ${chalk6.bold(bin)}
1163
+ Make sure it is installed and available in your PATH.`
1164
+ );
1165
+ } else {
1166
+ p6.log.error(`Failed to start process: ${err.message}`);
1167
+ }
1168
+ process.exit(1);
1169
+ });
1170
+ child.on("close", (code, signal) => {
1171
+ if (signal) {
1172
+ const sigNum = signalToNumber(signal);
1173
+ process.exit(128 + sigNum);
1174
+ }
1175
+ const exitCode = code ?? 1;
1176
+ if (exitCode !== 0) {
1177
+ p6.log.warn(`Process exited with code ${chalk6.bold(String(exitCode))}.`);
1178
+ }
1179
+ process.exit(exitCode);
1180
+ });
1181
+ }
1182
+ function secretsToRecord(secrets) {
1183
+ const record = {};
1184
+ for (const { key, value } of secrets) {
1185
+ record[key] = value;
1186
+ }
1187
+ return record;
1188
+ }
1189
+ function filterStringRecord(env) {
1190
+ const out = {};
1191
+ for (const [key, value] of Object.entries(env)) {
1192
+ if (value !== void 0) out[key] = value;
1193
+ }
1194
+ return out;
1195
+ }
1196
+ function signalToNumber(signal) {
1197
+ const map = {
1198
+ SIGHUP: 1,
1199
+ SIGINT: 2,
1200
+ SIGQUIT: 3,
1201
+ SIGKILL: 9,
1202
+ SIGTERM: 15,
1203
+ SIGSTOP: 19
1204
+ };
1205
+ return map[signal] ?? 0;
1206
+ }
1207
+ function printApiError5(err) {
1208
+ if (err instanceof ApiError) {
1209
+ if (err.status === 401) {
1210
+ p6.log.error("Unauthorized. Run `vaultsy login` to re-authenticate.");
1211
+ } else if (err.status === 404) {
1212
+ p6.log.error("Project or environment not found. Check the project ID and environment name.");
1213
+ } else {
1214
+ p6.log.error(`API error ${err.status}: ${err.message}`);
1215
+ }
1216
+ } else if (err instanceof Error) {
1217
+ p6.log.error(err.message);
1218
+ } else {
1219
+ p6.log.error("An unexpected error occurred.");
1220
+ }
1221
+ }
1222
+
1223
+ // src/index.ts
1224
+ var program = new Command();
1225
+ program.name("vaultsy").description("Official CLI for Vaultsy \u2014 manage secrets from your terminal").version("0.1.0");
1226
+ program.command("login").description("Authenticate with your Vaultsy instance and save credentials locally").option("-t, --token <token>", "API token (skip the interactive prompt)").option(
1227
+ "-u, --base-url <url>",
1228
+ "Base URL of your Vaultsy instance (default: https://vaultsy.app)"
1229
+ ).action(async (opts) => {
1230
+ await loginCommand(opts);
1231
+ });
1232
+ program.command("logout").description("Remove locally stored credentials (~/.vaultsy/config.json)").action(async () => {
1233
+ const { clearConfig: clearConfig2, configExists: configExists2 } = await Promise.resolve().then(() => (init_config(), config_exports));
1234
+ const p7 = await import("@clack/prompts");
1235
+ const chalk7 = (await import("chalk")).default;
1236
+ if (!configExists2()) {
1237
+ p7.log.warn("No credentials found \u2014 already logged out.");
1238
+ return;
1239
+ }
1240
+ clearConfig2();
1241
+ p7.log.success(chalk7.green("\u2713") + " Logged out. Credentials removed.");
1242
+ });
1243
+ program.command("pull [project] [env]").description("Pull secrets from Vaultsy and write them to a local .env file").option("-o, --output <file>", "Output file path (default: .env or .env.<env>)").option("-y, --yes", "Skip confirmation prompts").action(
1244
+ async (project, env, opts) => {
1245
+ await pullCommand(project, env, opts);
1246
+ }
1247
+ );
1248
+ program.command("push [project] [env]").description("Push secrets from a local .env file up to Vaultsy").option("-i, --input <file>", "Input file path (default: .env or .env.<env>)").option("-y, --yes", "Skip the diff confirmation prompt").action(
1249
+ async (project, env, opts) => {
1250
+ await pushCommand(project, env, opts);
1251
+ }
1252
+ );
1253
+ program.command("history [project] [env]").description("List version snapshots for an environment").action(async (project, env) => {
1254
+ await historyCommand(project, env);
1255
+ });
1256
+ program.command("rollback [project] [env] [versionId]").description("Roll an environment back to a previous version snapshot").option("-y, --yes", "Skip the confirmation prompt").action(
1257
+ async (project, env, versionId, opts) => {
1258
+ await rollbackCommand(project, env, versionId, opts);
1259
+ }
1260
+ );
1261
+ program.command("run [project] [env]").description(
1262
+ "Pull secrets and inject them as env vars into a subprocess \u2014 secrets never touch disk"
1263
+ ).allowUnknownOption().helpOption("-H, --help", "Display help for the run command").action(
1264
+ async (project, env, _opts, cmd) => {
1265
+ const rawArgs = cmd.parent?.args ?? [];
1266
+ const separatorIndex = rawArgs.indexOf("--");
1267
+ const commandArgs = separatorIndex !== -1 ? rawArgs.slice(separatorIndex + 1) : [];
1268
+ await runCommand(project, env, commandArgs, {});
1269
+ }
1270
+ );
1271
+ program.command("init").description(
1272
+ "Create a vaultsy.json in the current directory to pin a project and default environment"
1273
+ ).action(async () => {
1274
+ const p7 = await import("@clack/prompts");
1275
+ const chalk7 = (await import("chalk")).default;
1276
+ const { listProjects: listProjects2 } = await Promise.resolve().then(() => (init_api(), api_exports));
1277
+ const { writeProjectConfig: writeProjectConfig2, findProjectConfig: findProjectConfig2 } = await Promise.resolve().then(() => (init_env(), env_exports));
1278
+ p7.intro(chalk7.bold.cyan("vaultsy init"));
1279
+ const existing = findProjectConfig2();
1280
+ if (existing) {
1281
+ p7.log.warn(
1282
+ `A ${chalk7.bold("vaultsy.json")} already exists at ${chalk7.dim(existing.dir)}.
1283
+ Delete it first if you want to re-initialise.`
1284
+ );
1285
+ process.exit(0);
1286
+ }
1287
+ const spinner7 = p7.spinner();
1288
+ spinner7.start("Fetching your projects\u2026");
1289
+ let projects;
1290
+ try {
1291
+ projects = await listProjects2();
1292
+ spinner7.stop(`Found ${projects.length} project${projects.length !== 1 ? "s" : ""}.`);
1293
+ } catch (err) {
1294
+ spinner7.stop("Failed to fetch projects.");
1295
+ if (err instanceof Error) p7.log.error(err.message);
1296
+ process.exit(1);
1297
+ }
1298
+ if (projects.length === 0) {
1299
+ p7.log.error("No projects found. Create one at your Vaultsy dashboard first.");
1300
+ process.exit(1);
1301
+ }
1302
+ const selectedProject = await p7.select({
1303
+ message: "Which project does this directory belong to?",
1304
+ options: projects.map((proj) => ({
1305
+ value: proj.id,
1306
+ label: proj.title,
1307
+ hint: proj.id
1308
+ }))
1309
+ });
1310
+ if (p7.isCancel(selectedProject)) {
1311
+ p7.cancel("Init cancelled.");
1312
+ process.exit(0);
1313
+ }
1314
+ const selectedEnv = await p7.select({
1315
+ message: "Default environment for this directory?",
1316
+ options: EnvironmentType.map((e) => ({ value: e, label: e })),
1317
+ initialValue: "development"
1318
+ });
1319
+ if (p7.isCancel(selectedEnv)) {
1320
+ p7.cancel("Init cancelled.");
1321
+ process.exit(0);
1322
+ }
1323
+ writeProjectConfig2({ project: selectedProject, defaultEnv: selectedEnv });
1324
+ p7.outro(
1325
+ `${chalk7.green("\u2713")} Created ${chalk7.bold("vaultsy.json")}
1326
+ Run ${chalk7.cyan("vaultsy pull")} or ${chalk7.cyan("vaultsy push")} with no arguments from this directory.`
1327
+ );
1328
+ });
1329
+ program.command("whoami").description("Show the currently authenticated user").action(async () => {
1330
+ const p7 = await import("@clack/prompts");
1331
+ const chalk7 = (await import("chalk")).default;
1332
+ const { getMe: getMe2 } = await Promise.resolve().then(() => (init_api(), api_exports));
1333
+ try {
1334
+ const me = await getMe2();
1335
+ p7.log.success(`Logged in as ${chalk7.bold(me.name)} ${chalk7.dim(`<${me.email}>`)}`);
1336
+ } catch (err) {
1337
+ if (err instanceof Error) {
1338
+ p7.log.error(err.message);
1339
+ } else {
1340
+ p7.log.error("Not authenticated. Run `vaultsy login` first.");
1341
+ }
1342
+ process.exit(1);
1343
+ }
1344
+ });
1345
+ program.parseAsync(process.argv).catch((err) => {
1346
+ const message = err instanceof Error ? err.message : String(err);
1347
+ console.error(`
1348
+ ${message}
1349
+ `);
1350
+ process.exit(1);
1351
+ });