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 +47 -0
- package/package.json +1 -1
- package/postinstall.js +1 -0
- package/src/commands/help.js +1 -0
- package/src/commands/init.js +119 -0
- package/src/commands/view.js +112 -0
- package/src/index.js +23 -1
- package/src/lib/project.js +56 -0
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
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);
|
package/src/commands/help.js
CHANGED
|
@@ -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('
|
|
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
|
+
}
|