@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 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
@@ -1,9 +1,5 @@
1
1
  # Envault CLI
2
2
 
3
- <p align="center">
4
- <img src="https://envault.tech/logo.png" alt="Envault Logo" width="100" />
5
- </p>
6
-
7
3
  <p align="center">
8
4
  <b>Secure Environment Variable Management for Modern Teams</b>
9
5
  </p>
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('1.0.0')
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.2",
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
  ],
@@ -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
- const envPath = '.env';
25
- if (!fs.existsSync(envPath)) {
26
- console.error(chalk.red('Error: .env file not found.'));
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(`✔ Successfully deployed ${secrets.length} secrets!`));
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)));
@@ -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. Create one in the dashboard first.'));
47
- return;
46
+ console.log(chalk.yellow('No existing projects found.'));
48
47
  }
49
48
 
50
- const { projectId } = await inquirer.prompt([{
49
+ const { selectedProjectId } = await inquirer.prompt([{
51
50
  type: 'list',
52
- name: 'projectId',
51
+ name: 'selectedProjectId',
53
52
  message: 'Select the project to link:',
54
- choices: projects.map(p => ({ name: p.name, value: p.id }))
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
- fs.writeFileSync('envault.json', JSON.stringify({ projectId }, null, 2));
58
- console.log(chalk.green(`\n✔ Project linked! (ID: ${projectId})`));
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
  }
@@ -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));
@@ -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: 'This will overwrite your current .env file. Continue?',
64
+ message: 'Are you sure you want to overwrite your local .env file?',
29
65
  default: false
30
66
  }]);
31
- if (!confirm) return;
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(`✔ Pulled ${data.secrets.length} secrets to .env`));
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;