@fentz26/envcp 1.2.0-exp.92feaad → 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/README.md +21 -36
- package/VERSION +1 -1
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +43 -23
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/gemini.d.ts +5 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +27 -9
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/openai.d.ts +5 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +36 -12
- package/dist/adapters/openai.js.map +1 -1
- package/dist/adapters/rest.d.ts +1 -0
- package/dist/adapters/rest.d.ts.map +1 -1
- package/dist/adapters/rest.js +7 -3
- package/dist/adapters/rest.js.map +1 -1
- package/dist/cli/index.js +1328 -336
- package/dist/cli/index.js.map +1 -1
- package/dist/config/config-hmac.d.ts.map +1 -1
- package/dist/config/config-hmac.js +5 -1
- package/dist/config/config-hmac.js.map +1 -1
- package/dist/config/manager.d.ts +12 -2
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +169 -87
- package/dist/config/manager.js.map +1 -1
- package/dist/server/unified.d.ts +1 -0
- package/dist/server/unified.d.ts.map +1 -1
- package/dist/server/unified.js +53 -29
- package/dist/server/unified.js.map +1 -1
- package/dist/service/config.d.ts.map +1 -1
- package/dist/service/config.js +1 -7
- package/dist/service/config.js.map +1 -1
- package/dist/types.d.ts +265 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/crypto.d.ts +5 -3
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +5 -6
- package/dist/utils/crypto.js.map +1 -1
- package/dist/utils/fs.d.ts +11 -1
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +20 -11
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/hsm.d.ts.map +1 -1
- package/dist/utils/hsm.js +6 -2
- package/dist/utils/hsm.js.map +1 -1
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/http.js +8 -3
- package/dist/utils/http.js.map +1 -1
- package/dist/utils/prompt.d.ts +11 -0
- package/dist/utils/prompt.d.ts.map +1 -1
- package/dist/utils/prompt.js +239 -2
- package/dist/utils/prompt.js.map +1 -1
- package/dist/utils/session.d.ts +3 -1
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +38 -16
- package/dist/utils/session.js.map +1 -1
- package/package.json +2 -2
- package/scripts/sync-version.js +3 -3
package/dist/cli/index.js
CHANGED
|
@@ -3,9 +3,9 @@ import chalk from 'chalk';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import * as fs from 'fs/promises';
|
|
6
|
-
import { promptPassword, promptInput, promptConfirm,
|
|
7
|
-
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
8
|
-
import { loadConfig, initConfig, saveConfig, parseEnvFile, registerMcpConfig, isBlacklisted, canAccess } from '../config/manager.js';
|
|
6
|
+
import { promptPassword, promptInput, promptConfirm, promptMenu, promptTabbedMenu } from '../utils/prompt.js';
|
|
7
|
+
import { ensureDir, pathExists, findProjectRoot } from '../utils/fs.js';
|
|
8
|
+
import { loadConfig, loadScopedConfig, initConfig, saveConfig, saveScopedConfig, parseEnvFile, registerMcpConfig, isBlacklisted, canAccess } from '../config/manager.js';
|
|
9
9
|
import { ConfigGuard } from '../config/config-guard.js';
|
|
10
10
|
import { StorageManager, LogManager, resolveLogPath } from '../storage/index.js';
|
|
11
11
|
import { VERSION } from '../version.js';
|
|
@@ -14,11 +14,12 @@ import { maskValue, validatePassword, encrypt, decrypt, generateRecoveryKey, cre
|
|
|
14
14
|
import { KeychainManager } from '../utils/keychain.js';
|
|
15
15
|
import { HsmManager } from '../utils/hsm.js';
|
|
16
16
|
import { checkForUpdate, formatUpdateMessage, logUpdateCheck, fetchReleases, filterByChannel } from '../utils/update-checker.js';
|
|
17
|
-
import { spawnSync } from 'child_process';
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
18
|
import { LockoutManager } from '../utils/lockout.js';
|
|
19
|
-
import { initMemoryProtection } from '../utils/secure-memory.js';
|
|
19
|
+
import { initMemoryProtection, secureCompare } from '../utils/secure-memory.js';
|
|
20
20
|
import { getGlobalVaultPath, getProjectVaultPath, resolveVaultPath, resolveSessionPath, setActiveVault, listVaults, initNamedVault, } from '../vault/index.js';
|
|
21
|
-
import {
|
|
21
|
+
import { loadServiceConfig, saveServiceConfig } from '../service/config.js';
|
|
22
|
+
import { installService, statusService, uninstallService } from '../service/index.js';
|
|
22
23
|
initMemoryProtection();
|
|
23
24
|
async function resolveCliContext(vaultOverride) {
|
|
24
25
|
if (vaultOverride === 'global') {
|
|
@@ -66,223 +67,342 @@ function buildSecuritySettings(securityChoice) {
|
|
|
66
67
|
},
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
function extractTransferVariables(data, invalidMessage) {
|
|
71
|
+
const variables = data.variables;
|
|
72
|
+
if (!variables || typeof variables !== 'object') {
|
|
73
|
+
console.log(chalk.red(invalidMessage));
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
meta: data.meta,
|
|
78
|
+
variables: variables,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function logTransferInfo(title, meta, variables, labels) {
|
|
82
|
+
if (!meta) {
|
|
81
83
|
return;
|
|
82
84
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
console.log(chalk.blue(title));
|
|
86
|
+
if (meta.project)
|
|
87
|
+
console.log(chalk.gray(` ${labels.project}: ${meta.project}`));
|
|
88
|
+
if (meta.timestamp)
|
|
89
|
+
console.log(chalk.gray(` ${labels.timestamp}: ${meta.timestamp}`));
|
|
90
|
+
console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
|
|
91
|
+
}
|
|
92
|
+
async function getAuthPassword(projectPath, config) {
|
|
89
93
|
let password = '';
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (factors.includes('password')) {
|
|
103
|
-
userPassword = await promptPassword('Enter password (multi-factor):');
|
|
104
|
-
}
|
|
105
|
-
else if (factors.includes('keychain')) {
|
|
106
|
-
const keychain = new KeychainManager(config.keychain?.service || 'envcp');
|
|
107
|
-
const stored = await keychain.retrievePassword(projectPath);
|
|
108
|
-
if (stored) {
|
|
109
|
-
userPassword = stored;
|
|
110
|
-
console.log(chalk.gray('Password retrieved from OS keychain (multi-factor)'));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
password = HsmManager.combineSecrets(hsmSecret, userPassword);
|
|
114
|
-
console.log(chalk.gray(`Authenticated via ${hsm.backendName} + password`));
|
|
94
|
+
const authMethod = config.auth?.method ?? 'password';
|
|
95
|
+
if (authMethod === 'hsm' || authMethod === 'multi') {
|
|
96
|
+
const hsm = HsmManager.fromConfig(config, projectPath);
|
|
97
|
+
const hsmAvailable = await hsm.isAvailable();
|
|
98
|
+
if (hsmAvailable) {
|
|
99
|
+
try {
|
|
100
|
+
const hsmSecret = await hsm.retrieveVaultPassword();
|
|
101
|
+
if (authMethod === 'multi') {
|
|
102
|
+
const factors = config.auth?.multi_factors ?? ['password', 'hsm'];
|
|
103
|
+
let userPassword = '';
|
|
104
|
+
if (factors.includes('password')) {
|
|
105
|
+
userPassword = await promptPassword('Enter password (multi-factor):');
|
|
115
106
|
}
|
|
116
|
-
else {
|
|
117
|
-
|
|
118
|
-
|
|
107
|
+
else if (factors.includes('keychain')) {
|
|
108
|
+
const keychain = new KeychainManager(config.keychain?.service || 'envcp');
|
|
109
|
+
const stored = await keychain.retrievePassword(projectPath);
|
|
110
|
+
if (stored) {
|
|
111
|
+
userPassword = stored;
|
|
112
|
+
console.log(chalk.gray('Password retrieved from OS keychain (multi-factor)'));
|
|
113
|
+
}
|
|
119
114
|
}
|
|
115
|
+
password = HsmManager.combineSecrets(hsmSecret, userPassword);
|
|
116
|
+
console.log(chalk.gray(`Authenticated via ${hsm.backendName} + password`));
|
|
120
117
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (fallback === 'none') {
|
|
125
|
-
console.log(chalk.red(`HSM authentication failed: ${msg}`));
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
console.log(chalk.yellow(`HSM unavailable: ${msg}`));
|
|
129
|
-
console.log(chalk.gray('Falling back to password...'));
|
|
118
|
+
else {
|
|
119
|
+
password = hsmSecret;
|
|
120
|
+
console.log(chalk.gray(`Authenticated via ${hsm.backendName}`));
|
|
130
121
|
}
|
|
131
122
|
}
|
|
132
|
-
|
|
123
|
+
catch (err) {
|
|
124
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
133
125
|
const fallback = config.auth?.fallback ?? 'password';
|
|
134
126
|
if (fallback === 'none') {
|
|
135
|
-
console.log(chalk.red(`HSM
|
|
136
|
-
return;
|
|
127
|
+
console.log(chalk.red(`HSM authentication failed: ${msg}`));
|
|
128
|
+
return null;
|
|
137
129
|
}
|
|
138
|
-
console.log(chalk.yellow(`HSM
|
|
130
|
+
console.log(chalk.yellow(`HSM unavailable: ${msg}`));
|
|
131
|
+
console.log(chalk.gray('Falling back to password...'));
|
|
139
132
|
}
|
|
140
133
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
password = stored;
|
|
147
|
-
console.log(chalk.gray('Password retrieved from OS keychain'));
|
|
134
|
+
else {
|
|
135
|
+
const fallback = config.auth?.fallback ?? 'password';
|
|
136
|
+
if (fallback === 'none') {
|
|
137
|
+
console.log(chalk.red(`HSM device (${hsm.backendName}) not found. No fallback configured.`));
|
|
138
|
+
return null;
|
|
148
139
|
}
|
|
140
|
+
console.log(chalk.yellow(`HSM device (${hsm.backendName}) not available. Falling back to password...`));
|
|
149
141
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
142
|
+
}
|
|
143
|
+
if (!password && (authMethod === 'keychain' || config.keychain?.enabled)) {
|
|
144
|
+
const keychain = new KeychainManager(config.keychain?.service || 'envcp');
|
|
145
|
+
const stored = await keychain.retrievePassword(projectPath);
|
|
146
|
+
if (stored) {
|
|
147
|
+
password = stored;
|
|
148
|
+
console.log(chalk.gray('Password retrieved from OS keychain'));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!password) {
|
|
152
|
+
password = await promptPassword('Enter password:');
|
|
153
|
+
const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
|
|
154
|
+
if (!passwordValid) {
|
|
155
|
+
console.log(chalk.red('Invalid password'));
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (passwordWarning) {
|
|
159
|
+
console.log(chalk.yellow('⚠ Weak password detected'));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return password;
|
|
163
|
+
}
|
|
164
|
+
async function withSession(fn, vaultOverride) {
|
|
165
|
+
const { projectPath, config } = await resolveCliContext(vaultOverride);
|
|
166
|
+
const vaultPath = vaultOverride
|
|
167
|
+
? vaultOverride === 'global'
|
|
168
|
+
? getGlobalVaultPath(config)
|
|
169
|
+
: getProjectVaultPath(projectPath, config)
|
|
170
|
+
: await resolveVaultPath(projectPath, config);
|
|
171
|
+
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
172
|
+
await logManager.init();
|
|
173
|
+
if (config.encryption?.enabled === false) {
|
|
174
|
+
const storage = new StorageManager(vaultPath, false);
|
|
175
|
+
await fn(storage, '', config, projectPath, logManager);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
179
|
+
await sessionManager.init();
|
|
180
|
+
let password = '';
|
|
181
|
+
if (config.session?.enabled !== false) {
|
|
182
|
+
let session = await sessionManager.load();
|
|
183
|
+
if (!session) {
|
|
184
|
+
const authPassword = await getAuthPassword(projectPath, config);
|
|
185
|
+
if (!authPassword) {
|
|
156
186
|
return;
|
|
157
187
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
188
|
+
password = authPassword;
|
|
189
|
+
session = await sessionManager.create(password);
|
|
161
190
|
}
|
|
162
|
-
|
|
191
|
+
password = sessionManager.getPassword() || password;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const authPassword = await getAuthPassword(projectPath, config);
|
|
195
|
+
if (!authPassword) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
password = authPassword;
|
|
199
|
+
await sessionManager.destroy();
|
|
163
200
|
}
|
|
164
|
-
password = sessionManager.getPassword() || password;
|
|
165
201
|
const storage = new StorageManager(vaultPath, config.storage.encrypted);
|
|
166
202
|
if (password)
|
|
167
203
|
storage.setPassword(password);
|
|
168
204
|
await fn(storage, password, config, projectPath, logManager);
|
|
169
205
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
206
|
+
function isInteractiveCli() {
|
|
207
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
208
|
+
}
|
|
209
|
+
async function isProjectInitialized(projectPath = process.cwd()) {
|
|
210
|
+
return pathExists(path.join(projectPath, 'envcp.yaml'));
|
|
211
|
+
}
|
|
212
|
+
async function chooseInitMode() {
|
|
213
|
+
if (!isInteractiveCli()) {
|
|
214
|
+
return 'basic';
|
|
215
|
+
}
|
|
216
|
+
return promptMenu('Set up EnvCP', [
|
|
217
|
+
{ label: 'Basic', value: 'basic', hint: '(quick, simple, low-tech)' },
|
|
218
|
+
{ label: 'Advanced', value: 'advanced', hint: '(guided, more choices)' },
|
|
219
|
+
{ label: 'Manual', value: 'manual', hint: '(full control)' },
|
|
220
|
+
], 'basic');
|
|
221
|
+
}
|
|
222
|
+
async function chooseSecurityMode() {
|
|
223
|
+
if (!isInteractiveCli()) {
|
|
224
|
+
return 'recoverable';
|
|
225
|
+
}
|
|
226
|
+
return promptMenu('How would you like to secure your variables?', [
|
|
227
|
+
{ label: 'No encryption', value: 'none', hint: '(fastest local setup)' },
|
|
228
|
+
{ label: 'Encrypted with recovery key', value: 'recoverable', hint: '(recommended)' },
|
|
229
|
+
{ label: 'Encrypted hard-lock', value: 'hard-lock', hint: '(max security)' },
|
|
230
|
+
], 'recoverable');
|
|
231
|
+
}
|
|
232
|
+
async function chooseServerMode() {
|
|
233
|
+
if (!isInteractiveCli()) {
|
|
234
|
+
return 'auto';
|
|
235
|
+
}
|
|
236
|
+
return promptMenu('Background service mode', [
|
|
237
|
+
{ label: 'Auto', value: 'auto', hint: '(recommended)' },
|
|
238
|
+
{ label: 'REST', value: 'rest' },
|
|
239
|
+
{ label: 'OpenAI', value: 'openai' },
|
|
240
|
+
{ label: 'Gemini', value: 'gemini' },
|
|
241
|
+
{ label: 'All', value: 'all' },
|
|
242
|
+
], 'auto');
|
|
243
|
+
}
|
|
244
|
+
async function buildServiceSetup(projectPath, initMode) {
|
|
245
|
+
if (!isInteractiveCli()) {
|
|
246
|
+
return { enabled: false, startNow: false, config: null };
|
|
247
|
+
}
|
|
248
|
+
const wantsStartup = await promptConfirm(initMode === 'basic'
|
|
249
|
+
? 'Start EnvCP automatically in the background?'
|
|
250
|
+
: 'Install background startup for EnvCP?', true);
|
|
251
|
+
if (!wantsStartup) {
|
|
252
|
+
return { enabled: false, startNow: false, config: null };
|
|
253
|
+
}
|
|
254
|
+
const serviceConfig = await loadServiceConfig();
|
|
255
|
+
serviceConfig.working_directory = projectPath;
|
|
256
|
+
serviceConfig.autostart = true;
|
|
257
|
+
if (initMode === 'manual') {
|
|
258
|
+
serviceConfig.server.mode = await chooseServerMode();
|
|
259
|
+
const hostInput = (await promptInput(`Service host [${serviceConfig.server.host}]:`)).trim();
|
|
260
|
+
if (hostInput) {
|
|
261
|
+
serviceConfig.server.host = hostInput;
|
|
262
|
+
}
|
|
263
|
+
const portInput = (await promptInput(`Service port [${serviceConfig.server.port}]:`)).trim();
|
|
264
|
+
if (portInput) {
|
|
265
|
+
const parsedPort = Number.parseInt(portInput, 10);
|
|
266
|
+
if (!Number.isNaN(parsedPort) && parsedPort > 0) {
|
|
267
|
+
serviceConfig.server.port = parsedPort;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const apiKey = (await promptInput('HTTP API key (leave blank for local-only use):')).trim();
|
|
271
|
+
serviceConfig.server.api_key = apiKey || undefined;
|
|
272
|
+
serviceConfig.restart_on_failure = await promptConfirm('Restart service if it crashes?', true);
|
|
273
|
+
}
|
|
274
|
+
const startNow = initMode === 'basic'
|
|
275
|
+
? true
|
|
276
|
+
: await promptConfirm('Start the background service now?', true);
|
|
277
|
+
return { enabled: true, startNow, config: serviceConfig };
|
|
278
|
+
}
|
|
279
|
+
async function installStartupServiceIfNeeded(projectPath, serviceSetup) {
|
|
280
|
+
if (!serviceSetup.enabled || !serviceSetup.config) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await saveServiceConfig(serviceSetup.config);
|
|
284
|
+
const result = await installService({ workingDirectory: projectPath, now: serviceSetup.startNow });
|
|
285
|
+
if (result.ok) {
|
|
286
|
+
console.log(chalk.green('Background service configured.'));
|
|
287
|
+
console.log(chalk.gray(` ${result.message}`));
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
console.log(chalk.yellow('Background service setup could not be completed automatically.'));
|
|
291
|
+
console.log(chalk.gray(` ${result.message}`));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function summarizeServiceStatus() {
|
|
295
|
+
const serviceConfig = await loadServiceConfig();
|
|
296
|
+
const status = await statusService();
|
|
297
|
+
const active = status.ok ? 'running' : 'not running';
|
|
298
|
+
return `${active}, autostart ${serviceConfig.autostart ? 'on' : 'off'}`;
|
|
299
|
+
}
|
|
300
|
+
async function maybeImportDotEnv(projectPath, config, password, shouldImport) {
|
|
301
|
+
if (!shouldImport) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const envPath = path.join(projectPath, '.env');
|
|
305
|
+
if (!await pathExists(envPath)) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const envContent = await fs.readFile(envPath, 'utf8');
|
|
309
|
+
const vars = parseEnvFile(envContent);
|
|
310
|
+
const count = Object.keys(vars).length;
|
|
311
|
+
if (count === 0) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
315
|
+
if (password) {
|
|
316
|
+
storage.setPassword(password);
|
|
317
|
+
}
|
|
318
|
+
const now = new Date().toISOString();
|
|
319
|
+
const existing = await storage.load();
|
|
320
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
321
|
+
existing[name] = {
|
|
322
|
+
name,
|
|
323
|
+
value,
|
|
324
|
+
encrypted: config.storage.encrypted,
|
|
325
|
+
created: now,
|
|
326
|
+
updated: now,
|
|
327
|
+
sync_to_env: true,
|
|
328
|
+
protected: false,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
await storage.save(existing);
|
|
332
|
+
if (password && config.session?.enabled !== false) {
|
|
333
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
334
|
+
await sessionManager.init();
|
|
335
|
+
await sessionManager.create(password);
|
|
336
|
+
}
|
|
337
|
+
console.log(chalk.green(` Imported ${count} variables from .env`));
|
|
338
|
+
console.log(chalk.gray(` Variables: ${Object.keys(vars).join(', ')}`));
|
|
339
|
+
}
|
|
340
|
+
async function runInitFlow(options = {}) {
|
|
341
|
+
const projectPath = process.cwd();
|
|
342
|
+
const projectName = options.project || path.basename(projectPath);
|
|
193
343
|
const configPath = path.join(projectPath, 'envcp.yaml');
|
|
194
|
-
if (await pathExists(configPath)
|
|
195
|
-
|
|
196
|
-
console.log(chalk.yellow('EnvCP is already initialized here.'));
|
|
344
|
+
if (await pathExists(configPath)) {
|
|
345
|
+
console.log(chalk.yellow('EnvCP is already set up here.'));
|
|
197
346
|
console.log(chalk.gray(` Config: ${configPath}`));
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
console.log('');
|
|
202
|
-
console.log(chalk.gray('Running init again would overwrite your config and may make'));
|
|
203
|
-
console.log(chalk.gray('existing encrypted variables permanently inaccessible.'));
|
|
204
|
-
console.log('');
|
|
205
|
-
console.log(chalk.gray('To reconfigure: edit envcp.yaml directly'));
|
|
206
|
-
console.log(chalk.gray('To force re-init (DESTRUCTIVE): envcp init --force'));
|
|
207
|
-
process.exit(1);
|
|
347
|
+
console.log(chalk.gray('Run `envcp setup` to change project settings.'));
|
|
348
|
+
return;
|
|
208
349
|
}
|
|
209
|
-
console.log(chalk.blue(
|
|
350
|
+
console.log(chalk.blue('Initializing EnvCP...'));
|
|
210
351
|
console.log('');
|
|
211
|
-
const config = await initConfig(projectPath, projectName
|
|
212
|
-
|
|
352
|
+
const config = await initConfig(projectPath, projectName);
|
|
353
|
+
const initMode = await chooseInitMode();
|
|
354
|
+
const hasDotEnv = await pathExists(path.join(projectPath, '.env'));
|
|
213
355
|
let securityChoice;
|
|
214
356
|
if (options.encrypt === false) {
|
|
215
357
|
securityChoice = 'none';
|
|
216
358
|
}
|
|
359
|
+
else if (initMode === 'basic') {
|
|
360
|
+
securityChoice = isInteractiveCli() && !await promptConfirm('Protect variables with encryption?', true)
|
|
361
|
+
? 'none'
|
|
362
|
+
: 'recoverable';
|
|
363
|
+
}
|
|
217
364
|
else {
|
|
218
|
-
securityChoice = await
|
|
219
|
-
{ name: 'No encryption (fastest setup, for local dev)', value: 'none' },
|
|
220
|
-
{ name: 'Encrypted with recovery key (recommended)', value: 'recoverable' },
|
|
221
|
-
{ name: 'Encrypted hard-lock (max security, no recovery)', value: 'hard-lock' },
|
|
222
|
-
], 'recoverable');
|
|
365
|
+
securityChoice = await chooseSecurityMode();
|
|
223
366
|
}
|
|
224
367
|
const securitySettings = buildSecuritySettings(securityChoice);
|
|
225
368
|
config.encryption = securitySettings.encryption;
|
|
226
369
|
config.storage.encrypted = securitySettings.storageEncrypted;
|
|
227
370
|
config.security = securitySettings.security;
|
|
228
|
-
|
|
229
|
-
|
|
371
|
+
if (initMode === 'basic') {
|
|
372
|
+
config.session.enabled = false;
|
|
373
|
+
config.access.require_confirmation = false;
|
|
374
|
+
}
|
|
375
|
+
else if (initMode === 'advanced') {
|
|
376
|
+
config.session.enabled = securityChoice === 'none'
|
|
377
|
+
? false
|
|
378
|
+
: (!isInteractiveCli() || await promptConfirm('Keep an unlocked session between commands?', true));
|
|
379
|
+
config.access.require_confirmation = !isInteractiveCli() || await promptConfirm('Ask before risky AI actions?', true);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
config.session.enabled = securityChoice === 'none'
|
|
383
|
+
? false
|
|
384
|
+
: (!isInteractiveCli() || await promptConfirm('Enable sessions?', true));
|
|
385
|
+
config.access.require_confirmation = !isInteractiveCli() || await promptConfirm('Require confirmation for risky AI actions?', true);
|
|
386
|
+
}
|
|
387
|
+
let password = '';
|
|
230
388
|
if (securityChoice !== 'none') {
|
|
231
|
-
|
|
389
|
+
password = await promptPassword('Set encryption password:');
|
|
232
390
|
const confirmPwd = await promptPassword('Confirm password:');
|
|
233
|
-
|
|
234
|
-
if (password !== confirmPwd) {
|
|
391
|
+
if (!secureCompare(Buffer.from(password, 'utf8'), Buffer.from(confirmPwd, 'utf8'))) {
|
|
235
392
|
console.log(chalk.red('Passwords do not match. Aborting.'));
|
|
236
393
|
return;
|
|
237
394
|
}
|
|
238
|
-
pwd = password;
|
|
239
395
|
}
|
|
240
|
-
await saveConfig(config, projectPath
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// Auto-import .env
|
|
249
|
-
if (!options.skipEnv) {
|
|
250
|
-
const envPath = path.join(projectPath, '.env');
|
|
251
|
-
if (await pathExists(envPath)) {
|
|
252
|
-
const envContent = await fs.readFile(envPath, 'utf8');
|
|
253
|
-
const vars = parseEnvFile(envContent);
|
|
254
|
-
const count = Object.keys(vars).length;
|
|
255
|
-
if (count > 0) {
|
|
256
|
-
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
257
|
-
if (pwd)
|
|
258
|
-
storage.setPassword(pwd);
|
|
259
|
-
const now = new Date().toISOString();
|
|
260
|
-
const existing = await storage.load();
|
|
261
|
-
for (const [name, value] of Object.entries(vars)) {
|
|
262
|
-
existing[name] = {
|
|
263
|
-
name, value,
|
|
264
|
-
encrypted: config.storage.encrypted,
|
|
265
|
-
created: now, updated: now,
|
|
266
|
-
sync_to_env: true,
|
|
267
|
-
protected: false,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
await storage.save(existing);
|
|
271
|
-
// Create session for encrypted mode
|
|
272
|
-
if (pwd) {
|
|
273
|
-
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
274
|
-
await sessionManager.init();
|
|
275
|
-
await sessionManager.create(pwd);
|
|
276
|
-
}
|
|
277
|
-
console.log(chalk.green(` Imported ${count} variables from .env`));
|
|
278
|
-
console.log(chalk.gray(` Variables: ${Object.keys(vars).join(', ')}`));
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
// Generate recovery key for encrypted recoverable mode
|
|
283
|
-
if (securityChoice === 'recoverable' && pwd) {
|
|
396
|
+
await saveConfig(config, projectPath);
|
|
397
|
+
const shouldImportEnv = options.skipEnv
|
|
398
|
+
? false
|
|
399
|
+
: hasDotEnv && (isInteractiveCli() ? await promptConfirm('Import variables from .env?', true) : false);
|
|
400
|
+
await maybeImportDotEnv(projectPath, config, password, shouldImportEnv);
|
|
401
|
+
const serviceSetup = await buildServiceSetup(projectPath, initMode);
|
|
402
|
+
await installStartupServiceIfNeeded(projectPath, serviceSetup);
|
|
403
|
+
if (securityChoice === 'recoverable' && password) {
|
|
284
404
|
const recoveryKey = generateRecoveryKey();
|
|
285
|
-
const recoveryData = await createRecoveryData(
|
|
405
|
+
const recoveryData = await createRecoveryData(password, recoveryKey);
|
|
286
406
|
const recoveryPath = path.join(projectPath, config.security.recovery_file);
|
|
287
407
|
await fs.writeFile(recoveryPath, recoveryData, 'utf8');
|
|
288
408
|
console.log('');
|
|
@@ -290,7 +410,6 @@ program
|
|
|
290
410
|
console.log(chalk.yellow.bold(` ${recoveryKey}`));
|
|
291
411
|
console.log(chalk.gray(' This key is shown ONCE. If you lose it, you cannot recover your password.'));
|
|
292
412
|
}
|
|
293
|
-
// Auto-register MCP in all detected tools
|
|
294
413
|
if (!options.skipMcp) {
|
|
295
414
|
const result = await registerMcpConfig(projectPath);
|
|
296
415
|
console.log('');
|
|
@@ -300,24 +419,12 @@ program
|
|
|
300
419
|
console.log(chalk.gray(` + ${name}`));
|
|
301
420
|
}
|
|
302
421
|
}
|
|
303
|
-
if (result.alreadyConfigured.length > 0) {
|
|
304
|
-
for (const name of result.alreadyConfigured) {
|
|
305
|
-
console.log(chalk.gray(` = ${name} (already configured)`));
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
if (result.manual.length > 0) {
|
|
309
|
-
console.log(chalk.gray(' Manual setup needed:'));
|
|
310
|
-
for (const name of result.manual) {
|
|
311
|
-
console.log(chalk.gray(` ? ${name}`));
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
422
|
if (result.registered.length === 0 && result.alreadyConfigured.length === 0) {
|
|
315
423
|
console.log(chalk.gray(' No AI tools detected for auto-registration'));
|
|
316
424
|
}
|
|
317
425
|
}
|
|
318
|
-
// Setup HSM authentication if --auth-method hsm|multi provided
|
|
319
426
|
const authMethod = options.authMethod;
|
|
320
|
-
if (
|
|
427
|
+
if (password && (authMethod === 'hsm' || authMethod === 'multi')) {
|
|
321
428
|
const hsmType = options.hsmType || 'yubikey';
|
|
322
429
|
config.hsm = {
|
|
323
430
|
...config.hsm,
|
|
@@ -333,55 +440,649 @@ program
|
|
|
333
440
|
multi_factors: authMethod === 'multi' ? ['password', 'hsm'] : ['hsm'],
|
|
334
441
|
fallback: 'password',
|
|
335
442
|
};
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
443
|
+
await saveConfig(config, projectPath);
|
|
444
|
+
}
|
|
445
|
+
const modeLabel = securityChoice === 'none' ? 'no encryption' : securityChoice;
|
|
446
|
+
console.log(chalk.green('EnvCP initialized!'));
|
|
447
|
+
console.log(chalk.gray(` Project: ${config.project}`));
|
|
448
|
+
console.log(chalk.gray(` Mode: ${initMode}`));
|
|
449
|
+
console.log(chalk.gray(` Security: ${modeLabel}`));
|
|
450
|
+
console.log(chalk.gray(` Session: ${config.session.enabled ? 'on' : 'off'}`));
|
|
451
|
+
console.log('');
|
|
452
|
+
console.log(chalk.green('Done! Your AI tools can now use EnvCP.'));
|
|
453
|
+
}
|
|
454
|
+
async function runAddSecretFlow() {
|
|
455
|
+
const name = await promptInput('Secret name:');
|
|
456
|
+
if (!name.trim()) {
|
|
457
|
+
console.log(chalk.yellow('No secret name entered.'));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
await withSession(async (storage, _password, config) => {
|
|
461
|
+
const value = await promptPassword('Enter value:');
|
|
462
|
+
const tagsInput = await promptInput('Tags (comma-separated):');
|
|
463
|
+
const description = await promptInput('Description:');
|
|
464
|
+
const tags = tagsInput.split(',').map((t) => t.trim()).filter(Boolean);
|
|
465
|
+
const now = new Date().toISOString();
|
|
466
|
+
const variable = {
|
|
467
|
+
name: name.trim(),
|
|
468
|
+
value,
|
|
469
|
+
encrypted: config.storage.encrypted,
|
|
470
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
471
|
+
description: description || undefined,
|
|
472
|
+
created: now,
|
|
473
|
+
updated: now,
|
|
474
|
+
sync_to_env: true,
|
|
475
|
+
protected: false,
|
|
476
|
+
};
|
|
477
|
+
await storage.set(variable.name, variable);
|
|
478
|
+
console.log(chalk.green(`Variable '${variable.name}' added successfully`));
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
async function runConfigMenu() {
|
|
482
|
+
const projectPath = process.cwd();
|
|
483
|
+
const config = await loadConfig(projectPath);
|
|
484
|
+
const serviceConfig = await loadServiceConfig();
|
|
485
|
+
if (!isInteractiveCli()) {
|
|
486
|
+
console.log(chalk.blue('EnvCP config'));
|
|
487
|
+
console.log(chalk.gray(` Session: ${config.session.enabled ? 'on' : 'off'}`));
|
|
488
|
+
console.log(chalk.gray(` Sync: ${config.sync.enabled ? 'on' : 'off'}`));
|
|
489
|
+
console.log(chalk.gray(` Audit: ${config.audit.enabled ? 'on' : 'off'}`));
|
|
490
|
+
console.log(chalk.gray(` Startup: ${await summarizeServiceStatus()}`));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
while (true) {
|
|
494
|
+
const choice = await promptTabbedMenu('EnvCP config', [
|
|
495
|
+
{
|
|
496
|
+
label: 'General',
|
|
497
|
+
items: [
|
|
498
|
+
{ label: 'Toggle session', value: 'general.session', hint: config.session.enabled ? '(on)' : '(off)' },
|
|
499
|
+
{ label: 'Toggle sync to .env', value: 'general.sync', hint: config.sync.enabled ? '(on)' : '(off)' },
|
|
500
|
+
{ label: 'Toggle startup service', value: 'general.startup', hint: serviceConfig.autostart ? '(on)' : '(off)' },
|
|
501
|
+
{ label: 'Toggle masked values', value: 'general.mask', hint: config.access.mask_values ? '(on)' : '(off)' },
|
|
502
|
+
{ label: 'Back', value: 'general.back' },
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
label: 'Advanced',
|
|
507
|
+
items: [
|
|
508
|
+
{ label: 'Toggle audit logging', value: 'advanced.audit', hint: config.audit.enabled ? '(on)' : '(off)' },
|
|
509
|
+
{ label: 'Toggle AI confirmation', value: 'advanced.confirm', hint: config.access.require_confirmation ? '(on)' : '(off)' },
|
|
510
|
+
{ label: 'Show startup status', value: 'advanced.startup-status', hint: '(service)' },
|
|
511
|
+
{ label: 'Show config path', value: 'advanced.path', hint: '(envcp.yaml)' },
|
|
512
|
+
{ label: 'Reload config guard', value: 'advanced.reload', hint: '(password required)' },
|
|
513
|
+
{ label: 'Back', value: 'advanced.back' },
|
|
514
|
+
],
|
|
515
|
+
},
|
|
516
|
+
]);
|
|
517
|
+
if (choice.endsWith('.back')) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (choice === 'general.session') {
|
|
521
|
+
config.session.enabled = !config.session.enabled;
|
|
522
|
+
await saveConfig(config, projectPath);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (choice === 'general.sync') {
|
|
526
|
+
config.sync.enabled = !config.sync.enabled;
|
|
527
|
+
await saveConfig(config, projectPath);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (choice === 'general.startup') {
|
|
531
|
+
serviceConfig.autostart = !serviceConfig.autostart;
|
|
532
|
+
serviceConfig.working_directory = projectPath;
|
|
533
|
+
await saveServiceConfig(serviceConfig);
|
|
534
|
+
const result = serviceConfig.autostart
|
|
535
|
+
? await installService({ workingDirectory: projectPath })
|
|
536
|
+
: await uninstallService();
|
|
537
|
+
console.log(result.ok ? chalk.green(result.message) : chalk.yellow(result.message));
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (choice === 'general.mask') {
|
|
541
|
+
config.access.mask_values = !config.access.mask_values;
|
|
542
|
+
await saveConfig(config, projectPath);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (choice === 'advanced.audit') {
|
|
546
|
+
config.audit.enabled = !config.audit.enabled;
|
|
547
|
+
await saveConfig(config, projectPath);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (choice === 'advanced.confirm') {
|
|
551
|
+
config.access.require_confirmation = !config.access.require_confirmation;
|
|
552
|
+
await saveConfig(config, projectPath);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (choice === 'advanced.startup-status') {
|
|
556
|
+
console.log(chalk.gray(await summarizeServiceStatus()));
|
|
557
|
+
await promptInput('Press Enter to continue');
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (choice === 'advanced.path') {
|
|
561
|
+
console.log(chalk.gray(path.join(projectPath, 'envcp.yaml')));
|
|
562
|
+
await promptInput('Press Enter to continue');
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (choice === 'advanced.reload') {
|
|
566
|
+
const configGuard = new ConfigGuard(projectPath);
|
|
567
|
+
const password = await promptPassword('Enter password to reload config:');
|
|
568
|
+
const result = await configGuard.reload(password);
|
|
569
|
+
console.log(result.success ? chalk.green('Config reloaded successfully') : chalk.red(result.error || 'Failed to reload config'));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function parseVariableRuleField(operation) {
|
|
574
|
+
switch (operation) {
|
|
575
|
+
case 'read':
|
|
576
|
+
return 'allow_ai_read';
|
|
577
|
+
case 'write':
|
|
578
|
+
return 'allow_ai_write';
|
|
579
|
+
case 'delete':
|
|
580
|
+
return 'allow_ai_delete';
|
|
581
|
+
case 'export':
|
|
582
|
+
return 'allow_ai_export';
|
|
583
|
+
case 'run':
|
|
584
|
+
case 'execute':
|
|
585
|
+
return 'allow_ai_execute';
|
|
586
|
+
case 'confirm':
|
|
587
|
+
case 'confirmation':
|
|
588
|
+
return 'require_confirmation';
|
|
589
|
+
default:
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function parseDefaultRuleField(operation) {
|
|
594
|
+
switch (operation) {
|
|
595
|
+
case 'list':
|
|
596
|
+
case 'show':
|
|
597
|
+
case 'names':
|
|
598
|
+
case 'active-check':
|
|
599
|
+
case 'active_check':
|
|
600
|
+
return 'allow_ai_active_check';
|
|
601
|
+
default:
|
|
602
|
+
return parseVariableRuleField(operation);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function formatRuleSetting(value, enabledLabel = 'allow', disabledLabel = 'deny') {
|
|
606
|
+
return value ? enabledLabel : disabledLabel;
|
|
607
|
+
}
|
|
608
|
+
function formatVariableRuleLines(rule) {
|
|
609
|
+
const lines = [];
|
|
610
|
+
const fields = [
|
|
611
|
+
['allow_ai_read', 'read', 'allow', 'deny'],
|
|
612
|
+
['allow_ai_write', 'write', 'allow', 'deny'],
|
|
613
|
+
['allow_ai_delete', 'delete', 'allow', 'deny'],
|
|
614
|
+
['allow_ai_export', 'export', 'allow', 'deny'],
|
|
615
|
+
['allow_ai_execute', 'run', 'allow', 'deny'],
|
|
616
|
+
['require_confirmation', 'confirmation', 'on', 'off'],
|
|
617
|
+
];
|
|
618
|
+
for (const [field, label, enabledLabel, disabledLabel] of fields) {
|
|
619
|
+
if (typeof rule[field] === 'boolean') {
|
|
620
|
+
lines.push(`${label}: ${formatRuleSetting(rule[field], enabledLabel, disabledLabel)}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (rule.active_window) {
|
|
624
|
+
lines.push(`active: ${rule.active_window.start}-${rule.active_window.end}`);
|
|
625
|
+
}
|
|
626
|
+
return lines.length > 0 ? lines : ['inherit all defaults'];
|
|
627
|
+
}
|
|
628
|
+
function formatClientRuleLines(rule) {
|
|
629
|
+
const lines = [];
|
|
630
|
+
const fields = [
|
|
631
|
+
['allow_ai_read', 'read', 'allow', 'deny'],
|
|
632
|
+
['allow_ai_write', 'write', 'allow', 'deny'],
|
|
633
|
+
['allow_ai_delete', 'delete', 'allow', 'deny'],
|
|
634
|
+
['allow_ai_export', 'export', 'allow', 'deny'],
|
|
635
|
+
['allow_ai_execute', 'run', 'allow', 'deny'],
|
|
636
|
+
['allow_ai_active_check', 'list names', 'allow', 'deny'],
|
|
637
|
+
['require_confirmation', 'confirmation', 'on', 'off'],
|
|
638
|
+
];
|
|
639
|
+
for (const [field, label, enabledLabel, disabledLabel] of fields) {
|
|
640
|
+
if (typeof rule[field] === 'boolean') {
|
|
641
|
+
lines.push(`${label}: ${formatRuleSetting(rule[field], enabledLabel, disabledLabel)}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return lines.length > 0 ? lines : ['inherit global defaults'];
|
|
645
|
+
}
|
|
646
|
+
function describeClientId(clientId) {
|
|
647
|
+
switch (clientId) {
|
|
648
|
+
case 'openai':
|
|
649
|
+
return 'OpenAI-compatible';
|
|
650
|
+
case 'gemini':
|
|
651
|
+
return 'Gemini-compatible';
|
|
652
|
+
case 'mcp':
|
|
653
|
+
return 'MCP client';
|
|
654
|
+
case 'api':
|
|
655
|
+
return 'REST API client';
|
|
656
|
+
case 'cli':
|
|
657
|
+
return 'EnvCP CLI';
|
|
658
|
+
default:
|
|
659
|
+
return 'Custom client';
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function formatClientLabel(clientId) {
|
|
663
|
+
return `${describeClientId(clientId)} (${clientId})`;
|
|
664
|
+
}
|
|
665
|
+
function getClientRule(config, clientId) {
|
|
666
|
+
return {
|
|
667
|
+
...(config.access.client_rules?.[clientId] || {}),
|
|
668
|
+
variable_rules: {
|
|
669
|
+
...(config.access.client_rules?.[clientId]?.variable_rules || {}),
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function saveClientRule(config, clientId, rule) {
|
|
674
|
+
const nextRules = { ...(config.access.client_rules || {}) };
|
|
675
|
+
const hasDefaults = [
|
|
676
|
+
rule.allow_ai_read,
|
|
677
|
+
rule.allow_ai_write,
|
|
678
|
+
rule.allow_ai_delete,
|
|
679
|
+
rule.allow_ai_export,
|
|
680
|
+
rule.allow_ai_execute,
|
|
681
|
+
rule.allow_ai_active_check,
|
|
682
|
+
rule.require_confirmation,
|
|
683
|
+
].some((value) => typeof value === 'boolean');
|
|
684
|
+
const hasVariables = Object.keys(rule.variable_rules || {}).length > 0;
|
|
685
|
+
if (!hasDefaults && !hasVariables) {
|
|
686
|
+
delete nextRules[clientId];
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
nextRules[clientId] = {
|
|
690
|
+
...rule,
|
|
691
|
+
variable_rules: rule.variable_rules || {},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
config.access.client_rules = nextRules;
|
|
695
|
+
}
|
|
696
|
+
function updateVariableRule(config, variableName, field, value, clientId) {
|
|
697
|
+
if (clientId) {
|
|
698
|
+
const clientRule = getClientRule(config, clientId);
|
|
699
|
+
const current = { ...(clientRule.variable_rules?.[variableName] || {}) };
|
|
700
|
+
if (value === undefined) {
|
|
701
|
+
delete current[field];
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
current[field] = value;
|
|
705
|
+
}
|
|
706
|
+
const nextVariableRules = { ...(clientRule.variable_rules || {}) };
|
|
707
|
+
if (Object.keys(current).length === 0) {
|
|
708
|
+
delete nextVariableRules[variableName];
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
nextVariableRules[variableName] = current;
|
|
712
|
+
}
|
|
713
|
+
clientRule.variable_rules = nextVariableRules;
|
|
714
|
+
saveClientRule(config, clientId, clientRule);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const current = { ...(config.access.variable_rules?.[variableName] || {}) };
|
|
718
|
+
if (value === undefined) {
|
|
719
|
+
delete current[field];
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
current[field] = value;
|
|
723
|
+
}
|
|
724
|
+
const nextRules = { ...(config.access.variable_rules || {}) };
|
|
725
|
+
if (Object.keys(current).length === 0) {
|
|
726
|
+
delete nextRules[variableName];
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
nextRules[variableName] = current;
|
|
730
|
+
}
|
|
731
|
+
config.access.variable_rules = nextRules;
|
|
732
|
+
}
|
|
733
|
+
function setVariableRuleWindow(config, variableName, start, end, clientId) {
|
|
734
|
+
if (clientId) {
|
|
735
|
+
const clientRule = getClientRule(config, clientId);
|
|
736
|
+
const current = { ...(clientRule.variable_rules?.[variableName] || {}) };
|
|
737
|
+
if (!start || !end) {
|
|
738
|
+
delete current.active_window;
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
current.active_window = { start, end };
|
|
742
|
+
}
|
|
743
|
+
const nextVariableRules = { ...(clientRule.variable_rules || {}) };
|
|
744
|
+
if (Object.keys(current).length === 0) {
|
|
745
|
+
delete nextVariableRules[variableName];
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
nextVariableRules[variableName] = current;
|
|
749
|
+
}
|
|
750
|
+
clientRule.variable_rules = nextVariableRules;
|
|
751
|
+
saveClientRule(config, clientId, clientRule);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const current = { ...(config.access.variable_rules?.[variableName] || {}) };
|
|
755
|
+
if (!start || !end) {
|
|
756
|
+
delete current.active_window;
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
current.active_window = { start, end };
|
|
760
|
+
}
|
|
761
|
+
const nextRules = { ...(config.access.variable_rules || {}) };
|
|
762
|
+
if (Object.keys(current).length === 0) {
|
|
763
|
+
delete nextRules[variableName];
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
nextRules[variableName] = current;
|
|
767
|
+
}
|
|
768
|
+
config.access.variable_rules = nextRules;
|
|
769
|
+
}
|
|
770
|
+
async function promptClientRuleTarget() {
|
|
771
|
+
const target = await promptMenu('Apply rule to', [
|
|
772
|
+
{ label: 'Everyone', value: 'global' },
|
|
773
|
+
{ label: 'One client / who', value: 'client' },
|
|
774
|
+
], 'global');
|
|
775
|
+
if (target !== 'client') {
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
const clientId = (await promptInput('Client id (examples: mcp, openai, gemini, api, cursor):')).trim();
|
|
779
|
+
return clientId || undefined;
|
|
780
|
+
}
|
|
781
|
+
async function editVariableRule(config, clientId) {
|
|
782
|
+
const variableName = (await promptInput('Variable name:')).trim();
|
|
783
|
+
if (!variableName) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const field = await promptMenu(`Rule for ${variableName}`, [
|
|
787
|
+
{ label: 'AI read', value: 'allow_ai_read' },
|
|
788
|
+
{ label: 'AI write', value: 'allow_ai_write' },
|
|
789
|
+
{ label: 'AI delete', value: 'allow_ai_delete' },
|
|
790
|
+
{ label: 'AI export', value: 'allow_ai_export' },
|
|
791
|
+
{ label: 'AI run', value: 'allow_ai_execute' },
|
|
792
|
+
{ label: 'Require confirmation', value: 'require_confirmation' },
|
|
793
|
+
]);
|
|
794
|
+
const value = await promptMenu(`Set ${field} for ${variableName}`, [
|
|
795
|
+
{ label: 'Allow', value: 'allow' },
|
|
796
|
+
{ label: 'Deny', value: 'deny' },
|
|
797
|
+
{ label: 'Inherit default', value: 'inherit' },
|
|
798
|
+
], 'inherit');
|
|
799
|
+
updateVariableRule(config, variableName, field, value === 'inherit' ? undefined : value === 'allow', clientId);
|
|
800
|
+
}
|
|
801
|
+
async function removeVariableRule(config, clientId) {
|
|
802
|
+
const variableName = (await promptInput('Variable rule to remove:')).trim();
|
|
803
|
+
if (!variableName) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (clientId) {
|
|
807
|
+
const clientRule = getClientRule(config, clientId);
|
|
808
|
+
const nextVariableRules = { ...(clientRule.variable_rules || {}) };
|
|
809
|
+
delete nextVariableRules[variableName];
|
|
810
|
+
clientRule.variable_rules = nextVariableRules;
|
|
811
|
+
saveClientRule(config, clientId, clientRule);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const nextRules = { ...(config.access.variable_rules || {}) };
|
|
815
|
+
delete nextRules[variableName];
|
|
816
|
+
config.access.variable_rules = nextRules;
|
|
817
|
+
}
|
|
818
|
+
async function editVariableRuleWindow(config, clientId) {
|
|
819
|
+
const variableName = (await promptInput('Variable name:')).trim();
|
|
820
|
+
if (!variableName) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const start = (await promptInput('Allowed from (HH:MM):')).trim();
|
|
824
|
+
const end = (await promptInput('Allowed until (HH:MM):')).trim();
|
|
825
|
+
setVariableRuleWindow(config, variableName, start, end, clientId);
|
|
826
|
+
}
|
|
827
|
+
function applyDefaultRule(config, field, value, clientId) {
|
|
828
|
+
if (clientId) {
|
|
829
|
+
const clientRule = getClientRule(config, clientId);
|
|
830
|
+
if (value === undefined) {
|
|
831
|
+
delete clientRule[field];
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
clientRule[field] = value;
|
|
835
|
+
}
|
|
836
|
+
saveClientRule(config, clientId, clientRule);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
switch (field) {
|
|
840
|
+
case 'allow_ai_read':
|
|
841
|
+
config.access.allow_ai_read = value === true;
|
|
842
|
+
break;
|
|
843
|
+
case 'allow_ai_write':
|
|
844
|
+
config.access.allow_ai_write = value === true;
|
|
845
|
+
break;
|
|
846
|
+
case 'allow_ai_delete':
|
|
847
|
+
config.access.allow_ai_delete = value === true;
|
|
848
|
+
break;
|
|
849
|
+
case 'allow_ai_export':
|
|
850
|
+
config.access.allow_ai_export = value === true;
|
|
851
|
+
break;
|
|
852
|
+
case 'allow_ai_execute':
|
|
853
|
+
config.access.allow_ai_execute = value === true;
|
|
854
|
+
break;
|
|
855
|
+
case 'allow_ai_active_check':
|
|
856
|
+
config.access.allow_ai_active_check = value === true;
|
|
857
|
+
break;
|
|
858
|
+
case 'require_confirmation':
|
|
859
|
+
config.access.require_confirmation = value === true;
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function parseRuleScope(value, fallback) {
|
|
864
|
+
if (!value) {
|
|
865
|
+
return fallback;
|
|
866
|
+
}
|
|
867
|
+
if (value === 'project' || value === 'home' || value === 'merged') {
|
|
868
|
+
return value;
|
|
869
|
+
}
|
|
870
|
+
throw new Error(`Unknown scope '${value}'. Use project, home, or merged.`);
|
|
871
|
+
}
|
|
872
|
+
async function chooseEditableRuleScope() {
|
|
873
|
+
return promptMenu('Rule scope', [
|
|
874
|
+
{ label: 'This project', value: 'project' },
|
|
875
|
+
{ label: 'Home / whole computer', value: 'home' },
|
|
876
|
+
], 'project');
|
|
877
|
+
}
|
|
878
|
+
function describeMergedDefaultOrigin(mergedValue, projectValue, homeValue) {
|
|
879
|
+
const sameAsProject = JSON.stringify(mergedValue) === JSON.stringify(projectValue);
|
|
880
|
+
const sameAsHome = JSON.stringify(mergedValue) === JSON.stringify(homeValue);
|
|
881
|
+
if (sameAsProject && sameAsHome) {
|
|
882
|
+
return 'default';
|
|
883
|
+
}
|
|
884
|
+
if (sameAsProject && !sameAsHome) {
|
|
885
|
+
return 'project';
|
|
886
|
+
}
|
|
887
|
+
if (!sameAsProject && sameAsHome) {
|
|
888
|
+
return 'home';
|
|
889
|
+
}
|
|
890
|
+
return 'merged';
|
|
891
|
+
}
|
|
892
|
+
function describeMergedVariableRuleOrigin(name, mergedRule, projectRules, homeRules) {
|
|
893
|
+
const projectRule = projectRules[name];
|
|
894
|
+
const homeRule = homeRules[name];
|
|
895
|
+
if (projectRule && homeRule) {
|
|
896
|
+
return JSON.stringify(mergedRule) === JSON.stringify(projectRule)
|
|
897
|
+
? 'project overrides home'
|
|
898
|
+
: 'home + project';
|
|
899
|
+
}
|
|
900
|
+
if (projectRule) {
|
|
901
|
+
return 'project';
|
|
902
|
+
}
|
|
903
|
+
if (homeRule) {
|
|
904
|
+
return 'home';
|
|
905
|
+
}
|
|
906
|
+
return 'merged';
|
|
907
|
+
}
|
|
908
|
+
function describeMergedClientRuleOrigin(clientId, mergedRule, projectRules, homeRules) {
|
|
909
|
+
const projectRule = projectRules[clientId];
|
|
910
|
+
const homeRule = homeRules[clientId];
|
|
911
|
+
if (projectRule && homeRule) {
|
|
912
|
+
return JSON.stringify(mergedRule) === JSON.stringify(projectRule)
|
|
913
|
+
? 'project overrides home'
|
|
914
|
+
: 'home + project';
|
|
915
|
+
}
|
|
916
|
+
if (projectRule) {
|
|
917
|
+
return 'project';
|
|
918
|
+
}
|
|
919
|
+
if (homeRule) {
|
|
920
|
+
return 'home';
|
|
921
|
+
}
|
|
922
|
+
return 'merged';
|
|
923
|
+
}
|
|
924
|
+
async function runRuleMenu() {
|
|
925
|
+
const projectPath = process.cwd();
|
|
926
|
+
let config = await loadScopedConfig(projectPath, 'merged');
|
|
927
|
+
if (!isInteractiveCli()) {
|
|
928
|
+
console.log(chalk.blue('EnvCP rules'));
|
|
929
|
+
console.log(chalk.gray(` AI read: ${config.access.allow_ai_read ? 'allow' : 'deny'}`));
|
|
930
|
+
console.log(chalk.gray(` AI write: ${config.access.allow_ai_write ? 'allow' : 'deny'}`));
|
|
931
|
+
console.log(chalk.gray(` AI delete: ${config.access.allow_ai_delete ? 'allow' : 'deny'}`));
|
|
932
|
+
console.log(chalk.gray(` AI run: ${config.access.allow_ai_execute ? 'allow' : 'deny'}`));
|
|
933
|
+
console.log(chalk.gray(` Variable rules: ${Object.keys(config.access.variable_rules || {}).length}`));
|
|
934
|
+
console.log(chalk.gray(` Who rules: ${Object.keys(config.access.client_rules || {}).length}`));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
while (true) {
|
|
938
|
+
const choice = await promptTabbedMenu('EnvCP rules', [
|
|
939
|
+
{
|
|
940
|
+
label: 'Defaults',
|
|
941
|
+
items: [
|
|
942
|
+
{ label: 'Toggle AI read', value: 'default.read', hint: config.access.allow_ai_read ? '(allow)' : '(deny)' },
|
|
943
|
+
{ label: 'Toggle AI write', value: 'default.write', hint: config.access.allow_ai_write ? '(allow)' : '(deny)' },
|
|
944
|
+
{ label: 'Toggle AI delete', value: 'default.delete', hint: config.access.allow_ai_delete ? '(allow)' : '(deny)' },
|
|
945
|
+
{ label: 'Toggle AI run', value: 'default.run', hint: config.access.allow_ai_execute ? '(allow)' : '(deny)' },
|
|
946
|
+
{ label: 'Toggle AI list names', value: 'default.list', hint: config.access.allow_ai_active_check ? '(allow)' : '(deny)' },
|
|
947
|
+
{ label: 'Edit one who default rule', value: 'default.client', hint: `(${Object.keys(config.access.client_rules || {}).length} saved)` },
|
|
948
|
+
{ label: 'Back', value: 'default.back' },
|
|
949
|
+
],
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
label: 'Variable',
|
|
953
|
+
items: [
|
|
954
|
+
{ label: 'Edit one variable rule', value: 'variable.edit', hint: `(${Object.keys(config.access.variable_rules || {}).length} saved)` },
|
|
955
|
+
{ label: 'Set active time window', value: 'variable.window', hint: '(HH:MM -> HH:MM)' },
|
|
956
|
+
{ label: 'Clear active time window', value: 'variable.clear-window', hint: '(one variable)' },
|
|
957
|
+
{ label: 'Remove one variable rule', value: 'variable.remove', hint: '(exact variable name)' },
|
|
958
|
+
{ label: 'Show variable rule names', value: 'variable.list', hint: '(saved rules)' },
|
|
959
|
+
{ label: 'Back', value: 'variable.back' },
|
|
960
|
+
],
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
label: 'Who',
|
|
964
|
+
items: [
|
|
965
|
+
{ label: 'Edit one who variable rule', value: 'who.edit', hint: `(${Object.keys(config.access.client_rules || {}).length} clients)` },
|
|
966
|
+
{ label: 'Set who time window', value: 'who.window', hint: '(client + variable)' },
|
|
967
|
+
{ label: 'Clear who time window', value: 'who.clear-window', hint: '(client + variable)' },
|
|
968
|
+
{ label: 'Remove one who variable rule', value: 'who.remove', hint: '(client + variable)' },
|
|
969
|
+
{ label: 'Show who names', value: 'who.list', hint: '(saved clients)' },
|
|
970
|
+
{ label: 'Back', value: 'who.back' },
|
|
971
|
+
],
|
|
972
|
+
},
|
|
973
|
+
]);
|
|
974
|
+
if (choice.endsWith('.back')) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (choice === 'variable.list') {
|
|
978
|
+
config = await loadScopedConfig(projectPath, 'merged');
|
|
979
|
+
const names = Object.keys(config.access.variable_rules || {});
|
|
980
|
+
console.log(names.length > 0 ? chalk.gray(names.join(', ')) : chalk.gray('No variable-specific rules yet.'));
|
|
981
|
+
await promptInput('Press Enter to continue');
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (choice === 'who.list') {
|
|
985
|
+
config = await loadScopedConfig(projectPath, 'merged');
|
|
986
|
+
const names = Object.keys(config.access.client_rules || {});
|
|
987
|
+
console.log(names.length > 0 ? chalk.gray(names.join(', ')) : chalk.gray('No who-specific rules yet.'));
|
|
988
|
+
await promptInput('Press Enter to continue');
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
const scope = await chooseEditableRuleScope();
|
|
992
|
+
config = await loadScopedConfig(projectPath, scope);
|
|
993
|
+
if (choice === 'default.read') {
|
|
994
|
+
config.access.allow_ai_read = !config.access.allow_ai_read;
|
|
995
|
+
}
|
|
996
|
+
else if (choice === 'default.write') {
|
|
997
|
+
config.access.allow_ai_write = !config.access.allow_ai_write;
|
|
998
|
+
}
|
|
999
|
+
else if (choice === 'default.delete') {
|
|
1000
|
+
config.access.allow_ai_delete = !config.access.allow_ai_delete;
|
|
1001
|
+
}
|
|
1002
|
+
else if (choice === 'default.run') {
|
|
1003
|
+
config.access.allow_ai_execute = !config.access.allow_ai_execute;
|
|
1004
|
+
}
|
|
1005
|
+
else if (choice === 'default.list') {
|
|
1006
|
+
config.access.allow_ai_active_check = !config.access.allow_ai_active_check;
|
|
1007
|
+
}
|
|
1008
|
+
else if (choice === 'default.client') {
|
|
1009
|
+
const clientId = await promptClientRuleTarget();
|
|
1010
|
+
if (clientId) {
|
|
1011
|
+
const field = await promptMenu(`Default rule for ${clientId}`, [
|
|
1012
|
+
{ label: 'AI read', value: 'allow_ai_read' },
|
|
1013
|
+
{ label: 'AI write', value: 'allow_ai_write' },
|
|
1014
|
+
{ label: 'AI delete', value: 'allow_ai_delete' },
|
|
1015
|
+
{ label: 'AI export', value: 'allow_ai_export' },
|
|
1016
|
+
{ label: 'AI run', value: 'allow_ai_execute' },
|
|
1017
|
+
{ label: 'AI list names', value: 'allow_ai_active_check' },
|
|
1018
|
+
{ label: 'Require confirmation', value: 'require_confirmation' },
|
|
1019
|
+
]);
|
|
1020
|
+
const value = await promptMenu(`Set ${field} for ${clientId}`, [
|
|
1021
|
+
{ label: 'Allow / on', value: 'allow' },
|
|
1022
|
+
{ label: 'Deny / off', value: 'deny' },
|
|
1023
|
+
{ label: 'Inherit global default', value: 'inherit' },
|
|
1024
|
+
], 'inherit');
|
|
1025
|
+
applyDefaultRule(config, field, value === 'inherit' ? undefined : value === 'allow', clientId);
|
|
344
1026
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
1027
|
+
}
|
|
1028
|
+
else if (choice === 'variable.edit') {
|
|
1029
|
+
await editVariableRule(config);
|
|
1030
|
+
}
|
|
1031
|
+
else if (choice === 'variable.window') {
|
|
1032
|
+
await editVariableRuleWindow(config);
|
|
1033
|
+
}
|
|
1034
|
+
else if (choice === 'variable.clear-window') {
|
|
1035
|
+
const variable = (await promptInput('Variable name:')).trim();
|
|
1036
|
+
if (variable) {
|
|
1037
|
+
setVariableRuleWindow(config, variable, undefined, undefined);
|
|
348
1038
|
}
|
|
349
1039
|
}
|
|
350
|
-
else {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
1040
|
+
else if (choice === 'variable.remove') {
|
|
1041
|
+
await removeVariableRule(config);
|
|
1042
|
+
}
|
|
1043
|
+
else if (choice === 'who.edit') {
|
|
1044
|
+
const clientId = await promptClientRuleTarget();
|
|
1045
|
+
if (clientId) {
|
|
1046
|
+
await editVariableRule(config, clientId);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
else if (choice === 'who.window') {
|
|
1050
|
+
const clientId = await promptClientRuleTarget();
|
|
1051
|
+
if (clientId) {
|
|
1052
|
+
await editVariableRuleWindow(config, clientId);
|
|
1053
|
+
}
|
|
355
1054
|
}
|
|
1055
|
+
else if (choice === 'who.clear-window') {
|
|
1056
|
+
const clientId = await promptClientRuleTarget();
|
|
1057
|
+
if (clientId) {
|
|
1058
|
+
const variable = (await promptInput('Variable name:')).trim();
|
|
1059
|
+
if (variable) {
|
|
1060
|
+
setVariableRuleWindow(config, variable, undefined, undefined, clientId);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
else if (choice === 'who.remove') {
|
|
1065
|
+
const clientId = await promptClientRuleTarget();
|
|
1066
|
+
if (clientId) {
|
|
1067
|
+
await removeVariableRule(config, clientId);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
await saveScopedConfig(config, projectPath, scope);
|
|
356
1071
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
});
|
|
360
|
-
program
|
|
361
|
-
.command('unlock')
|
|
362
|
-
.description('Unlock EnvCP session with password')
|
|
363
|
-
.option('--recovery-key <key>', 'Recovery key to clear permanent lockout')
|
|
364
|
-
.option('--save-to-keychain', 'Save password to OS keychain for auto-unlock')
|
|
365
|
-
.option('--setup-hsm', 'Protect vault password with hardware security module')
|
|
366
|
-
.option('--hsm-type <type>', 'HSM type: yubikey | gpg | pkcs11 (default: yubikey)')
|
|
367
|
-
.option('--key-id <id>', 'GPG key ID or PKCS#11 key label')
|
|
368
|
-
.option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library (.so / .dll)')
|
|
369
|
-
.option('--global', 'Unlock the global vault at ~/.envcp')
|
|
370
|
-
.action(async (options) => {
|
|
1072
|
+
}
|
|
1073
|
+
async function runUnlockFlow(options = {}) {
|
|
371
1074
|
const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
|
|
372
1075
|
const password = await promptPassword('Enter password:');
|
|
373
1076
|
const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
|
|
374
1077
|
if (!passwordValid) {
|
|
375
|
-
|
|
376
|
-
console.log(chalk.red("Invalid password"));
|
|
1078
|
+
console.log(chalk.red('Invalid password'));
|
|
377
1079
|
return;
|
|
378
1080
|
}
|
|
379
|
-
if (
|
|
1081
|
+
if (passwordWarning) {
|
|
380
1082
|
console.log(chalk.yellow('⚠ Weak password detected'));
|
|
381
1083
|
}
|
|
382
1084
|
const sessionDir = path.dirname(resolveSessionPath(projectPath, config));
|
|
383
|
-
|
|
384
|
-
// Handle recovery key if provided
|
|
1085
|
+
const lockoutManager = new LockoutManager(path.join(sessionDir, '.lockout'));
|
|
385
1086
|
if (options.recoveryKey) {
|
|
386
1087
|
const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
|
|
387
1088
|
if (!await pathExists(recoveryPath)) {
|
|
@@ -391,10 +1092,8 @@ program
|
|
|
391
1092
|
const recoveryData = await fs.readFile(recoveryPath, 'utf8');
|
|
392
1093
|
try {
|
|
393
1094
|
await recoverPassword(recoveryData, options.recoveryKey);
|
|
394
|
-
// Recovery key is valid - clear permanent lockout
|
|
395
1095
|
await lockoutManager.clearPermanentLockout();
|
|
396
1096
|
console.log(chalk.green('Permanent lockout cleared.'));
|
|
397
|
-
// Log recovery event
|
|
398
1097
|
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
399
1098
|
await logManager.init();
|
|
400
1099
|
await logManager.log({
|
|
@@ -407,17 +1106,15 @@ program
|
|
|
407
1106
|
session_id: '',
|
|
408
1107
|
client_id: 'cli',
|
|
409
1108
|
client_type: 'terminal',
|
|
410
|
-
ip: '127.0.0.1'
|
|
1109
|
+
ip: '127.0.0.1',
|
|
411
1110
|
});
|
|
412
1111
|
console.log(chalk.yellow('Note: You still need to enter the correct password.'));
|
|
413
|
-
// Continue with password prompt
|
|
414
1112
|
}
|
|
415
1113
|
catch {
|
|
416
1114
|
console.log(chalk.red('Invalid recovery key.'));
|
|
417
1115
|
return;
|
|
418
1116
|
}
|
|
419
1117
|
}
|
|
420
|
-
// Use new brute_force_protection config if available, fall back to session config
|
|
421
1118
|
const bfpConfig = config.security?.brute_force_protection;
|
|
422
1119
|
const lockoutThreshold = bfpConfig?.max_attempts ?? config.session?.lockout_threshold ?? 5;
|
|
423
1120
|
const lockoutBaseSeconds = bfpConfig?.lockout_duration ?? config.session?.lockout_base_seconds ?? 60;
|
|
@@ -426,14 +1123,12 @@ program
|
|
|
426
1123
|
const permanentThreshold = bfpConfig?.permanent_lockout_threshold ?? 0;
|
|
427
1124
|
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
428
1125
|
await sessionManager.init();
|
|
429
|
-
// Initialize audit logging
|
|
430
1126
|
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
431
1127
|
await logManager.init();
|
|
432
1128
|
const vaultPathForUnlock = await resolveVaultPath(projectPath, config);
|
|
433
1129
|
const storage = new StorageManager(vaultPathForUnlock, config.storage.encrypted);
|
|
434
1130
|
storage.setPassword(password);
|
|
435
1131
|
const storeExists = await storage.exists();
|
|
436
|
-
// Check lockout before attempting password verification on existing stores
|
|
437
1132
|
if (storeExists) {
|
|
438
1133
|
const lockoutStatus = await lockoutManager.check();
|
|
439
1134
|
if (lockoutStatus.locked) {
|
|
@@ -451,11 +1146,9 @@ program
|
|
|
451
1146
|
const recoveryData = await fs.readFile(recoveryPath, 'utf8');
|
|
452
1147
|
try {
|
|
453
1148
|
await recoverPassword(recoveryData, recoveryKey);
|
|
454
|
-
// Recovery key is valid - clear permanent lockout
|
|
455
1149
|
await lockoutManager.clearPermanentLockout();
|
|
456
1150
|
console.log(chalk.green('Permanent lockout cleared. You can now attempt to unlock.'));
|
|
457
1151
|
console.log(chalk.yellow('Note: You still need to enter the correct password.'));
|
|
458
|
-
// Log recovery event
|
|
459
1152
|
await logManager.log({
|
|
460
1153
|
timestamp: new Date().toISOString(),
|
|
461
1154
|
operation: 'unlock',
|
|
@@ -466,9 +1159,8 @@ program
|
|
|
466
1159
|
session_id: '',
|
|
467
1160
|
client_id: 'cli',
|
|
468
1161
|
client_type: 'terminal',
|
|
469
|
-
ip: '127.0.0.1'
|
|
1162
|
+
ip: '127.0.0.1',
|
|
470
1163
|
});
|
|
471
|
-
// Continue with password prompt
|
|
472
1164
|
}
|
|
473
1165
|
catch {
|
|
474
1166
|
console.log(chalk.red('Invalid recovery key.'));
|
|
@@ -487,12 +1179,10 @@ program
|
|
|
487
1179
|
}
|
|
488
1180
|
if (!storeExists) {
|
|
489
1181
|
const confirmPasswordValue = await promptPassword('Confirm password:');
|
|
490
|
-
|
|
491
|
-
if (confirmPasswordValue !== password) {
|
|
1182
|
+
if (!secureCompare(Buffer.from(confirmPasswordValue, 'utf8'), Buffer.from(password, 'utf8'))) {
|
|
492
1183
|
console.log(chalk.red('Passwords do not match'));
|
|
493
1184
|
return;
|
|
494
1185
|
}
|
|
495
|
-
// Generate recovery key for new stores in recoverable mode
|
|
496
1186
|
if (config.security?.mode === 'recoverable') {
|
|
497
1187
|
const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
|
|
498
1188
|
if (!await pathExists(recoveryPath)) {
|
|
@@ -511,10 +1201,9 @@ program
|
|
|
511
1201
|
try {
|
|
512
1202
|
await storage.load();
|
|
513
1203
|
}
|
|
514
|
-
catch
|
|
1204
|
+
catch {
|
|
515
1205
|
if (storeExists) {
|
|
516
1206
|
const status = await lockoutManager.recordFailure(lockoutThreshold, lockoutBaseSeconds, progressiveDelay, maxDelay, permanentThreshold);
|
|
517
|
-
// Log the failed attempt
|
|
518
1207
|
await logManager.log({
|
|
519
1208
|
timestamp: new Date().toISOString(),
|
|
520
1209
|
operation: 'auth_failure',
|
|
@@ -525,12 +1214,11 @@ program
|
|
|
525
1214
|
session_id: '',
|
|
526
1215
|
client_id: 'cli',
|
|
527
1216
|
client_type: 'terminal',
|
|
528
|
-
ip: '127.0.0.1'
|
|
1217
|
+
ip: '127.0.0.1',
|
|
529
1218
|
});
|
|
530
1219
|
if (status.permanent_locked) {
|
|
531
1220
|
console.log(chalk.red.bold('PERMANENT LOCKOUT TRIGGERED: Too many failed attempts.'));
|
|
532
1221
|
console.log(chalk.red('Recovery key or administrator intervention required.'));
|
|
533
|
-
// Log permanent lockout event
|
|
534
1222
|
await logManager.log({
|
|
535
1223
|
timestamp: new Date().toISOString(),
|
|
536
1224
|
operation: 'permanent_lockout',
|
|
@@ -541,12 +1229,11 @@ program
|
|
|
541
1229
|
session_id: '',
|
|
542
1230
|
client_id: 'cli',
|
|
543
1231
|
client_type: 'terminal',
|
|
544
|
-
ip: '127.0.0.1'
|
|
1232
|
+
ip: '127.0.0.1',
|
|
545
1233
|
});
|
|
546
1234
|
}
|
|
547
1235
|
else if (status.locked) {
|
|
548
1236
|
console.log(chalk.red(`Invalid password. Too many failed attempts — locked out for ${status.remaining_seconds} second(s).`));
|
|
549
|
-
// Log lockout event
|
|
550
1237
|
await logManager.log({
|
|
551
1238
|
timestamp: new Date().toISOString(),
|
|
552
1239
|
operation: 'lockout_triggered',
|
|
@@ -557,13 +1244,12 @@ program
|
|
|
557
1244
|
session_id: '',
|
|
558
1245
|
client_id: 'cli',
|
|
559
1246
|
client_type: 'terminal',
|
|
560
|
-
ip: '127.0.0.1'
|
|
1247
|
+
ip: '127.0.0.1',
|
|
561
1248
|
});
|
|
562
1249
|
}
|
|
563
1250
|
else {
|
|
564
1251
|
const remaining = lockoutThreshold - status.attempts;
|
|
565
1252
|
let message = `Invalid password. ${remaining} attempt(s) remaining before lockout.`;
|
|
566
|
-
// Show progressive delay if applied
|
|
567
1253
|
if (status.delay_seconds && status.delay_seconds > 0) {
|
|
568
1254
|
message += ` (Delayed ${status.delay_seconds}s)`;
|
|
569
1255
|
}
|
|
@@ -575,34 +1261,37 @@ program
|
|
|
575
1261
|
}
|
|
576
1262
|
return;
|
|
577
1263
|
}
|
|
578
|
-
// Successful unlock — clear any lockout state
|
|
579
1264
|
await lockoutManager.reset();
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
1265
|
+
if (config.session?.enabled === false) {
|
|
1266
|
+
await sessionManager.destroy();
|
|
1267
|
+
console.log(chalk.green('Password verified.'));
|
|
1268
|
+
console.log(chalk.gray('Session mode is off, so EnvCP will ask again next time.'));
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
const session = await sessionManager.create(password);
|
|
1272
|
+
await logManager.log({
|
|
1273
|
+
timestamp: new Date().toISOString(),
|
|
1274
|
+
operation: 'unlock',
|
|
1275
|
+
variable: '',
|
|
1276
|
+
source: 'cli',
|
|
1277
|
+
success: true,
|
|
1278
|
+
message: 'Session unlocked successfully',
|
|
1279
|
+
session_id: session.id,
|
|
1280
|
+
client_id: 'cli',
|
|
1281
|
+
client_type: 'terminal',
|
|
1282
|
+
ip: '127.0.0.1',
|
|
1283
|
+
});
|
|
1284
|
+
console.log(chalk.green('Session unlocked!'));
|
|
1285
|
+
console.log(chalk.gray(` Session ID: ${session.id}`));
|
|
1286
|
+
console.log(chalk.gray(` Expires in: ${config.session?.timeout_minutes || 30} minutes`));
|
|
1287
|
+
const maxExt = config.session?.max_extensions || 5;
|
|
1288
|
+
console.log(chalk.gray(` Extensions remaining: ${maxExt - session.extensions}/${maxExt}`));
|
|
1289
|
+
}
|
|
600
1290
|
if (options.saveToKeychain) {
|
|
601
1291
|
const keychain = new KeychainManager(config.keychain?.service || 'envcp');
|
|
602
1292
|
if (await keychain.isAvailable()) {
|
|
603
1293
|
const result = await keychain.storePassword(password, projectPath);
|
|
604
1294
|
if (result.success) {
|
|
605
|
-
// Enable keychain in config
|
|
606
1295
|
config.keychain = { ...config.keychain, enabled: true };
|
|
607
1296
|
await saveConfig(config, projectPath);
|
|
608
1297
|
console.log(chalk.green(`Password saved to ${keychain.backendName}`));
|
|
@@ -616,7 +1305,6 @@ program
|
|
|
616
1305
|
console.log(chalk.red(`OS keychain not available (${keychain.backendName})`));
|
|
617
1306
|
}
|
|
618
1307
|
}
|
|
619
|
-
// Setup HSM if requested
|
|
620
1308
|
if (options.setupHsm) {
|
|
621
1309
|
const hsmType = options.hsmType || 'yubikey';
|
|
622
1310
|
config.hsm = {
|
|
@@ -645,17 +1333,135 @@ program
|
|
|
645
1333
|
console.log(chalk.red(`HSM setup failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
646
1334
|
}
|
|
647
1335
|
}
|
|
1336
|
+
}
|
|
1337
|
+
async function runLockFlow(global = false) {
|
|
1338
|
+
const { projectPath, config } = await resolveCliContext(global ? 'global' : undefined);
|
|
1339
|
+
if (config.session?.enabled === false) {
|
|
1340
|
+
console.log(chalk.gray('Session mode is already off.'));
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
1344
|
+
await sessionManager.init();
|
|
1345
|
+
await sessionManager.destroy();
|
|
1346
|
+
console.log(chalk.green('Session locked'));
|
|
1347
|
+
}
|
|
1348
|
+
async function describeHomeState() {
|
|
1349
|
+
const config = await loadConfig(process.cwd());
|
|
1350
|
+
if (config.encryption?.enabled === false) {
|
|
1351
|
+
return { title: 'EnvCP (ready)', locked: false, sessionEnabled: false };
|
|
1352
|
+
}
|
|
1353
|
+
if (config.session?.enabled === false) {
|
|
1354
|
+
return { title: 'EnvCP (password each time)', locked: false, sessionEnabled: false };
|
|
1355
|
+
}
|
|
1356
|
+
const locked = !await pathExists(resolveSessionPath(process.cwd(), config));
|
|
1357
|
+
return {
|
|
1358
|
+
title: locked ? 'EnvCP (locked)' : 'EnvCP (session active)',
|
|
1359
|
+
locked,
|
|
1360
|
+
sessionEnabled: true,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
async function runInteractiveHome() {
|
|
1364
|
+
if (!await isProjectInitialized()) {
|
|
1365
|
+
console.log(chalk.yellow('EnvCP is not set up yet in this folder.'));
|
|
1366
|
+
await runInitFlow();
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
while (true) {
|
|
1370
|
+
const homeState = await describeHomeState();
|
|
1371
|
+
const choices = [
|
|
1372
|
+
...(homeState.sessionEnabled
|
|
1373
|
+
? [{ label: homeState.locked ? 'Unlock' : 'Lock', value: homeState.locked ? 'unlock' : 'lock' }]
|
|
1374
|
+
: []),
|
|
1375
|
+
{ label: 'Add secret', value: 'add' },
|
|
1376
|
+
{ label: 'Setup project', value: 'setup' },
|
|
1377
|
+
{ label: 'Config', value: 'config' },
|
|
1378
|
+
{ label: 'Rules', value: 'rules' },
|
|
1379
|
+
{ label: 'Exit', value: 'exit' },
|
|
1380
|
+
];
|
|
1381
|
+
const choice = await promptMenu(homeState.title, choices, choices[0].value);
|
|
1382
|
+
if (choice === 'unlock') {
|
|
1383
|
+
await runUnlockFlow();
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
if (choice === 'lock') {
|
|
1387
|
+
await runLockFlow();
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
if (choice === 'add') {
|
|
1391
|
+
await runAddSecretFlow();
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
if (choice === 'setup') {
|
|
1395
|
+
await runConfigMenu();
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (choice === 'config') {
|
|
1399
|
+
await runConfigMenu();
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
if (choice === 'rules') {
|
|
1403
|
+
await runRuleMenu();
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
if (choice === 'exit') {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const program = new Command();
|
|
1412
|
+
program
|
|
1413
|
+
.name('envcp')
|
|
1414
|
+
.description('Secure environment variable management for AI-assisted coding')
|
|
1415
|
+
.version(VERSION);
|
|
1416
|
+
program
|
|
1417
|
+
.command('init')
|
|
1418
|
+
.description('Set up EnvCP for first-time use')
|
|
1419
|
+
.option('-p, --project <name>', 'Project name')
|
|
1420
|
+
.option('--no-encrypt', 'Skip encryption (passwordless mode)')
|
|
1421
|
+
.option('--skip-env', 'Skip .env auto-import')
|
|
1422
|
+
.option('--skip-mcp', 'Skip MCP auto-registration')
|
|
1423
|
+
.option('--auth-method <method>', 'Authentication method: password | keychain | hsm | multi (default: password)')
|
|
1424
|
+
.option('--hsm-type <type>', 'HSM type for --auth-method hsm|multi: yubikey | gpg | pkcs11')
|
|
1425
|
+
.option('--key-id <id>', 'GPG key ID or PKCS#11 key label for HSM auth')
|
|
1426
|
+
.option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library for --hsm-type pkcs11')
|
|
1427
|
+
.action(async (options) => {
|
|
1428
|
+
await runInitFlow(options);
|
|
1429
|
+
});
|
|
1430
|
+
program
|
|
1431
|
+
.command('setup')
|
|
1432
|
+
.description('Set up or reconfigure the current project')
|
|
1433
|
+
.action(async () => {
|
|
1434
|
+
if (!await isProjectInitialized()) {
|
|
1435
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
1436
|
+
if (path.resolve(process.cwd()) === path.resolve(home)) {
|
|
1437
|
+
console.log(chalk.yellow('envcp setup configures a project folder.'));
|
|
1438
|
+
console.log(chalk.gray('Open your project first, then run `envcp setup`.'));
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
await runInitFlow();
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
await runConfigMenu();
|
|
1445
|
+
});
|
|
1446
|
+
program
|
|
1447
|
+
.command('unlock')
|
|
1448
|
+
.description('Unlock EnvCP session with password')
|
|
1449
|
+
.option('--recovery-key <key>', 'Recovery key to clear permanent lockout')
|
|
1450
|
+
.option('--save-to-keychain', 'Save password to OS keychain for auto-unlock')
|
|
1451
|
+
.option('--setup-hsm', 'Protect vault password with hardware security module')
|
|
1452
|
+
.option('--hsm-type <type>', 'HSM type: yubikey | gpg | pkcs11 (default: yubikey)')
|
|
1453
|
+
.option('--key-id <id>', 'GPG key ID or PKCS#11 key label')
|
|
1454
|
+
.option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library (.so / .dll)')
|
|
1455
|
+
.option('--global', 'Unlock the global vault at ~/.envcp')
|
|
1456
|
+
.action(async (options) => {
|
|
1457
|
+
await runUnlockFlow(options);
|
|
648
1458
|
});
|
|
649
1459
|
program
|
|
650
1460
|
.command('lock')
|
|
651
1461
|
.description('Lock EnvCP session')
|
|
652
1462
|
.option('--global', 'Lock the global vault session at ~/.envcp/.session')
|
|
653
1463
|
.action(async (options) => {
|
|
654
|
-
|
|
655
|
-
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
656
|
-
await sessionManager.init();
|
|
657
|
-
await sessionManager.destroy();
|
|
658
|
-
console.log(chalk.green('Session locked'));
|
|
1464
|
+
await runLockFlow(!!options.global);
|
|
659
1465
|
});
|
|
660
1466
|
program
|
|
661
1467
|
.command('status')
|
|
@@ -663,6 +1469,16 @@ program
|
|
|
663
1469
|
.option('--global', 'Check status of the global vault session at ~/.envcp/.session')
|
|
664
1470
|
.action(async (options) => {
|
|
665
1471
|
const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
|
|
1472
|
+
if (config.encryption?.enabled === false) {
|
|
1473
|
+
console.log(chalk.green('Ready'));
|
|
1474
|
+
console.log(chalk.gray(' Encryption is off, so no unlock is needed.'));
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
if (config.session?.enabled === false) {
|
|
1478
|
+
console.log(chalk.yellow('Session mode is off'));
|
|
1479
|
+
console.log(chalk.gray(' EnvCP will ask for the password each time.'));
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
666
1482
|
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
667
1483
|
await sessionManager.init();
|
|
668
1484
|
const password = await promptPassword('Enter password:');
|
|
@@ -679,9 +1495,13 @@ program
|
|
|
679
1495
|
console.log(chalk.gray(` Remaining: ${remaining} minutes`));
|
|
680
1496
|
console.log(chalk.gray(` Extensions remaining: ${maxExt - session.extensions}/${maxExt}`));
|
|
681
1497
|
});
|
|
682
|
-
program
|
|
1498
|
+
const configCommand = program
|
|
683
1499
|
.command('config')
|
|
684
|
-
.description('
|
|
1500
|
+
.description('Configure EnvCP settings')
|
|
1501
|
+
.action(async () => {
|
|
1502
|
+
await runConfigMenu();
|
|
1503
|
+
});
|
|
1504
|
+
configCommand
|
|
685
1505
|
.command('reload')
|
|
686
1506
|
.description('Reload config from envcp.yaml (requires password)')
|
|
687
1507
|
.action(async () => {
|
|
@@ -691,12 +1511,190 @@ program
|
|
|
691
1511
|
const result = await configGuard.reload(password);
|
|
692
1512
|
if (result.success) {
|
|
693
1513
|
console.log(chalk.green('Config reloaded successfully'));
|
|
694
|
-
console.log(chalk.gray(
|
|
1514
|
+
console.log(chalk.gray(` New config hash: ${configGuard.getHash()?.substring(0, 16)}...`));
|
|
695
1515
|
}
|
|
696
1516
|
else {
|
|
697
1517
|
console.log(chalk.red(result.error || 'Failed to reload config'));
|
|
698
1518
|
}
|
|
699
1519
|
});
|
|
1520
|
+
const ruleCommand = program
|
|
1521
|
+
.command('rule')
|
|
1522
|
+
.description('Manage EnvCP AI access rules')
|
|
1523
|
+
.action(async () => {
|
|
1524
|
+
await runRuleMenu();
|
|
1525
|
+
});
|
|
1526
|
+
ruleCommand
|
|
1527
|
+
.command('list')
|
|
1528
|
+
.description('List default, variable-specific, and who-specific AI rules')
|
|
1529
|
+
.option('--scope <scope>', 'Rule scope: merged | project | home', 'merged')
|
|
1530
|
+
.action(async (options) => {
|
|
1531
|
+
const scope = parseRuleScope(options.scope, 'merged');
|
|
1532
|
+
const config = await loadScopedConfig(process.cwd(), scope);
|
|
1533
|
+
const projectConfig = scope === 'merged' ? await loadScopedConfig(process.cwd(), 'project') : null;
|
|
1534
|
+
const homeConfig = scope === 'merged' ? await loadScopedConfig(process.cwd(), 'home') : null;
|
|
1535
|
+
console.log(chalk.blue('EnvCP rules'));
|
|
1536
|
+
console.log(chalk.gray(` Scope: ${scope}`));
|
|
1537
|
+
const formatOrigin = (origin) => scope === 'merged' ? ` (${origin})` : '';
|
|
1538
|
+
console.log(chalk.gray(` Default read: ${config.access.allow_ai_read ? 'allow' : 'deny'}${formatOrigin(scope === 'merged' ? describeMergedDefaultOrigin(config.access.allow_ai_read, projectConfig.access.allow_ai_read, homeConfig.access.allow_ai_read) : scope)}`));
|
|
1539
|
+
console.log(chalk.gray(` Default write: ${config.access.allow_ai_write ? 'allow' : 'deny'}${formatOrigin(scope === 'merged' ? describeMergedDefaultOrigin(config.access.allow_ai_write, projectConfig.access.allow_ai_write, homeConfig.access.allow_ai_write) : scope)}`));
|
|
1540
|
+
console.log(chalk.gray(` Default delete: ${config.access.allow_ai_delete ? 'allow' : 'deny'}${formatOrigin(scope === 'merged' ? describeMergedDefaultOrigin(config.access.allow_ai_delete, projectConfig.access.allow_ai_delete, homeConfig.access.allow_ai_delete) : scope)}`));
|
|
1541
|
+
console.log(chalk.gray(` Default export: ${config.access.allow_ai_export ? 'allow' : 'deny'}${formatOrigin(scope === 'merged' ? describeMergedDefaultOrigin(config.access.allow_ai_export, projectConfig.access.allow_ai_export, homeConfig.access.allow_ai_export) : scope)}`));
|
|
1542
|
+
console.log(chalk.gray(` Default run: ${config.access.allow_ai_execute ? 'allow' : 'deny'}${formatOrigin(scope === 'merged' ? describeMergedDefaultOrigin(config.access.allow_ai_execute, projectConfig.access.allow_ai_execute, homeConfig.access.allow_ai_execute) : scope)}`));
|
|
1543
|
+
console.log(chalk.gray(` Default list names: ${config.access.allow_ai_active_check ? 'allow' : 'deny'}${formatOrigin(scope === 'merged' ? describeMergedDefaultOrigin(config.access.allow_ai_active_check, projectConfig.access.allow_ai_active_check, homeConfig.access.allow_ai_active_check) : scope)}`));
|
|
1544
|
+
console.log(chalk.gray(` Default confirmation: ${config.access.require_confirmation ? 'on' : 'off'}${formatOrigin(scope === 'merged' ? describeMergedDefaultOrigin(config.access.require_confirmation, projectConfig.access.require_confirmation, homeConfig.access.require_confirmation) : scope)}`));
|
|
1545
|
+
const variableRules = Object.entries(config.access.variable_rules || {});
|
|
1546
|
+
if (variableRules.length === 0) {
|
|
1547
|
+
console.log(chalk.gray(' Variable rules: none'));
|
|
1548
|
+
}
|
|
1549
|
+
else {
|
|
1550
|
+
console.log(chalk.gray(' Variable rules:'));
|
|
1551
|
+
for (const [name, rule] of variableRules) {
|
|
1552
|
+
const origin = scope === 'merged'
|
|
1553
|
+
? describeMergedVariableRuleOrigin(name, rule, projectConfig.access.variable_rules || {}, homeConfig.access.variable_rules || {})
|
|
1554
|
+
: scope;
|
|
1555
|
+
console.log(chalk.gray(` ${name} [${origin}]`));
|
|
1556
|
+
for (const line of formatVariableRuleLines(rule)) {
|
|
1557
|
+
console.log(chalk.gray(` ${line}`));
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
const clientRules = Object.entries(config.access.client_rules || {});
|
|
1562
|
+
if (clientRules.length === 0) {
|
|
1563
|
+
console.log(chalk.gray(' Who rules: none'));
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
console.log(chalk.gray(' Who rules:'));
|
|
1567
|
+
for (const [clientId, rule] of clientRules) {
|
|
1568
|
+
const origin = scope === 'merged'
|
|
1569
|
+
? describeMergedClientRuleOrigin(clientId, rule, projectConfig.access.client_rules || {}, homeConfig.access.client_rules || {})
|
|
1570
|
+
: scope;
|
|
1571
|
+
console.log(chalk.gray(` ${formatClientLabel(clientId)} [${origin}]`));
|
|
1572
|
+
for (const line of formatClientRuleLines(rule)) {
|
|
1573
|
+
console.log(chalk.gray(` ${line}`));
|
|
1574
|
+
}
|
|
1575
|
+
const variableRules = Object.entries(rule.variable_rules || {});
|
|
1576
|
+
if (variableRules.length > 0) {
|
|
1577
|
+
console.log(chalk.gray(' variable rules:'));
|
|
1578
|
+
for (const [name, variableRule] of variableRules) {
|
|
1579
|
+
console.log(chalk.gray(` ${name}`));
|
|
1580
|
+
for (const line of formatVariableRuleLines(variableRule)) {
|
|
1581
|
+
console.log(chalk.gray(` ${line}`));
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (scope === 'merged') {
|
|
1587
|
+
console.log(chalk.gray(' Tip: use `envcp rule set-default ... --who <id>` or `envcp rule set-variable ... --who <id>` to edit one client.'));
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
ruleCommand
|
|
1591
|
+
.command('set-default <operation> <mode>')
|
|
1592
|
+
.description('Set a default AI rule, e.g. read allow | list deny | confirm allow')
|
|
1593
|
+
.option('--scope <scope>', 'Rule scope: project | home', 'project')
|
|
1594
|
+
.option('--who <who>', 'Target one client id, e.g. mcp | openai | gemini | api | cursor')
|
|
1595
|
+
.action(async (operation, mode, options) => {
|
|
1596
|
+
const field = parseDefaultRuleField(operation);
|
|
1597
|
+
if (!field) {
|
|
1598
|
+
throw new Error(`Unknown rule operation '${operation}'`);
|
|
1599
|
+
}
|
|
1600
|
+
if (!['allow', 'deny', 'inherit'].includes(mode)) {
|
|
1601
|
+
throw new Error(`Rule mode must be 'allow', 'deny', or 'inherit', got '${mode}'`);
|
|
1602
|
+
}
|
|
1603
|
+
const scope = parseRuleScope(options.scope, 'project');
|
|
1604
|
+
if (scope === 'merged') {
|
|
1605
|
+
throw new Error('set-default cannot write to merged scope. Use project or home.');
|
|
1606
|
+
}
|
|
1607
|
+
if (mode === 'inherit' && !options.who) {
|
|
1608
|
+
throw new Error('inherit is only supported with --who. Global defaults must be allow or deny.');
|
|
1609
|
+
}
|
|
1610
|
+
const config = await loadScopedConfig(process.cwd(), scope);
|
|
1611
|
+
applyDefaultRule(config, field, mode === 'inherit' ? undefined : mode === 'allow', options.who);
|
|
1612
|
+
await saveScopedConfig(config, process.cwd(), scope);
|
|
1613
|
+
const target = options.who ? ` for ${options.who}` : '';
|
|
1614
|
+
console.log(chalk.green(`Default ${operation} rule set to ${mode}${target} in ${scope} scope`));
|
|
1615
|
+
});
|
|
1616
|
+
ruleCommand
|
|
1617
|
+
.command('set-variable <name> <operation> <mode>')
|
|
1618
|
+
.description('Set a variable-specific AI rule, e.g. OPENAI_API_KEY run deny')
|
|
1619
|
+
.option('--scope <scope>', 'Rule scope: project | home', 'project')
|
|
1620
|
+
.option('--who <who>', 'Target one client id, e.g. mcp | openai | gemini | api | cursor')
|
|
1621
|
+
.action(async (name, operation, mode, options) => {
|
|
1622
|
+
const field = parseVariableRuleField(operation);
|
|
1623
|
+
if (!field) {
|
|
1624
|
+
throw new Error(`Unknown rule operation '${operation}'`);
|
|
1625
|
+
}
|
|
1626
|
+
if (!['allow', 'deny', 'inherit'].includes(mode)) {
|
|
1627
|
+
throw new Error(`Rule mode must be 'allow', 'deny', or 'inherit', got '${mode}'`);
|
|
1628
|
+
}
|
|
1629
|
+
const scope = parseRuleScope(options.scope, 'project');
|
|
1630
|
+
if (scope === 'merged') {
|
|
1631
|
+
throw new Error('set-variable cannot write to merged scope. Use project or home.');
|
|
1632
|
+
}
|
|
1633
|
+
const config = await loadScopedConfig(process.cwd(), scope);
|
|
1634
|
+
updateVariableRule(config, name, field, mode === 'inherit' ? undefined : mode === 'allow', options.who);
|
|
1635
|
+
await saveScopedConfig(config, process.cwd(), scope);
|
|
1636
|
+
const target = options.who ? ` for ${options.who}` : '';
|
|
1637
|
+
console.log(chalk.green(`Variable rule for ${name} ${operation} set to ${mode}${target} in ${scope} scope`));
|
|
1638
|
+
});
|
|
1639
|
+
ruleCommand
|
|
1640
|
+
.command('set-window <name> <start> <end>')
|
|
1641
|
+
.description('Set an active time window for a variable rule, e.g. OPENAI_API_KEY 09:00 18:00')
|
|
1642
|
+
.option('--scope <scope>', 'Rule scope: project | home', 'project')
|
|
1643
|
+
.option('--who <who>', 'Target one client id, e.g. mcp | openai | gemini | api | cursor')
|
|
1644
|
+
.action(async (name, start, end, options) => {
|
|
1645
|
+
const scope = parseRuleScope(options.scope, 'project');
|
|
1646
|
+
if (scope === 'merged') {
|
|
1647
|
+
throw new Error('set-window cannot write to merged scope. Use project or home.');
|
|
1648
|
+
}
|
|
1649
|
+
const config = await loadScopedConfig(process.cwd(), scope);
|
|
1650
|
+
setVariableRuleWindow(config, name, start, end, options.who);
|
|
1651
|
+
await saveScopedConfig(config, process.cwd(), scope);
|
|
1652
|
+
const target = options.who ? ` for ${options.who}` : '';
|
|
1653
|
+
console.log(chalk.green(`Variable rule window for ${name} set to ${start}-${end}${target} in ${scope} scope`));
|
|
1654
|
+
});
|
|
1655
|
+
ruleCommand
|
|
1656
|
+
.command('clear-window <name>')
|
|
1657
|
+
.description('Clear the active time window for a variable rule')
|
|
1658
|
+
.option('--scope <scope>', 'Rule scope: project | home', 'project')
|
|
1659
|
+
.option('--who <who>', 'Target one client id, e.g. mcp | openai | gemini | api | cursor')
|
|
1660
|
+
.action(async (name, options) => {
|
|
1661
|
+
const scope = parseRuleScope(options.scope, 'project');
|
|
1662
|
+
if (scope === 'merged') {
|
|
1663
|
+
throw new Error('clear-window cannot write to merged scope. Use project or home.');
|
|
1664
|
+
}
|
|
1665
|
+
const config = await loadScopedConfig(process.cwd(), scope);
|
|
1666
|
+
setVariableRuleWindow(config, name, undefined, undefined, options.who);
|
|
1667
|
+
await saveScopedConfig(config, process.cwd(), scope);
|
|
1668
|
+
const target = options.who ? ` for ${options.who}` : '';
|
|
1669
|
+
console.log(chalk.green(`Cleared variable rule window for ${name}${target} in ${scope} scope`));
|
|
1670
|
+
});
|
|
1671
|
+
ruleCommand
|
|
1672
|
+
.command('remove-variable <name>')
|
|
1673
|
+
.description('Remove all variable-specific rules for one variable')
|
|
1674
|
+
.option('--scope <scope>', 'Rule scope: project | home', 'project')
|
|
1675
|
+
.option('--who <who>', 'Target one client id, e.g. mcp | openai | gemini | api | cursor')
|
|
1676
|
+
.action(async (name, options) => {
|
|
1677
|
+
const scope = parseRuleScope(options.scope, 'project');
|
|
1678
|
+
if (scope === 'merged') {
|
|
1679
|
+
throw new Error('remove-variable cannot write to merged scope. Use project or home.');
|
|
1680
|
+
}
|
|
1681
|
+
const config = await loadScopedConfig(process.cwd(), scope);
|
|
1682
|
+
if (options.who) {
|
|
1683
|
+
const clientRule = getClientRule(config, options.who);
|
|
1684
|
+
const nextVariableRules = { ...(clientRule.variable_rules || {}) };
|
|
1685
|
+
delete nextVariableRules[name];
|
|
1686
|
+
clientRule.variable_rules = nextVariableRules;
|
|
1687
|
+
saveClientRule(config, options.who, clientRule);
|
|
1688
|
+
}
|
|
1689
|
+
else {
|
|
1690
|
+
const nextRules = { ...(config.access.variable_rules || {}) };
|
|
1691
|
+
delete nextRules[name];
|
|
1692
|
+
config.access.variable_rules = nextRules;
|
|
1693
|
+
}
|
|
1694
|
+
await saveScopedConfig(config, process.cwd(), scope);
|
|
1695
|
+
const target = options.who ? ` for ${options.who}` : '';
|
|
1696
|
+
console.log(chalk.green(`Removed variable-specific rules for ${name}${target} in ${scope} scope`));
|
|
1697
|
+
});
|
|
700
1698
|
program
|
|
701
1699
|
.command('extend')
|
|
702
1700
|
.description('Extend session timeout')
|
|
@@ -1108,7 +2106,7 @@ program
|
|
|
1108
2106
|
const vaultPath = await resolveVaultPath(projectPath, config);
|
|
1109
2107
|
const sessionPath = resolveSessionPath(projectPath, config);
|
|
1110
2108
|
const mode = options.mode;
|
|
1111
|
-
const port = parseInt(options.port, 10);
|
|
2109
|
+
const port = Number.parseInt(options.port, 10);
|
|
1112
2110
|
const host = options.host;
|
|
1113
2111
|
const apiKey = options.apiKey;
|
|
1114
2112
|
let password = '';
|
|
@@ -1284,20 +2282,12 @@ program
|
|
|
1284
2282
|
console.log(chalk.red('Failed to decrypt. Wrong password or invalid file.'));
|
|
1285
2283
|
return;
|
|
1286
2284
|
}
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
if (!variables || typeof variables !== 'object') {
|
|
1290
|
-
console.log(chalk.red('Invalid export format'));
|
|
2285
|
+
const transferData = extractTransferVariables(importData, 'Invalid export format');
|
|
2286
|
+
if (!transferData) {
|
|
1291
2287
|
return;
|
|
1292
2288
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
if (meta.project)
|
|
1296
|
-
console.log(chalk.gray(` From project: ${meta.project}`));
|
|
1297
|
-
if (meta.timestamp)
|
|
1298
|
-
console.log(chalk.gray(` Exported: ${meta.timestamp}`));
|
|
1299
|
-
console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
|
|
1300
|
-
}
|
|
2289
|
+
const { meta, variables } = transferData;
|
|
2290
|
+
logTransferInfo('Import info:', meta, variables, { project: 'From project', timestamp: 'Exported' });
|
|
1301
2291
|
if (options.dryRun) {
|
|
1302
2292
|
const current = await storage.load();
|
|
1303
2293
|
const importNames = Object.keys(variables);
|
|
@@ -1402,20 +2392,12 @@ program
|
|
|
1402
2392
|
console.log(chalk.red('Failed to decrypt backup. Wrong password or corrupted file.'));
|
|
1403
2393
|
return;
|
|
1404
2394
|
}
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1407
|
-
if (!variables || typeof variables !== 'object') {
|
|
1408
|
-
console.log(chalk.red('Invalid backup format'));
|
|
2395
|
+
const transferData = extractTransferVariables(backupData, 'Invalid backup format');
|
|
2396
|
+
if (!transferData) {
|
|
1409
2397
|
return;
|
|
1410
2398
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
if (meta.project)
|
|
1414
|
-
console.log(chalk.gray(` Project: ${meta.project}`));
|
|
1415
|
-
if (meta.timestamp)
|
|
1416
|
-
console.log(chalk.gray(` Created: ${meta.timestamp}`));
|
|
1417
|
-
console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
|
|
1418
|
-
}
|
|
2399
|
+
const { meta, variables } = transferData;
|
|
2400
|
+
logTransferInfo('Backup info:', meta, variables, { project: 'Project', timestamp: 'Created' });
|
|
1419
2401
|
const confirm = await promptConfirm(options.merge ? 'Merge backup into current store?' : 'Replace current store with backup?', false);
|
|
1420
2402
|
if (!confirm) {
|
|
1421
2403
|
console.log(chalk.yellow('Restore cancelled'));
|
|
@@ -1592,7 +2574,7 @@ program
|
|
|
1592
2574
|
});
|
|
1593
2575
|
console.log(chalk.gray('\n [0] Cancel\n'));
|
|
1594
2576
|
const answer = await promptInput('Pick a version (0 to cancel):');
|
|
1595
|
-
const picked = parseInt(answer.trim(), 10);
|
|
2577
|
+
const picked = Number.parseInt(answer.trim(), 10);
|
|
1596
2578
|
if (!picked || picked < 1 || picked > releases.length) {
|
|
1597
2579
|
console.log(chalk.gray('Cancelled.'));
|
|
1598
2580
|
return;
|
|
@@ -1622,7 +2604,32 @@ program
|
|
|
1622
2604
|
console.log(chalk.red('\n Installation failed.'));
|
|
1623
2605
|
}
|
|
1624
2606
|
});
|
|
1625
|
-
|
|
2607
|
+
async function switchVaultContext(name) {
|
|
2608
|
+
const projectPath = process.cwd();
|
|
2609
|
+
if (name !== 'global' && name !== 'project') {
|
|
2610
|
+
const vaultDir = path.join(projectPath, '.envcp/vaults', name);
|
|
2611
|
+
if (!await pathExists(vaultDir)) {
|
|
2612
|
+
console.log(chalk.red(`Named vault "${name}" does not exist. Create it with: envcp vault --name ${name} init`));
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
await setActiveVault(projectPath, name);
|
|
2617
|
+
console.log(chalk.green(`Switched to vault: ${name}`));
|
|
2618
|
+
}
|
|
2619
|
+
async function listVaultContexts() {
|
|
2620
|
+
const projectPath = process.cwd();
|
|
2621
|
+
const config = await loadConfig(projectPath);
|
|
2622
|
+
const vaults = await listVaults(projectPath, config);
|
|
2623
|
+
console.log('Available vaults:');
|
|
2624
|
+
for (const vault of vaults) {
|
|
2625
|
+
const active = vault.active ? chalk.green(' (active)') : '';
|
|
2626
|
+
const exists = await pathExists(vault.path);
|
|
2627
|
+
const status = exists ? '' : chalk.gray(' [not initialized]');
|
|
2628
|
+
console.log(` ${vault.name}${active}${status}`);
|
|
2629
|
+
console.log(chalk.gray(` ${vault.path}`));
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
const vaultCommand = program
|
|
1626
2633
|
.command('vault')
|
|
1627
2634
|
.description('Manage vaults (global, project, or named)')
|
|
1628
2635
|
.option('--global', 'Operate on the global vault')
|
|
@@ -1731,38 +2738,18 @@ program
|
|
|
1731
2738
|
}
|
|
1732
2739
|
}, vaultOverride);
|
|
1733
2740
|
}));
|
|
1734
|
-
|
|
1735
|
-
.
|
|
2741
|
+
vaultCommand
|
|
2742
|
+
.addCommand(new Command('use')
|
|
1736
2743
|
.description('Switch active vault context')
|
|
1737
2744
|
.argument('<name>', 'Vault name (global, project, or named vault)')
|
|
1738
2745
|
.action(async (name) => {
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
if (!await pathExists(vaultDir)) {
|
|
1743
|
-
console.log(chalk.red(`Named vault "${name}" does not exist. Create it with: envcp vault --name ${name} init`));
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
await setActiveVault(projectPath, name);
|
|
1748
|
-
console.log(chalk.green(`Switched to vault: ${name}`));
|
|
1749
|
-
});
|
|
1750
|
-
program
|
|
1751
|
-
.command('vault-list')
|
|
2746
|
+
await switchVaultContext(name);
|
|
2747
|
+
}))
|
|
2748
|
+
.addCommand(new Command('contexts')
|
|
1752
2749
|
.description('List all available vaults')
|
|
1753
2750
|
.action(async () => {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
const vaults = await listVaults(projectPath, config);
|
|
1757
|
-
console.log('Available vaults:');
|
|
1758
|
-
for (const v of vaults) {
|
|
1759
|
-
const active = v.active ? chalk.green(' (active)') : '';
|
|
1760
|
-
const exists = await pathExists(v.path);
|
|
1761
|
-
const status = exists ? '' : chalk.gray(' [not initialized]');
|
|
1762
|
-
console.log(` ${v.name}${active}${status}`);
|
|
1763
|
-
console.log(chalk.gray(` ${v.path}`));
|
|
1764
|
-
}
|
|
1765
|
-
});
|
|
2751
|
+
await listVaultContexts();
|
|
2752
|
+
}));
|
|
1766
2753
|
program
|
|
1767
2754
|
.command('keychain')
|
|
1768
2755
|
.description('Manage OS keychain integration')
|
|
@@ -1892,7 +2879,7 @@ program
|
|
|
1892
2879
|
.option('--source <source>', 'Filter by source (cli, mcp, api)')
|
|
1893
2880
|
.option('--success', 'Show only successful operations')
|
|
1894
2881
|
.option('--failure', 'Show only failed operations')
|
|
1895
|
-
.option('--tail <n>', 'Show last N entries', parseInt)
|
|
2882
|
+
.option('--tail <n>', 'Show last N entries', (value) => Number.parseInt(value, 10))
|
|
1896
2883
|
.option('--dates', 'List all available log dates')
|
|
1897
2884
|
.option('--verify', 'Verify HMAC integrity of log entries')
|
|
1898
2885
|
.action(async (options) => {
|
|
@@ -2009,5 +2996,10 @@ program
|
|
|
2009
2996
|
console.log(chalk.gray('No log files to protect.'));
|
|
2010
2997
|
}
|
|
2011
2998
|
});
|
|
2012
|
-
|
|
2999
|
+
if (process.argv.length <= 2 && isInteractiveCli()) {
|
|
3000
|
+
await runInteractiveHome();
|
|
3001
|
+
}
|
|
3002
|
+
else {
|
|
3003
|
+
await program.parseAsync(process.argv);
|
|
3004
|
+
}
|
|
2013
3005
|
//# sourceMappingURL=index.js.map
|