vaulter-cli 0.2.1 → 2.3.1

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 CHANGED
@@ -30,11 +30,18 @@ vaulter add my-openai-key
30
30
  # Remove a key
31
31
  vaulter remove my-openai-key
32
32
 
33
+ # View decrypted values in your terminal
34
+ vaulter view STRIPE_SECRET
35
+ vaulter view KEY1 KEY2
36
+
33
37
  # Generate a .env file from your vault
34
38
  vaulter make .env
35
39
 
36
40
  # Upload a local .env file to your vault
37
41
  vaulter save .env
42
+
43
+ # Initialize current directory as a Vaulter project
44
+ vaulter init
38
45
  ```
39
46
 
40
47
  ## Commands
@@ -46,11 +53,32 @@ vaulter save .env
46
53
  | `vaulter ls` | List all API keys in your vault |
47
54
  | `vaulter add <name>` | Add a new API key to your vault |
48
55
  | `vaulter remove <name-or-id>` | Remove an API key from your vault |
56
+ | `vaulter view [key_names...]` | Decrypt and display one or more API keys in your terminal |
49
57
  | `vaulter make [file]` | Generate a .env file from your vault keys |
50
58
  | `vaulter save [file]` | Upload a local .env file to your vault |
59
+ | `vaulter init` | Initialize current directory as a Vaulter project |
51
60
  | `vaulter web-app` | Open the Vaulter web app in your browser |
52
61
  | `vaulter help` | Show all available commands |
53
62
 
63
+ ## Viewing Keys
64
+
65
+ ### `vaulter view [key_names...]`
66
+
67
+ Decrypt and print key values directly to your terminal — without writing them to a file.
68
+
69
+ ```bash
70
+ # Interactive: select which keys to view via checkbox
71
+ vaulter view
72
+
73
+ # View a specific key by name
74
+ vaulter view STRIPE_SECRET
75
+
76
+ # View multiple keys at once
77
+ vaulter view STRIPE_SECRET OPENAI_API_KEY
78
+ ```
79
+
80
+ Key names are matched case-insensitively. If a name doesn't match any key in your vault, a warning is printed and the rest continue. A security reminder is shown below the output table — clear your terminal when done.
81
+
54
82
  ## .env Support
55
83
 
56
84
  ### `vaulter make [file]`
@@ -80,6 +108,25 @@ vaulter save
80
108
  vaulter save .env.production
81
109
  ```
82
110
 
111
+ ## Project Initialization
112
+
113
+ ### `vaulter init`
114
+
115
+ Initialize the current directory as a Vaulter project. This creates a `.vaulter/config.json` file that associates the directory with a named project, and automatically adds `.vaulter/` to your `.gitignore` if one exists.
116
+
117
+ ```bash
118
+ # Interactive setup (prompts for project name)
119
+ vaulter init
120
+
121
+ # Non-interactive, use directory name as project name
122
+ vaulter init --yes
123
+
124
+ # Specify a project name directly
125
+ vaulter init --name my-project
126
+ ```
127
+
128
+ After initialization, if no `.env` is found and you're signed in, you'll be offered the option to generate one from your vault immediately.
129
+
83
130
  ## Authentication
84
131
 
85
132
  Vaulter uses browser-based device auth. Running `vaulter sign-in` opens your browser where you log in, and the CLI receives a token automatically. Credentials are stored locally at `~/.vaulter/credentials.json` with restricted file permissions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vaulter-cli",
3
- "version": "0.2.1",
3
+ "version": "2.3.1",
4
4
  "description": "CLI tool for Vaulter - Secure API Key Manager",
5
5
  "author": "faris-sait",
6
6
  "repository": {
package/postinstall.js CHANGED
@@ -17,6 +17,7 @@ console.log(purple4 + ' Your keys. Your vault.' + reset);
17
17
  console.log('');
18
18
  console.log(purple + ' COMMANDS' + reset);
19
19
  console.log('');
20
+ console.log(' ' + bold + white + 'vaulter init ' + reset + dim + 'Initialize current directory as a Vaulter project' + reset);
20
21
  console.log(' ' + bold + white + 'vaulter sign-in ' + reset + dim + 'Authenticate with Vaulter via browser' + reset);
21
22
  console.log(' ' + bold + white + 'vaulter sign-out ' + reset + dim + 'Sign out and clear saved credentials' + reset);
22
23
  console.log(' ' + bold + white + 'vaulter ls ' + reset + dim + 'List all API keys in your vault' + reset);
@@ -16,6 +16,7 @@ export async function showHelp() {
16
16
  console.log('');
17
17
 
18
18
  const commands = [
19
+ { name: 'init', desc: 'Initialize current directory as a Vaulter project' },
19
20
  { name: 'sign-in', desc: 'Authenticate with Vaulter via browser' },
20
21
  { name: 'sign-out', desc: 'Sign out and clear saved credentials' },
21
22
  { name: 'ls', desc: 'List all API keys in your vault' },
@@ -0,0 +1,119 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import inquirer from 'inquirer';
4
+ import {
5
+ isInitialized,
6
+ getProjectConfig,
7
+ createDefaultConfig,
8
+ writeProjectConfig,
9
+ getProjectDirPath,
10
+ } from '../lib/project.js';
11
+ import { isAuthenticated } from '../lib/auth.js';
12
+ import { success, error, warn, tip, dim, purple, bold, white } from '../lib/ui.js';
13
+
14
+ export async function initProject(options = {}) {
15
+ const root = process.cwd();
16
+ const dirName = path.basename(root);
17
+ const yes = options.yes || false;
18
+
19
+ console.log('');
20
+
21
+ // Check if already initialized
22
+ if (isInitialized(root)) {
23
+ const existing = getProjectConfig(root);
24
+
25
+ if (existing) {
26
+ console.log(dim(` Existing project: ${bold(existing.project?.name || 'unknown')}`));
27
+ console.log(dim(` Created: ${existing.project?.createdAt || 'unknown'}`));
28
+ console.log('');
29
+ }
30
+
31
+ if (!yes) {
32
+ const { reinit } = await inquirer.prompt([
33
+ {
34
+ type: 'confirm',
35
+ name: 'reinit',
36
+ message: purple('.vaulter/ already exists. Re-initialize?'),
37
+ default: false,
38
+ },
39
+ ]);
40
+ if (!reinit) {
41
+ warn('Cancelled.');
42
+ console.log('');
43
+ return;
44
+ }
45
+ }
46
+ }
47
+
48
+ // Prompt for project name
49
+ let projectName = options.name || null;
50
+
51
+ if (!projectName && !yes) {
52
+ const { name } = await inquirer.prompt([
53
+ {
54
+ type: 'input',
55
+ name: 'name',
56
+ message: purple('Project name:'),
57
+ default: dirName,
58
+ },
59
+ ]);
60
+ projectName = name;
61
+ }
62
+
63
+ if (!projectName) {
64
+ projectName = dirName;
65
+ }
66
+
67
+ // Create config
68
+ const config = createDefaultConfig(projectName);
69
+
70
+ try {
71
+ writeProjectConfig(root, config);
72
+ } catch (err) {
73
+ error(`Failed to create .vaulter/config.json: ${err.message}`);
74
+ console.log('');
75
+ return;
76
+ }
77
+
78
+ success(`Initialized Vaulter project "${projectName}"`);
79
+
80
+ // Append .vaulter/ to .gitignore if it exists and doesn't already contain it
81
+ const gitignorePath = path.join(root, '.gitignore');
82
+ if (fs.existsSync(gitignorePath)) {
83
+ try {
84
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
85
+ if (!content.includes('.vaulter/')) {
86
+ fs.appendFileSync(gitignorePath, '\n.vaulter/\n');
87
+ success('Added .vaulter/ to .gitignore');
88
+ }
89
+ } catch {
90
+ warn('Could not update .gitignore (non-fatal)');
91
+ }
92
+ }
93
+
94
+ // .env tips based on state
95
+ const envPath = path.join(root, '.env');
96
+ const envExists = fs.existsSync(envPath);
97
+
98
+ if (envExists) {
99
+ tip('Run `vaulter save` to upload your .env to the vault, or `vaulter make` to regenerate it.');
100
+ } else if (isAuthenticated()) {
101
+ console.log('');
102
+ const { generate } = await inquirer.prompt([
103
+ {
104
+ type: 'confirm',
105
+ name: 'generate',
106
+ message: purple('No .env found. Generate one from your vault?'),
107
+ default: true,
108
+ },
109
+ ]);
110
+ if (generate) {
111
+ const { makeEnv } = await import('./make.js');
112
+ await makeEnv(undefined, {});
113
+ }
114
+ } else {
115
+ tip('Run `vaulter sign-in` to connect your vault, then `vaulter make` to generate a .env.');
116
+ }
117
+
118
+ console.log('');
119
+ }
@@ -0,0 +1,112 @@
1
+ import inquirer from 'inquirer';
2
+ import ora from 'ora';
3
+ import Table from 'cli-table3';
4
+ import chalk from 'chalk';
5
+ import { apiFetch } from '../lib/api.js';
6
+ import { purple, dim, error, warn } from '../lib/ui.js';
7
+
8
+ export async function viewKeys(names) {
9
+ const spinner = ora({ text: 'Fetching vault keys...', color: 'magenta' }).start();
10
+
11
+ let keys;
12
+ try {
13
+ const data = await apiFetch('/api/keys');
14
+ keys = data.keys || [];
15
+ } catch (err) {
16
+ spinner.fail('Failed to fetch keys');
17
+ error(err.message);
18
+ return;
19
+ }
20
+
21
+ spinner.stop();
22
+
23
+ if (keys.length === 0) {
24
+ console.log('');
25
+ warn('No keys in your vault. Run `vaulter add <name>` to add one.');
26
+ console.log('');
27
+ return;
28
+ }
29
+
30
+ let selected;
31
+
32
+ if (names.length === 0) {
33
+ // Interactive checkbox selection
34
+ console.log('');
35
+ const { selectedKeys } = await inquirer.prompt([
36
+ {
37
+ type: 'checkbox',
38
+ name: 'selectedKeys',
39
+ message: purple('Select keys to view:'),
40
+ choices: keys.map((k) => ({
41
+ name: `${k.name} ${dim(`(${k.masked_key})`)}`,
42
+ value: k,
43
+ checked: false,
44
+ })),
45
+ },
46
+ ]);
47
+
48
+ if (selectedKeys.length === 0) {
49
+ console.log('');
50
+ warn('No keys selected.');
51
+ console.log('');
52
+ return;
53
+ }
54
+
55
+ selected = selectedKeys;
56
+ } else {
57
+ // Match provided names case-insensitively
58
+ selected = [];
59
+ for (const name of names) {
60
+ const match = keys.find((k) => k.name.toLowerCase() === name.toLowerCase());
61
+ if (!match) {
62
+ warn(`No key found matching "${name}".`);
63
+ } else {
64
+ selected.push(match);
65
+ }
66
+ }
67
+
68
+ if (selected.length === 0) {
69
+ console.log('');
70
+ return;
71
+ }
72
+ }
73
+
74
+ // Decrypt each selected key
75
+ const decryptSpinner = ora({ text: `Decrypting ${selected.length} key(s)...`, color: 'magenta' }).start();
76
+
77
+ const rows = [];
78
+ for (const key of selected) {
79
+ try {
80
+ const data = await apiFetch(`/api/keys/${key.id}?decrypt=true`);
81
+ rows.push([chalk.white(key.name), chalk.hex('#a78bfa')(data.decrypted_key)]);
82
+ } catch (err) {
83
+ rows.push([chalk.white(key.name), dim('(failed to decrypt)')]);
84
+ }
85
+ }
86
+
87
+ decryptSpinner.stop();
88
+
89
+ const table = new Table({
90
+ head: [purple.bold('Name'), purple.bold('Decrypted Value')],
91
+ style: {
92
+ head: [],
93
+ border: ['dim'],
94
+ },
95
+ chars: {
96
+ 'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐',
97
+ 'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘',
98
+ 'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼',
99
+ 'right': '│', 'right-mid': '┤', 'middle': '│',
100
+ },
101
+ });
102
+
103
+ for (const row of rows) {
104
+ table.push(row);
105
+ }
106
+
107
+ console.log('');
108
+ console.log(table.toString());
109
+ console.log('');
110
+ console.log(dim(' These values are sensitive. Clear your terminal when done.'));
111
+ console.log('');
112
+ }
package/src/index.js CHANGED
@@ -8,6 +8,8 @@ import { addKey } from './commands/add.js';
8
8
  import { removeKey } from './commands/remove.js';
9
9
  import { makeEnv } from './commands/make.js';
10
10
  import { saveEnv } from './commands/save.js';
11
+ import { viewKeys } from './commands/view.js';
12
+ import { initProject } from './commands/init.js';
11
13
  import { openWebApp } from './commands/web-app.js';
12
14
  import { showHelp } from './commands/help.js';
13
15
  import { printLogo } from './assets/logo.js';
@@ -19,7 +21,7 @@ const program = new Command();
19
21
  program
20
22
  .name('vaulter')
21
23
  .description('Vaulter CLI - Secure API Key Manager')
22
- .version('0.2.1')
24
+ .version('2.3.1')
23
25
  .action(async () => {
24
26
  await showHelp();
25
27
  });
@@ -90,6 +92,26 @@ program
90
92
  await saveEnv(filename);
91
93
  });
92
94
 
95
+ program
96
+ .command('view [key_names...]')
97
+ .description('Decrypt and display one or more API keys in your terminal')
98
+ .action(async (names) => {
99
+ if (!isAuthenticated()) {
100
+ error('Not authenticated. Run `vaulter sign-in` first.');
101
+ process.exit(1);
102
+ }
103
+ await viewKeys(names);
104
+ });
105
+
106
+ program
107
+ .command('init')
108
+ .description('Initialize current directory as a Vaulter project')
109
+ .option('-n, --name <name>', 'Project name')
110
+ .option('-y, --yes', 'Non-interactive, use all defaults')
111
+ .action(async (options) => {
112
+ await initProject(options);
113
+ });
114
+
93
115
  program
94
116
  .command('web-app')
95
117
  .description('Open the Vaulter web app in your browser')
@@ -0,0 +1,56 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const VAULTER_PROJECT_DIR = '.vaulter';
5
+ const CONFIG_FILE = 'config.json';
6
+
7
+ export function getProjectDirPath(root = process.cwd()) {
8
+ return path.join(root, VAULTER_PROJECT_DIR);
9
+ }
10
+
11
+ export function getConfigFilePath(root = process.cwd()) {
12
+ return path.join(root, VAULTER_PROJECT_DIR, CONFIG_FILE);
13
+ }
14
+
15
+ export function isInitialized(root = process.cwd()) {
16
+ return fs.existsSync(getConfigFilePath(root));
17
+ }
18
+
19
+ export function getProjectConfig(root = process.cwd()) {
20
+ try {
21
+ const data = JSON.parse(fs.readFileSync(getConfigFilePath(root), 'utf-8'));
22
+ return data;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export function ensureProjectDir(root = process.cwd()) {
29
+ const dirPath = getProjectDirPath(root);
30
+ if (!fs.existsSync(dirPath)) {
31
+ fs.mkdirSync(dirPath, { mode: 0o700 });
32
+ }
33
+ }
34
+
35
+ export function createDefaultConfig(name) {
36
+ const now = new Date().toISOString();
37
+ return {
38
+ version: 1,
39
+ project: {
40
+ name,
41
+ createdAt: now,
42
+ updatedAt: now,
43
+ },
44
+ sync: { enabled: false, remote: null },
45
+ env: { autoGenerate: false, filename: '.env', selectedKeys: [] },
46
+ };
47
+ }
48
+
49
+ export function writeProjectConfig(root, config) {
50
+ ensureProjectDir(root);
51
+ fs.writeFileSync(
52
+ getConfigFilePath(root),
53
+ JSON.stringify(config, null, 2) + '\n',
54
+ { mode: 0o600 }
55
+ );
56
+ }