@dinanathdash/envault 1.0.2 → 1.2.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/CHANGELOG.md +14 -0
- package/README.md +0 -4
- package/bin/envault.js +6 -1
- package/package.json +5 -1
- package/release.config.js +1 -1
- package/src/commands/deploy.js +79 -4
- package/src/commands/init.js +36 -7
- package/src/commands/login.js +34 -1
- package/src/commands/pull.js +42 -3
- package/src/lib/api.js +23 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.2.0](https://github.com/DinanathDash/Envault/compare/v1.1.0...v1.2.0) (2026-02-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Improve CLI login with clipboard support and user email display, enhance `.env` file detection for deploy, and add a `/api/cli/me` endpoint. ([f66aa4a](https://github.com/DinanathDash/Envault/commit/f66aa4a028242d4939fdedcd4165b2bba2a5e290))
|
|
7
|
+
|
|
8
|
+
# [1.1.0](https://github.com/DinanathDash/Envault/compare/v1.0.2...v1.1.0) (2026-02-01)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* Implement CLI device authentication, add new CLI API routes for projects and secrets, and enhance CLI commands with improved warnings. ([e4871c4](https://github.com/DinanathDash/Envault/commit/e4871c453d7e3974e94505e1f65014350a15a53a))
|
|
14
|
+
|
|
1
15
|
## [1.0.2](https://github.com/DinanathDash/Envault/compare/v1.0.1...v1.0.2) (2026-02-01)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
package/bin/envault.js
CHANGED
|
@@ -7,6 +7,10 @@ import { login } from '../src/commands/login.js';
|
|
|
7
7
|
import { init } from '../src/commands/init.js';
|
|
8
8
|
import { deploy } from '../src/commands/deploy.js';
|
|
9
9
|
import { pull } from '../src/commands/pull.js';
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const pkg = require('../package.json');
|
|
10
14
|
|
|
11
15
|
const program = new Command();
|
|
12
16
|
|
|
@@ -26,7 +30,7 @@ const showLogo = () => {
|
|
|
26
30
|
program
|
|
27
31
|
.name('envault')
|
|
28
32
|
.description('Cliff-side security for your environment variables')
|
|
29
|
-
.version(
|
|
33
|
+
.version(pkg.version)
|
|
30
34
|
.hook('preAction', (thisCommand) => {
|
|
31
35
|
// Show logo on all commands? Maybe too noisy.
|
|
32
36
|
// Let's show it only on help or specific ones.
|
|
@@ -50,6 +54,7 @@ program.command('deploy')
|
|
|
50
54
|
.description('Deploy local .env to Envault (Encrypt & Push)')
|
|
51
55
|
.option('-p, --project <id>', 'Project ID')
|
|
52
56
|
.option('--dry-run', 'Show what would change without pushing')
|
|
57
|
+
.option('-f, --force', 'Skip confirmation prompts')
|
|
53
58
|
.action(async (options) => {
|
|
54
59
|
await deploy(options);
|
|
55
60
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dinanathdash/envault",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Envault CLI - Securely manage your environment variables",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"axios": "^1.6.0",
|
|
26
26
|
"boxen": "^7.1.1",
|
|
27
27
|
"chalk": "^5.3.0",
|
|
28
|
+
"clipboardy": "^5.1.0",
|
|
28
29
|
"commander": "^11.1.0",
|
|
29
30
|
"conf": "^12.0.0",
|
|
30
31
|
"dotenv": "^16.3.1",
|
|
@@ -37,5 +38,8 @@
|
|
|
37
38
|
"@semantic-release/exec": "^7.1.0",
|
|
38
39
|
"@semantic-release/git": "^10.0.1",
|
|
39
40
|
"semantic-release": "^25.0.3"
|
|
41
|
+
},
|
|
42
|
+
"overrides": {
|
|
43
|
+
"tar": "^7.5.7"
|
|
40
44
|
}
|
|
41
45
|
}
|
package/release.config.js
CHANGED
|
@@ -13,7 +13,7 @@ const config = {
|
|
|
13
13
|
[
|
|
14
14
|
'@semantic-release/git',
|
|
15
15
|
{
|
|
16
|
-
assets: ['package.json', 'CHANGELOG.md', 'README.md'],
|
|
16
|
+
assets: ['package.json', 'package-lock.json', 'CHANGELOG.md', 'README.md'],
|
|
17
17
|
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
|
|
18
18
|
},
|
|
19
19
|
],
|
package/src/commands/deploy.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import boxen from 'boxen';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
1
3
|
|
|
2
4
|
import fs from 'fs';
|
|
3
5
|
import dotenv from 'dotenv';
|
|
@@ -21,10 +23,32 @@ export async function deploy(options) {
|
|
|
21
23
|
return;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// 1. Scan for env files
|
|
27
|
+
const allFiles = fs.readdirSync(process.cwd());
|
|
28
|
+
const envFiles = allFiles.filter(file => file.startsWith('.env') && !['.env.example', '.env.template', '.env.sample'].includes(file));
|
|
29
|
+
|
|
30
|
+
let envPath = '.env';
|
|
31
|
+
|
|
32
|
+
if (envFiles.length === 0) {
|
|
33
|
+
console.error(chalk.red('Error: No .env files found (looked for files starting with .env, excluding .example/.template/.sample).'));
|
|
27
34
|
return;
|
|
35
|
+
} else if (envFiles.length === 1) {
|
|
36
|
+
envPath = envFiles[0];
|
|
37
|
+
console.log(chalk.blue(`Using environment file: ${envPath}`));
|
|
38
|
+
} else {
|
|
39
|
+
// prioritize .env.local if users want, but for now let's ask
|
|
40
|
+
// OR we can default to .env.local if present, else asking.
|
|
41
|
+
// The plan said "Multiple files found: Use inquirer to show a list"
|
|
42
|
+
|
|
43
|
+
console.log(chalk.yellow(`Multiple environment files found: ${envFiles.join(', ')}`));
|
|
44
|
+
const { selectedEnv } = await inquirer.prompt([{
|
|
45
|
+
type: 'list',
|
|
46
|
+
name: 'selectedEnv',
|
|
47
|
+
message: 'Which environment file do you want to deploy?',
|
|
48
|
+
choices: envFiles
|
|
49
|
+
}]);
|
|
50
|
+
envPath = selectedEnv;
|
|
51
|
+
console.log(chalk.blue(`Selected: ${envPath}`));
|
|
28
52
|
}
|
|
29
53
|
|
|
30
54
|
const envConfig = dotenv.parse(fs.readFileSync(envPath));
|
|
@@ -41,11 +65,62 @@ export async function deploy(options) {
|
|
|
41
65
|
return;
|
|
42
66
|
}
|
|
43
67
|
|
|
68
|
+
if (!options.force) {
|
|
69
|
+
let projectName = 'Envault';
|
|
70
|
+
try {
|
|
71
|
+
// Attempt to fetch project name. If fails, fallback to 'Envault'
|
|
72
|
+
// We use the list endpoint as we know it exists from init.js
|
|
73
|
+
const { data } = await api.get('/projects');
|
|
74
|
+
const projects = data.projects || data.data || [];
|
|
75
|
+
const project = projects.find(p => p.id === projectId);
|
|
76
|
+
if (project) {
|
|
77
|
+
projectName = project.name;
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
// Ignore error and use default
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://envault.tech';
|
|
84
|
+
|
|
85
|
+
console.log(boxen(
|
|
86
|
+
chalk.red.bold('WARNING: OVERWRITING REMOTE SECRETS') +
|
|
87
|
+
'\n\n' +
|
|
88
|
+
chalk.white('You are about to ') + chalk.red.bold('DEPLOY') + chalk.white(' local variables to your project:') +
|
|
89
|
+
'\n' +
|
|
90
|
+
chalk.cyan.bold(projectName) +
|
|
91
|
+
'\n\n' +
|
|
92
|
+
chalk.white('Existing secrets in the project will be ') + chalk.red.bold('OVERWRITTEN') + chalk.white(' by values in your .env.') +
|
|
93
|
+
'\n\n' +
|
|
94
|
+
chalk.dim('We recommend checking the dashboard for differences:') +
|
|
95
|
+
'\n' +
|
|
96
|
+
chalk.cyan(`${appUrl}/project/${projectId}`),
|
|
97
|
+
{
|
|
98
|
+
padding: 1,
|
|
99
|
+
margin: 1,
|
|
100
|
+
borderStyle: 'double',
|
|
101
|
+
borderColor: 'red',
|
|
102
|
+
title: 'Deploy Warning',
|
|
103
|
+
titleAlignment: 'center'
|
|
104
|
+
}
|
|
105
|
+
));
|
|
106
|
+
|
|
107
|
+
const { confirm } = await inquirer.prompt([{
|
|
108
|
+
type: 'confirm',
|
|
109
|
+
name: 'confirm',
|
|
110
|
+
message: `Are you sure you want to deploy ${secrets.length} secrets to the project?`,
|
|
111
|
+
default: false
|
|
112
|
+
}]);
|
|
113
|
+
if (!confirm) {
|
|
114
|
+
console.log(chalk.yellow('Operation cancelled.'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
44
119
|
const spinner = ora('Encrypting and deploying secrets...').start();
|
|
45
120
|
|
|
46
121
|
try {
|
|
47
122
|
const { data } = await api.post(`/projects/${projectId}/secrets`, { secrets });
|
|
48
|
-
spinner.succeed(chalk.green(
|
|
123
|
+
spinner.succeed(chalk.green(`Successfully deployed ${secrets.length} secrets!`));
|
|
49
124
|
} catch (error) {
|
|
50
125
|
spinner.fail('Deploy failed.');
|
|
51
126
|
console.error(chalk.red(handleApiError(error)));
|
package/src/commands/init.js
CHANGED
|
@@ -43,17 +43,46 @@ export async function init() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (projects.length === 0) {
|
|
46
|
-
console.log(chalk.yellow('No projects found.
|
|
47
|
-
return;
|
|
46
|
+
console.log(chalk.yellow('No existing projects found.'));
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
const {
|
|
49
|
+
const { selectedProjectId } = await inquirer.prompt([{
|
|
51
50
|
type: 'list',
|
|
52
|
-
name: '
|
|
51
|
+
name: 'selectedProjectId',
|
|
53
52
|
message: 'Select the project to link:',
|
|
54
|
-
choices:
|
|
53
|
+
choices: [
|
|
54
|
+
new inquirer.Separator(),
|
|
55
|
+
{ name: '+ Create New Project', value: 'CREATE_NEW' },
|
|
56
|
+
new inquirer.Separator(),
|
|
57
|
+
...projects.map(p => ({ name: p.name, value: p.id }))
|
|
58
|
+
]
|
|
55
59
|
}]);
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
let projectId = selectedProjectId;
|
|
62
|
+
|
|
63
|
+
if (selectedProjectId === 'CREATE_NEW') {
|
|
64
|
+
const { newProjectName } = await inquirer.prompt([{
|
|
65
|
+
type: 'input',
|
|
66
|
+
name: 'newProjectName',
|
|
67
|
+
message: 'Enter name for the new project:',
|
|
68
|
+
validate: input => input.trim().length > 0 ? true : 'Project name cannot be empty'
|
|
69
|
+
}]);
|
|
70
|
+
|
|
71
|
+
const createSpinner = ora('Creating project...').start();
|
|
72
|
+
try {
|
|
73
|
+
const { data } = await api.post('/projects', { name: newProjectName });
|
|
74
|
+
projectId = data.project.id;
|
|
75
|
+
createSpinner.succeed(chalk.green(`Project "${data.project.name}" created!`));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
createSpinner.fail('Failed to create project.');
|
|
78
|
+
console.error(chalk.red(handleApiError(error)));
|
|
79
|
+
return; // Exit if creation fails
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Only write config if we have a valid projectId
|
|
84
|
+
if (projectId) {
|
|
85
|
+
fs.writeFileSync('envault.json', JSON.stringify({ projectId }, null, 2));
|
|
86
|
+
console.log(chalk.green(`\n✔ Project linked! (ID: ${projectId})`));
|
|
87
|
+
}
|
|
59
88
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -3,8 +3,10 @@ import chalk from 'chalk';
|
|
|
3
3
|
import boxen from 'boxen';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import open from 'open';
|
|
6
|
+
import clipboard from 'clipboardy';
|
|
6
7
|
import { api, handleApiError } from '../lib/api.js';
|
|
7
8
|
import { setToken } from '../lib/config.js';
|
|
9
|
+
import os from 'os';
|
|
8
10
|
|
|
9
11
|
export async function login() {
|
|
10
12
|
console.log(chalk.blue(' Starting Device Authentication Flow...\n'));
|
|
@@ -13,8 +15,16 @@ export async function login() {
|
|
|
13
15
|
const spinner = ora('Contacting Envault servers...').start();
|
|
14
16
|
let deviceCode, userCode, verificationUri, interval;
|
|
15
17
|
|
|
18
|
+
const deviceInfo = {
|
|
19
|
+
hostname: os.hostname(),
|
|
20
|
+
platform: os.platform(),
|
|
21
|
+
release: os.release(),
|
|
22
|
+
type: os.type(),
|
|
23
|
+
arch: os.arch()
|
|
24
|
+
};
|
|
25
|
+
|
|
16
26
|
try {
|
|
17
|
-
const { data } = await api.post('/auth/device/code');
|
|
27
|
+
const { data } = await api.post('/auth/device/code', { device_info: deviceInfo });
|
|
18
28
|
deviceCode = data.device_code;
|
|
19
29
|
userCode = data.user_code; // 8-char
|
|
20
30
|
verificationUri = data.verification_uri;
|
|
@@ -32,12 +42,20 @@ export async function login() {
|
|
|
32
42
|
console.log(boxen(chalk.green.bold(userCode), {
|
|
33
43
|
title: 'Authentication Code',
|
|
34
44
|
titleAlignment: 'center',
|
|
45
|
+
textAlignment: 'center',
|
|
35
46
|
padding: 1,
|
|
36
47
|
margin: 1,
|
|
37
48
|
borderStyle: 'round',
|
|
38
49
|
borderColor: 'green'
|
|
39
50
|
}));
|
|
40
51
|
|
|
52
|
+
try {
|
|
53
|
+
clipboard.writeSync(userCode);
|
|
54
|
+
console.log(chalk.dim('(Code copied to clipboard)'));
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// Ignore errors if clipboard fails (headless etc)
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
// Open browser automatically
|
|
42
60
|
try {
|
|
43
61
|
await open(verificationUri);
|
|
@@ -78,7 +96,22 @@ export async function login() {
|
|
|
78
96
|
try {
|
|
79
97
|
const token = await poll();
|
|
80
98
|
setToken(token);
|
|
99
|
+
|
|
100
|
+
// Fetch user info
|
|
101
|
+
let email = '';
|
|
102
|
+
try {
|
|
103
|
+
const { data: userData } = await api.get('/me');
|
|
104
|
+
if (userData && userData.email) {
|
|
105
|
+
email = userData.email;
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// Ignore error if fetching user info fails, just show generic success
|
|
109
|
+
}
|
|
110
|
+
|
|
81
111
|
spinner.succeed(chalk.green('Successfully authenticated! Token saved.'));
|
|
112
|
+
if (email) {
|
|
113
|
+
console.log(chalk.green(`Logged in as: ${chalk.bold(email)}`));
|
|
114
|
+
}
|
|
82
115
|
} catch (error) {
|
|
83
116
|
spinner.fail('Authentication failed.');
|
|
84
117
|
console.error(chalk.red(error.message));
|
package/src/commands/pull.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import ora from 'ora';
|
|
5
|
+
import boxen from 'boxen';
|
|
6
|
+
|
|
5
7
|
import inquirer from 'inquirer';
|
|
6
8
|
import { api, handleApiError } from '../lib/api.js';
|
|
7
9
|
|
|
@@ -22,13 +24,50 @@ export async function pull(options) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
if (fs.existsSync('.env') && !options.force) {
|
|
27
|
+
let projectName = 'Envault';
|
|
28
|
+
try {
|
|
29
|
+
const { data } = await api.get('/projects');
|
|
30
|
+
const projects = data.projects || data.data || [];
|
|
31
|
+
const project = projects.find(p => p.id === projectId);
|
|
32
|
+
if (project) {
|
|
33
|
+
projectName = project.name;
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// Ignore
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://envault.tech';
|
|
40
|
+
|
|
41
|
+
console.log(boxen(
|
|
42
|
+
chalk.red.bold('WARNING: POTENTIAL DATA LOSS') +
|
|
43
|
+
'\n\n' +
|
|
44
|
+
chalk.white('You are about to ') + chalk.red.bold('OVERWRITE') + chalk.white(' your local ') + chalk.yellow('.env') + chalk.white(' file.') +
|
|
45
|
+
'\n\n' +
|
|
46
|
+
chalk.white('Any local changes not synced to ') + chalk.cyan.bold(projectName) + chalk.white(' will be ') + chalk.red.bold('PERMANENTLY LOST.') +
|
|
47
|
+
'\n\n' +
|
|
48
|
+
chalk.dim('We recommend checking the dashboard for differences:') +
|
|
49
|
+
'\n' +
|
|
50
|
+
chalk.cyan(`${appUrl}/project/${projectId}`),
|
|
51
|
+
{
|
|
52
|
+
padding: 1,
|
|
53
|
+
margin: 1,
|
|
54
|
+
borderStyle: 'double',
|
|
55
|
+
borderColor: 'red',
|
|
56
|
+
title: 'Sync Warning',
|
|
57
|
+
titleAlignment: 'center'
|
|
58
|
+
}
|
|
59
|
+
));
|
|
60
|
+
|
|
25
61
|
const { confirm } = await inquirer.prompt([{
|
|
26
62
|
type: 'confirm',
|
|
27
63
|
name: 'confirm',
|
|
28
|
-
message: '
|
|
64
|
+
message: 'Are you sure you want to overwrite your local .env file?',
|
|
29
65
|
default: false
|
|
30
66
|
}]);
|
|
31
|
-
if (!confirm)
|
|
67
|
+
if (!confirm) {
|
|
68
|
+
console.log(chalk.yellow('Operation cancelled.'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
32
71
|
}
|
|
33
72
|
|
|
34
73
|
const spinner = ora('Fetching secrets...').start();
|
|
@@ -47,7 +86,7 @@ export async function pull(options) {
|
|
|
47
86
|
.join('\n');
|
|
48
87
|
|
|
49
88
|
fs.writeFileSync('.env', envContent);
|
|
50
|
-
spinner.succeed(chalk.green(
|
|
89
|
+
spinner.succeed(chalk.green(`Pulled ${data.secrets.length} secrets to .env`));
|
|
51
90
|
|
|
52
91
|
} catch (error) {
|
|
53
92
|
spinner.fail('Pull failed.');
|
package/src/lib/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import axios from 'axios';
|
|
3
|
-
import { getToken, getApiUrl } from './config.js';
|
|
3
|
+
import { getToken, getApiUrl, clearToken } from './config.js';
|
|
4
4
|
|
|
5
5
|
export const api = axios.create({
|
|
6
6
|
baseURL: getApiUrl(),
|
|
@@ -17,6 +17,28 @@ api.interceptors.request.use((config) => {
|
|
|
17
17
|
return config;
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
// Handle token expiration
|
|
21
|
+
api.interceptors.response.use(
|
|
22
|
+
(response) => response,
|
|
23
|
+
(error) => {
|
|
24
|
+
if (error.response?.status === 401) {
|
|
25
|
+
const errorMsg = error.response?.data?.error;
|
|
26
|
+
if (errorMsg === 'token_expired') {
|
|
27
|
+
console.error('\n❌ Your session has expired (tokens are valid for 3 days).');
|
|
28
|
+
console.error('Please run "envault login" to authenticate again.\n');
|
|
29
|
+
clearToken();
|
|
30
|
+
process.exit(1);
|
|
31
|
+
} else if (errorMsg === 'Invalid token' || errorMsg === 'Missing or invalid authorization header') {
|
|
32
|
+
console.error('\n❌ Authentication required.');
|
|
33
|
+
console.error('Please run "envault login" to authenticate.\n');
|
|
34
|
+
clearToken();
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Promise.reject(error);
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
20
42
|
export function handleApiError(error) {
|
|
21
43
|
if (error.response) {
|
|
22
44
|
return error.response.data.error || error.response.statusText;
|