keyra-cli 0.1.0 → 0.1.2

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.
@@ -1,11 +1,11 @@
1
1
  {
2
- "name": "envpilot",
2
+ "name": "keyra-cli",
3
3
  "version": "0.1.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
- "name": "envpilot",
8
+ "name": "keyra-cli",
9
9
  "version": "0.1.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
@@ -17,7 +17,7 @@
17
17
  "ora": "^9.3.0"
18
18
  },
19
19
  "bin": {
20
- "envpilot": "dist/index.js"
20
+ "keyra": "dist/index.js"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/inquirer": "^9.0.9",
@@ -1209,6 +1209,7 @@
1209
1209
  "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
1210
1210
  "devOptional": true,
1211
1211
  "license": "MIT",
1212
+ "peer": true,
1212
1213
  "dependencies": {
1213
1214
  "undici-types": "~7.16.0"
1214
1215
  }
@@ -1571,6 +1572,7 @@
1571
1572
  "dev": true,
1572
1573
  "hasInstallScript": true,
1573
1574
  "license": "MIT",
1575
+ "peer": true,
1574
1576
  "bin": {
1575
1577
  "esbuild": "bin/esbuild"
1576
1578
  },
@@ -2044,6 +2046,7 @@
2044
2046
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
2045
2047
  "dev": true,
2046
2048
  "license": "MIT",
2049
+ "peer": true,
2047
2050
  "engines": {
2048
2051
  "node": ">=12"
2049
2052
  },
@@ -2489,6 +2492,7 @@
2489
2492
  "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
2490
2493
  "dev": true,
2491
2494
  "license": "Apache-2.0",
2495
+ "peer": true,
2492
2496
  "bin": {
2493
2497
  "tsc": "bin/tsc",
2494
2498
  "tsserver": "bin/tsserver"
package/cli/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
- "name": "keyra",
3
- "version": "0.1.0",
2
+ "name": "keyra-cli",
3
+ "version": "0.1.2",
4
4
  "description": "Encrypted .env vault. Sync, share, and never lose an API key.",
5
5
  "bin": {
6
- "keyra": "./dist/index.js"
6
+ "keyra-cli": "./dist/index.js"
7
7
  },
8
+ "files": [
9
+ "dist"
10
+ ],
8
11
  "scripts": {
9
12
  "build": "tsup src/index.ts --format esm --target node18 --clean --shims",
10
- "dev": "tsup src/index.ts --format esm --target node18 --watch --shims"
13
+ "dev": "tsup src/index.ts --format esm --target node18 --watch --shims",
14
+ "prepublishOnly": "npm run build"
11
15
  },
12
16
  "keywords": [
13
17
  "env",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keyra-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "scripts": {
5
5
  "dev": "next dev",
6
6
  "build": "next build",
@@ -1,89 +0,0 @@
1
- import { Command } from 'commander';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import chalk from 'chalk';
5
- import { success, error, warning, printBox } from '../lib/ui.js';
6
-
7
- const HOOK_MARKER = '# keyra-guard';
8
- const PRE_COMMIT_HOOK = `#!/bin/sh
9
- ${HOOK_MARKER}
10
- # Keyra Git Guardian — blocks accidental .env commits
11
- # https://keyra.dev
12
-
13
- staged=$(git diff --cached --name-only 2>/dev/null)
14
-
15
- for file in $staged; do
16
- if echo "$file" | grep -qE '(^|\\.)\\.env(\\..*)?$'; then
17
- echo ""
18
- echo " \\033[0;31m✗ Keyra Git Guardian blocked your commit\\033[0m"
19
- echo " \\033[0;33m⚠ Detected .env file in staged changes: $file\\033[0m"
20
- echo ""
21
- echo " Commit secrets to your Keyra vault instead:"
22
- echo " keyra push"
23
- echo ""
24
- exit 1
25
- fi
26
- done
27
- `;
28
-
29
- export const guardCommand = new Command('guard')
30
- .description('Install a pre-commit hook to block accidental .env commits')
31
- .action(async () => {
32
- // Find .git directory
33
- let gitDir: string | null = null;
34
- let dir = process.cwd();
35
- for (let i = 0; i < 10; i++) {
36
- const candidate = path.join(dir, '.git');
37
- if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
38
- gitDir = candidate;
39
- break;
40
- }
41
- const parent = path.dirname(dir);
42
- if (parent === dir) break;
43
- dir = parent;
44
- }
45
-
46
- if (!gitDir) {
47
- error('Not inside a git repository.');
48
- process.exit(1);
49
- }
50
-
51
- const hooksDir = path.join(gitDir, 'hooks');
52
- if (!fs.existsSync(hooksDir)) {
53
- fs.mkdirSync(hooksDir, { recursive: true });
54
- }
55
-
56
- const hookPath = path.join(hooksDir, 'pre-commit');
57
-
58
- // Check if already installed
59
- if (fs.existsSync(hookPath)) {
60
- const existing = fs.readFileSync(hookPath, 'utf-8');
61
- if (existing.includes(HOOK_MARKER)) {
62
- warning('Git Guardian is already installed in this repository.');
63
- return;
64
- }
65
- // Append to existing hook
66
- fs.appendFileSync(hookPath, '\n' + PRE_COMMIT_HOOK);
67
- success('Git Guardian appended to existing pre-commit hook.');
68
- } else {
69
- fs.writeFileSync(hookPath, PRE_COMMIT_HOOK);
70
- }
71
-
72
- // Make executable (no-op on Windows, but harmless)
73
- try {
74
- fs.chmodSync(hookPath, 0o755);
75
- } catch {
76
- // Windows doesn't support chmod
77
- }
78
-
79
- printBox(
80
- chalk.green.bold('Git Guardian installed') +
81
- '\n\n' +
82
- chalk.dim('Pre-commit hook added to: ') +
83
- chalk.white(hookPath) +
84
- '\n\n' +
85
- chalk.dim('Now if you accidentally stage a .env file,\n') +
86
- chalk.dim('git commit will be blocked automatically.'),
87
- 'green'
88
- );
89
- });
@@ -1,172 +0,0 @@
1
- import { Command } from 'commander';
2
- import path from 'path';
3
- import inquirer from 'inquirer';
4
- import chalk from 'chalk';
5
- import {
6
- requireAuth,
7
- hasProjectConfig,
8
- loadProjectConfig,
9
- saveProjectConfig,
10
- addToGitignore,
11
- } from '../lib/config.js';
12
- import { createProject, upsertVaultEntries } from '../lib/api.js';
13
- import { readEnvFile } from '../lib/env-file.js';
14
- import { encrypt } from '../lib/encryption.js';
15
- import { success, error, info, spinner, printBox } from '../lib/ui.js';
16
-
17
- export const initCommand = new Command('init')
18
- .description('Link current directory to a Keyra project')
19
- .action(async () => {
20
- let config;
21
- try {
22
- config = requireAuth();
23
- } catch (err: any) {
24
- error(err.message);
25
- return;
26
- }
27
-
28
- if (hasProjectConfig()) {
29
- const existing = loadProjectConfig();
30
- info(
31
- `This project is already linked to vault project: ${chalk.bold(existing?.projectName)}`
32
- );
33
- info('Run `keyra push` or `keyra pull` to sync.');
34
- return;
35
- }
36
-
37
- const folderName = path.basename(process.cwd());
38
-
39
- const { name } = await inquirer.prompt([
40
- {
41
- type: 'input',
42
- name: 'name',
43
- message: 'Project name:',
44
- default: folderName,
45
- validate: (input: string) =>
46
- input.trim().length > 0 || 'Name is required',
47
- },
48
- ]);
49
-
50
- const s = spinner('Creating project...');
51
- s.start();
52
-
53
- let project;
54
- try {
55
- project = await createProject(name.trim());
56
- s.stop();
57
- success(`Project "${project.name}" created`);
58
- } catch (err: any) {
59
- s.stop();
60
- error(err.message);
61
- return;
62
- }
63
-
64
- // Check for existing .env file
65
- const envData = readEnvFile();
66
- if (envData) {
67
- const varCount = Object.keys(envData.vars).length;
68
- const { importVars } = await inquirer.prompt([
69
- {
70
- type: 'confirm',
71
- name: 'importVars',
72
- message: `Found ${envData.filename} with ${varCount} variable${varCount !== 1 ? 's' : ''}. Import them to your vault?`,
73
- default: true,
74
- },
75
- ]);
76
-
77
- if (importVars) {
78
- const uploadSpinner = spinner('Encrypting and uploading...');
79
- uploadSpinner.start();
80
-
81
- try {
82
- const entries = Object.entries(envData.vars).map(([key, value]) => {
83
- const encrypted = encrypt(value, config.passphrase);
84
- return {
85
- key_name: key,
86
- encrypted_value: encrypted.encrypted,
87
- iv: encrypted.iv,
88
- salt: encrypted.salt,
89
- auth_tag: encrypted.authTag,
90
- category: guessCategory(key),
91
- };
92
- });
93
-
94
- await upsertVaultEntries(project.id, entries);
95
- uploadSpinner.stop();
96
- success(`Imported ${entries.length} variable${entries.length !== 1 ? 's' : ''} to vault`);
97
- } catch (err: any) {
98
- uploadSpinner.stop();
99
- error(`Failed to import: ${err.message}`);
100
- }
101
- }
102
- }
103
-
104
- saveProjectConfig({
105
- projectId: project.id,
106
- projectName: project.name,
107
- });
108
-
109
- addToGitignore('.keyra.json');
110
-
111
- printBox(
112
- chalk.green.bold(`Project "${project.name}" created and linked.`) +
113
- '\n\n' +
114
- chalk.dim('Use ') +
115
- chalk.white('keyra push') +
116
- chalk.dim(' and ') +
117
- chalk.white('keyra pull') +
118
- chalk.dim(' to sync.')
119
- );
120
- });
121
-
122
- function guessCategory(key: string): string {
123
- const k = key.toUpperCase();
124
- if (k.includes('OPENAI') || k.includes('ANTHROPIC') || k.includes('AI'))
125
- return 'ai';
126
- if (
127
- k.includes('DATABASE') ||
128
- k.includes('DB_') ||
129
- k.includes('POSTGRES') ||
130
- k.includes('MYSQL') ||
131
- k.includes('MONGO') ||
132
- k.includes('REDIS') ||
133
- k.includes('SUPABASE')
134
- )
135
- return 'database';
136
- if (
137
- k.includes('AUTH') ||
138
- k.includes('JWT') ||
139
- k.includes('SESSION') ||
140
- k.includes('NEXTAUTH') ||
141
- k.includes('CLERK')
142
- )
143
- return 'auth';
144
- if (k.includes('STRIPE') || k.includes('PAYMENT') || k.includes('PAYPAL'))
145
- return 'payment';
146
- if (
147
- k.includes('VERCEL') ||
148
- k.includes('AWS') ||
149
- k.includes('GCP') ||
150
- k.includes('AZURE') ||
151
- k.includes('HEROKU') ||
152
- k.includes('DEPLOY')
153
- )
154
- return 'hosting';
155
- if (
156
- k.includes('SMTP') ||
157
- k.includes('EMAIL') ||
158
- k.includes('SENDGRID') ||
159
- k.includes('RESEND') ||
160
- k.includes('MAILGUN')
161
- )
162
- return 'email';
163
- if (
164
- k.includes('ANALYTICS') ||
165
- k.includes('MIXPANEL') ||
166
- k.includes('GA_') ||
167
- k.includes('POSTHOG') ||
168
- k.includes('SENTRY')
169
- )
170
- return 'analytics';
171
- return 'general';
172
- }
@@ -1,60 +0,0 @@
1
- import { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import { requireAuth } from '../lib/config.js';
4
- import { getProjects } from '../lib/api.js';
5
- import { error, info, spinner } from '../lib/ui.js';
6
-
7
- export const listCommand = new Command('list')
8
- .alias('ls')
9
- .description('List all your projects')
10
- .action(async () => {
11
- try {
12
- requireAuth();
13
- } catch (err: any) {
14
- error(err.message);
15
- return;
16
- }
17
-
18
- const s = spinner('Loading projects...');
19
- s.start();
20
-
21
- try {
22
- const projects = await getProjects();
23
- s.stop();
24
-
25
- if (projects.length === 0) {
26
- info('No projects yet. Run `keyra init` in a project folder.');
27
- return;
28
- }
29
-
30
- console.log();
31
- console.log(chalk.bold(` ${projects.length} project${projects.length !== 1 ? 's' : ''}:`));
32
- console.log();
33
-
34
- // Calculate column widths
35
- const maxName = Math.max(...projects.map((p) => p.name.length), 4);
36
-
37
- // Header
38
- console.log(
39
- chalk.dim(
40
- ` ${'Name'.padEnd(maxName + 2)}${'Vars'.padEnd(8)}Last Synced`
41
- )
42
- );
43
- console.log(chalk.dim(` ${'─'.repeat(maxName + 2 + 8 + 20)}`));
44
-
45
- for (const project of projects) {
46
- const name = chalk.green(project.name.padEnd(maxName + 2));
47
- const vars = String(project.var_count).padEnd(8);
48
- const synced = project.last_synced_at
49
- ? new Date(project.last_synced_at).toLocaleDateString()
50
- : chalk.dim('never');
51
-
52
- console.log(` ${name}${vars}${synced}`);
53
- }
54
-
55
- console.log();
56
- } catch (err: any) {
57
- s.stop();
58
- error(err.message);
59
- }
60
- });
@@ -1,116 +0,0 @@
1
- import { Command } from 'commander';
2
- import inquirer from 'inquirer';
3
- import open from 'open';
4
- import chalk from 'chalk';
5
- import { createDeviceCode, pollAuth } from '../lib/api.js';
6
- import { saveConfig } from '../lib/config.js';
7
- import { banner, spinner, success, error, printBox } from '../lib/ui.js';
8
-
9
- export const loginCommand = new Command('login')
10
- .description('Authenticate with Keyra via browser')
11
- .option('--api-url <url>', 'API URL', 'https://keyra.dev')
12
- .action(async (options) => {
13
- banner();
14
-
15
- const baseUrl = options.apiUrl;
16
- saveConfig({ apiUrl: baseUrl });
17
-
18
- const s = spinner('Creating device code...');
19
- s.start();
20
-
21
- let deviceCode: string;
22
- try {
23
- const result = await createDeviceCode();
24
- deviceCode = result.deviceCode;
25
- s.stop();
26
- } catch (err: any) {
27
- s.stop();
28
- error(err.message);
29
- return;
30
- }
31
- const authUrl = `${baseUrl}/cli/auth?code=${deviceCode}`;
32
-
33
- console.log();
34
- console.log(chalk.dim(' Opening browser to authenticate...'));
35
- console.log(chalk.dim(` ${authUrl}`));
36
- console.log();
37
-
38
- try {
39
- await open(authUrl);
40
- } catch {
41
- console.log(chalk.yellow(' Could not open browser automatically.'));
42
- console.log(chalk.yellow(` Open this URL manually: ${authUrl}`));
43
- }
44
-
45
- const pollSpinner = spinner('Waiting for you to sign in...');
46
- pollSpinner.start();
47
-
48
- const maxAttempts = 150; // 5 minutes at 2 second intervals
49
- let accessToken: string | undefined;
50
-
51
- for (let i = 0; i < maxAttempts; i++) {
52
- await new Promise((resolve) => setTimeout(resolve, 2000));
53
-
54
- try {
55
- const result = await pollAuth(deviceCode);
56
- if (result.status === 'authorized' && result.accessToken) {
57
- accessToken = result.accessToken;
58
- break;
59
- }
60
- if (result.status === 'expired') {
61
- pollSpinner.stop();
62
- error('Authentication timed out. Please try again.');
63
- return;
64
- }
65
- } catch {
66
- // Polling error, continue
67
- }
68
- }
69
-
70
- pollSpinner.stop();
71
-
72
- if (!accessToken) {
73
- error('Authentication timed out. Please try again.');
74
- return;
75
- }
76
-
77
- success('Signed in successfully!');
78
- console.log();
79
-
80
- const { passphrase } = await inquirer.prompt([
81
- {
82
- type: 'password',
83
- name: 'passphrase',
84
- message:
85
- 'Enter an encryption passphrase (this encrypts your secrets locally — remember it!):',
86
- mask: '*',
87
- validate: (input: string) =>
88
- input.length >= 4 || 'Passphrase must be at least 4 characters',
89
- },
90
- ]);
91
-
92
- const { confirm } = await inquirer.prompt([
93
- {
94
- type: 'password',
95
- name: 'confirm',
96
- message: 'Confirm passphrase:',
97
- mask: '*',
98
- validate: (input: string) =>
99
- input === passphrase || 'Passphrases do not match',
100
- },
101
- ]);
102
-
103
- saveConfig({
104
- apiUrl: baseUrl,
105
- accessToken,
106
- passphrase,
107
- });
108
-
109
- printBox(
110
- chalk.green.bold('You\'re logged in!') +
111
- '\n\n' +
112
- chalk.dim('Run ') +
113
- chalk.white('keyra init') +
114
- chalk.dim(' in a project to get started.')
115
- );
116
- });
@@ -1,10 +0,0 @@
1
- import { Command } from 'commander';
2
- import { clearConfig } from '../lib/config.js';
3
- import { success } from '../lib/ui.js';
4
-
5
- export const logoutCommand = new Command('logout')
6
- .description('Clear stored credentials')
7
- .action(() => {
8
- clearConfig();
9
- success('Logged out. Your vault data is still safe in the cloud.');
10
- });
@@ -1,94 +0,0 @@
1
- import { Command } from 'commander';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import chalk from 'chalk';
5
- import { requireAuth, requireProjectConfig } from '../lib/config.js';
6
- import { getVaultEntries, getProjects } from '../lib/api.js';
7
- import { decrypt } from '../lib/encryption.js';
8
- import { writeEnvFile } from '../lib/env-file.js';
9
- import { success, error, warning, spinner, promptSecret } from '../lib/ui.js';
10
-
11
- export const pullCommand = new Command('pull')
12
- .description('Pull vault variables to local .env')
13
- .option('-f, --file <filename>', 'Output filename', '.env')
14
- .action(async (options) => {
15
- let config;
16
- try {
17
- config = requireAuth();
18
- } catch (err: any) {
19
- error(err.message);
20
- return;
21
- }
22
-
23
- let projectConfig;
24
- try {
25
- projectConfig = requireProjectConfig();
26
- } catch (err: any) {
27
- error(err.message);
28
- return;
29
- }
30
-
31
- // Check if project has a password
32
- let projectPassword: string | null = null;
33
- try {
34
- const projects = await getProjects();
35
- const project = projects.find((p) => p.id === projectConfig.projectId);
36
- if (project?.has_project_password) {
37
- projectPassword = await promptSecret('Project password: ');
38
- const blob = JSON.parse(project.project_password_salt!) as { encrypted: string; iv: string; salt: string; authTag: string };
39
- try {
40
- const result = decrypt(blob.encrypted, blob.iv, blob.salt, blob.authTag, projectPassword);
41
- if (result !== 'keyra-verified') {
42
- error('Incorrect project password');
43
- return;
44
- }
45
- } catch {
46
- error('Incorrect project password');
47
- return;
48
- }
49
- }
50
- } catch (err: any) {
51
- error('Failed to fetch project info: ' + err.message);
52
- return;
53
- }
54
-
55
- const s = spinner('Pulling from vault...');
56
- s.start();
57
-
58
- try {
59
- const entries = await getVaultEntries(projectConfig.projectId);
60
-
61
- if (entries.length === 0) {
62
- s.stop();
63
- warning('No variables found in vault. Push some first with `keyra push`.');
64
- return;
65
- }
66
-
67
- const vars: Record<string, string> = {};
68
- for (const entry of entries) {
69
- if (projectPassword) {
70
- // Double decrypt: outer with vault passphrase, inner with project password
71
- const innerJson = decrypt(entry.encrypted_value, entry.iv, entry.salt, entry.auth_tag, config.passphrase);
72
- const inner = JSON.parse(innerJson) as { encrypted: string; iv: string; salt: string; authTag: string };
73
- vars[entry.key_name] = decrypt(inner.encrypted, inner.iv, inner.salt, inner.authTag, projectPassword);
74
- } else {
75
- vars[entry.key_name] = decrypt(entry.encrypted_value, entry.iv, entry.salt, entry.auth_tag, config.passphrase);
76
- }
77
- }
78
-
79
- const existed = fs.existsSync(path.join(process.cwd(), options.file));
80
- writeEnvFile(vars, undefined, options.file);
81
-
82
- s.stop();
83
- success(
84
- 'Pulled ' + entries.length + ' variable' + (entries.length !== 1 ? 's' : '') + ' from vault -> ' + options.file + ' ' + chalk.dim('(' + projectConfig.projectName + ')')
85
- );
86
-
87
- if (existed) {
88
- warning('Existing ' + options.file + ' was overwritten');
89
- }
90
- } catch (err: any) {
91
- s.stop();
92
- error(err.message);
93
- }
94
- });