lsh-framework 0.5.13 → 0.7.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 +493 -245
- package/dist/cli.js +130 -25
- package/dist/commands/theme.js +2 -1
- package/dist/lib/base-job-manager.js +2 -1
- package/dist/lib/job-manager.js +55 -7
- package/dist/lib/lshrc-init.js +3 -1
- package/dist/lib/secrets-manager.js +127 -0
- package/dist/services/secrets/secrets.js +33 -0
- package/package.json +19 -15
package/dist/cli.js
CHANGED
|
@@ -36,8 +36,12 @@ function getVersion() {
|
|
|
36
36
|
const program = new Command();
|
|
37
37
|
program
|
|
38
38
|
.name('lsh')
|
|
39
|
-
.description('LSH -
|
|
40
|
-
.version(getVersion())
|
|
39
|
+
.description('LSH - Encrypted secrets manager with automatic rotation and team sync')
|
|
40
|
+
.version(getVersion())
|
|
41
|
+
.showSuggestionAfterError(true)
|
|
42
|
+
.showHelpAfterError('(add --help for additional information)')
|
|
43
|
+
.allowUnknownOption(false)
|
|
44
|
+
.enablePositionalOptions();
|
|
41
45
|
// Options for main command
|
|
42
46
|
program
|
|
43
47
|
.option('-i, --interactive', 'Start interactive shell')
|
|
@@ -64,36 +68,36 @@ program
|
|
|
64
68
|
await startInteractiveShell(options);
|
|
65
69
|
}
|
|
66
70
|
else {
|
|
67
|
-
// No arguments - show
|
|
68
|
-
console.log('LSH -
|
|
71
|
+
// No arguments - show secrets-focused help
|
|
72
|
+
console.log('LSH - Encrypted Secrets Manager with Automatic Rotation');
|
|
69
73
|
console.log('');
|
|
70
|
-
console.log('
|
|
74
|
+
console.log('🔐 Secrets Management (Primary Features):');
|
|
75
|
+
console.log(' lib secrets sync Check sync status & get recommendations');
|
|
76
|
+
console.log(' lib secrets push Upload .env to encrypted cloud storage');
|
|
77
|
+
console.log(' lib secrets pull Download .env from cloud storage');
|
|
78
|
+
console.log(' lib secrets list List all stored environments');
|
|
79
|
+
console.log(' lib secrets show View secrets (masked)');
|
|
80
|
+
console.log(' lib secrets key Generate encryption key');
|
|
81
|
+
console.log(' lib secrets create Create new .env file');
|
|
71
82
|
console.log('');
|
|
72
|
-
console.log('
|
|
73
|
-
console.log('
|
|
74
|
-
console.log('
|
|
75
|
-
console.log('
|
|
76
|
-
console.log(' zsh ZSH compatibility commands');
|
|
77
|
-
console.log(' help Show detailed help');
|
|
83
|
+
console.log('🔄 Automation (Schedule secret rotation):');
|
|
84
|
+
console.log(' lib cron add Schedule automatic tasks');
|
|
85
|
+
console.log(' lib cron list List scheduled jobs');
|
|
86
|
+
console.log(' lib daemon start Start persistent daemon');
|
|
78
87
|
console.log('');
|
|
79
|
-
console.log('
|
|
80
|
-
console.log('
|
|
81
|
-
console.log('
|
|
82
|
-
console.log('
|
|
83
|
-
console.log(' self theme Manage themes');
|
|
84
|
-
console.log(' self zsh-import Import ZSH configs');
|
|
88
|
+
console.log('🚀 Quick Start:');
|
|
89
|
+
console.log(' lsh lib secrets key # Generate encryption key');
|
|
90
|
+
console.log(' lsh lib secrets push --env dev # Push your secrets');
|
|
91
|
+
console.log(' lsh lib secrets pull --env dev # Pull on another machine');
|
|
85
92
|
console.log('');
|
|
86
|
-
console.log('
|
|
93
|
+
console.log('📚 More Commands:');
|
|
87
94
|
console.log(' lib api API server management');
|
|
88
|
-
console.log(' lib daemon Daemon management');
|
|
89
|
-
console.log(' lib cron Cron job management');
|
|
90
|
-
console.log(' lib secrets Secrets management');
|
|
91
95
|
console.log(' lib supabase Supabase database management');
|
|
96
|
+
console.log(' self Self-management commands');
|
|
97
|
+
console.log(' -i, --interactive Start interactive shell');
|
|
98
|
+
console.log(' --help Show all options');
|
|
92
99
|
console.log('');
|
|
93
|
-
console.log('
|
|
94
|
-
console.log(' lsh -i Start interactive shell');
|
|
95
|
-
console.log(' lsh --help Show all options');
|
|
96
|
-
console.log(' lsh help Show detailed help with examples');
|
|
100
|
+
console.log('📖 Documentation: https://github.com/gwicho38/lsh');
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
catch (error) {
|
|
@@ -158,6 +162,50 @@ program
|
|
|
158
162
|
.action(() => {
|
|
159
163
|
showDetailedHelp();
|
|
160
164
|
});
|
|
165
|
+
/**
|
|
166
|
+
* Calculate string similarity (Levenshtein distance)
|
|
167
|
+
*/
|
|
168
|
+
function similarity(s1, s2) {
|
|
169
|
+
const longer = s1.length > s2.length ? s1 : s2;
|
|
170
|
+
const shorter = s1.length > s2.length ? s2 : s1;
|
|
171
|
+
if (longer.length === 0)
|
|
172
|
+
return 1.0;
|
|
173
|
+
const editDistance = levenshtein(longer, shorter);
|
|
174
|
+
return (longer.length - editDistance) / longer.length;
|
|
175
|
+
}
|
|
176
|
+
function levenshtein(s1, s2) {
|
|
177
|
+
const costs = [];
|
|
178
|
+
for (let i = 0; i <= s1.length; i++) {
|
|
179
|
+
let lastValue = i;
|
|
180
|
+
for (let j = 0; j <= s2.length; j++) {
|
|
181
|
+
if (i === 0) {
|
|
182
|
+
costs[j] = j;
|
|
183
|
+
}
|
|
184
|
+
else if (j > 0) {
|
|
185
|
+
let newValue = costs[j - 1];
|
|
186
|
+
if (s1.charAt(i - 1) !== s2.charAt(j - 1)) {
|
|
187
|
+
newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
|
|
188
|
+
}
|
|
189
|
+
costs[j - 1] = lastValue;
|
|
190
|
+
lastValue = newValue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (i > 0)
|
|
194
|
+
costs[s2.length] = lastValue;
|
|
195
|
+
}
|
|
196
|
+
return costs[s2.length];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Find similar commands for suggestions
|
|
200
|
+
*/
|
|
201
|
+
function findSimilarCommands(input, validCommands) {
|
|
202
|
+
const similarities = validCommands
|
|
203
|
+
.map(cmd => ({ cmd, score: similarity(input, cmd) }))
|
|
204
|
+
.filter(item => item.score > 0.5) // Only suggest if similarity > 50%
|
|
205
|
+
.sort((a, b) => b.score - a.score)
|
|
206
|
+
.slice(0, 3); // Top 3 suggestions
|
|
207
|
+
return similarities.map(item => item.cmd);
|
|
208
|
+
}
|
|
161
209
|
// Register async command modules
|
|
162
210
|
(async () => {
|
|
163
211
|
// REPL interactive shell
|
|
@@ -173,6 +221,63 @@ program
|
|
|
173
221
|
// Self-management commands with nested utilities
|
|
174
222
|
registerZshImportCommands(selfCommand);
|
|
175
223
|
registerThemeCommands(selfCommand);
|
|
224
|
+
// Pre-parse check for unknown commands
|
|
225
|
+
const args = process.argv.slice(2);
|
|
226
|
+
if (args.length > 0) {
|
|
227
|
+
const firstArg = args[0];
|
|
228
|
+
const validCommands = program.commands.map(cmd => cmd.name());
|
|
229
|
+
const validOptions = ['-i', '--interactive', '-c', '--command', '-s', '--script',
|
|
230
|
+
'--rc', '--zsh-compat', '--source-zshrc', '--package-manager',
|
|
231
|
+
'-v', '--verbose', '-d', '--debug', '-h', '--help', '-V', '--version'];
|
|
232
|
+
// Check if first argument looks like a command but isn't valid
|
|
233
|
+
if (!firstArg.startsWith('-') &&
|
|
234
|
+
!validCommands.includes(firstArg) &&
|
|
235
|
+
!validOptions.some(opt => args.includes(opt))) {
|
|
236
|
+
const suggestions = findSimilarCommands(firstArg, validCommands);
|
|
237
|
+
console.error(`error: unknown command '${firstArg}'`);
|
|
238
|
+
if (suggestions.length > 0) {
|
|
239
|
+
console.error(`\nDid you mean one of these?`);
|
|
240
|
+
suggestions.forEach(cmd => console.error(` ${cmd}`));
|
|
241
|
+
}
|
|
242
|
+
console.error(`\nRun 'lsh --help' to see available commands.`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Configure custom error output for better suggestions
|
|
247
|
+
program.configureOutput({
|
|
248
|
+
writeErr: (str) => {
|
|
249
|
+
// Intercept error messages to add suggestions
|
|
250
|
+
if (str.includes('error: unknown command')) {
|
|
251
|
+
const match = str.match(/unknown command '([^']+)'/);
|
|
252
|
+
if (match) {
|
|
253
|
+
const unknownCommand = match[1];
|
|
254
|
+
const validCommands = program.commands.map(cmd => cmd.name());
|
|
255
|
+
const suggestions = findSimilarCommands(unknownCommand, validCommands);
|
|
256
|
+
process.stderr.write(str);
|
|
257
|
+
if (suggestions.length > 0) {
|
|
258
|
+
process.stderr.write(`\nDid you mean one of these?\n`);
|
|
259
|
+
suggestions.forEach(cmd => process.stderr.write(` ${cmd}\n`));
|
|
260
|
+
}
|
|
261
|
+
process.stderr.write(`\nRun 'lsh --help' to see available commands.\n`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
process.stderr.write(str);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
// Add custom error handler for unknown commands
|
|
269
|
+
program.on('command:*', (operands) => {
|
|
270
|
+
const unknownCommand = operands[0];
|
|
271
|
+
const validCommands = program.commands.map(cmd => cmd.name());
|
|
272
|
+
const suggestions = findSimilarCommands(unknownCommand, validCommands);
|
|
273
|
+
console.error(`error: unknown command '${unknownCommand}'`);
|
|
274
|
+
if (suggestions.length > 0) {
|
|
275
|
+
console.error(`\nDid you mean one of these?`);
|
|
276
|
+
suggestions.forEach(cmd => console.error(` ${cmd}`));
|
|
277
|
+
}
|
|
278
|
+
console.error(`\nRun 'lsh --help' to see available commands.`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
});
|
|
176
281
|
// Parse command line arguments after all commands are registered
|
|
177
282
|
program.parse(process.argv);
|
|
178
283
|
})();
|
package/dist/commands/theme.js
CHANGED
|
@@ -61,7 +61,8 @@ export function registerThemeCommands(program) {
|
|
|
61
61
|
console.log(chalk.dim(' lsh theme import <name> # For Oh-My-Zsh themes'));
|
|
62
62
|
console.log(chalk.dim(' lsh theme apply <name>'));
|
|
63
63
|
console.log('');
|
|
64
|
-
process.exit(0)
|
|
64
|
+
// Note: Removed process.exit(0) to allow proper Jest testing
|
|
65
|
+
// Commander will handle exit automatically
|
|
65
66
|
});
|
|
66
67
|
themeCommand
|
|
67
68
|
.command('import <name>')
|
|
@@ -203,7 +203,8 @@ export class BaseJobManager extends EventEmitter {
|
|
|
203
203
|
async removeJob(jobId, force = false) {
|
|
204
204
|
const job = await this.getJob(jobId);
|
|
205
205
|
if (!job) {
|
|
206
|
-
|
|
206
|
+
// Return false instead of throwing when job doesn't exist
|
|
207
|
+
return false;
|
|
207
208
|
}
|
|
208
209
|
// Check if job is running
|
|
209
210
|
if (job.status === 'running' && !force) {
|
package/dist/lib/job-manager.js
CHANGED
|
@@ -14,13 +14,54 @@ export class JobManager extends BaseJobManager {
|
|
|
14
14
|
nextJobId = 1;
|
|
15
15
|
persistenceFile;
|
|
16
16
|
schedulerInterval;
|
|
17
|
+
initPromise;
|
|
17
18
|
constructor(persistenceFile = '/tmp/lsh-jobs.json') {
|
|
18
19
|
super(new MemoryJobStorage(), 'JobManager');
|
|
19
20
|
this.persistenceFile = persistenceFile;
|
|
20
|
-
this.loadPersistedJobs();
|
|
21
|
+
this.initPromise = this.loadPersistedJobs();
|
|
21
22
|
this.startScheduler();
|
|
22
23
|
this.setupCleanupHandlers();
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Wait for initialization to complete
|
|
27
|
+
*/
|
|
28
|
+
async ready() {
|
|
29
|
+
await this.initPromise;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a job and persist to filesystem
|
|
33
|
+
*/
|
|
34
|
+
async createJob(spec) {
|
|
35
|
+
const job = await super.createJob(spec);
|
|
36
|
+
await this.persistJobs();
|
|
37
|
+
return job;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Update a job and persist to filesystem
|
|
41
|
+
*/
|
|
42
|
+
async updateJob(jobId, updates) {
|
|
43
|
+
const job = await super.updateJob(jobId, updates);
|
|
44
|
+
await this.persistJobs();
|
|
45
|
+
return job;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Remove a job and persist to filesystem
|
|
49
|
+
*/
|
|
50
|
+
async removeJob(jobId, force = false) {
|
|
51
|
+
const result = await super.removeJob(jobId, force);
|
|
52
|
+
if (result) {
|
|
53
|
+
await this.persistJobs();
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Update job status and persist to filesystem
|
|
59
|
+
*/
|
|
60
|
+
async updateJobStatus(jobId, status, additionalUpdates) {
|
|
61
|
+
const job = await super.updateJobStatus(jobId, status, additionalUpdates);
|
|
62
|
+
await this.persistJobs();
|
|
63
|
+
return job;
|
|
64
|
+
}
|
|
24
65
|
/**
|
|
25
66
|
* Start a job (execute it as a process)
|
|
26
67
|
*/
|
|
@@ -67,14 +108,19 @@ export class JobManager extends BaseJobManager {
|
|
|
67
108
|
this.emit('jobOutput', job.id, 'stderr', data.toString());
|
|
68
109
|
});
|
|
69
110
|
// Handle completion
|
|
70
|
-
job.process.on('exit', (code, signal) => {
|
|
111
|
+
job.process.on('exit', async (code, signal) => {
|
|
112
|
+
// Check if job still exists (might have been removed during cleanup)
|
|
113
|
+
const existingJob = await this.getJob(job.id);
|
|
114
|
+
if (!existingJob) {
|
|
115
|
+
this.logger.debug(`Job ${job.id} already removed, skipping status update`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
71
118
|
const status = code === 0 ? 'completed' : (signal === 'SIGKILL' ? 'killed' : 'failed');
|
|
72
|
-
this.updateJobStatus(job.id, status, {
|
|
119
|
+
await this.updateJobStatus(job.id, status, {
|
|
73
120
|
completedAt: new Date(),
|
|
74
121
|
exitCode: code || undefined,
|
|
75
122
|
});
|
|
76
123
|
this.emit('jobCompleted', job, code, signal);
|
|
77
|
-
this.persistJobs();
|
|
78
124
|
});
|
|
79
125
|
// Set timeout if specified
|
|
80
126
|
if (job.timeout) {
|
|
@@ -87,7 +133,6 @@ export class JobManager extends BaseJobManager {
|
|
|
87
133
|
startedAt: new Date(),
|
|
88
134
|
pid: job.pid,
|
|
89
135
|
});
|
|
90
|
-
await this.persistJobs();
|
|
91
136
|
return updatedJob;
|
|
92
137
|
}
|
|
93
138
|
catch (error) {
|
|
@@ -96,7 +141,6 @@ export class JobManager extends BaseJobManager {
|
|
|
96
141
|
stderr: error.message,
|
|
97
142
|
});
|
|
98
143
|
this.emit('jobFailed', job, error);
|
|
99
|
-
await this.persistJobs();
|
|
100
144
|
throw error;
|
|
101
145
|
}
|
|
102
146
|
}
|
|
@@ -131,7 +175,6 @@ export class JobManager extends BaseJobManager {
|
|
|
131
175
|
const updatedJob = await this.updateJobStatus(jobId, 'stopped', {
|
|
132
176
|
completedAt: new Date(),
|
|
133
177
|
});
|
|
134
|
-
await this.persistJobs();
|
|
135
178
|
return updatedJob;
|
|
136
179
|
}
|
|
137
180
|
/**
|
|
@@ -289,6 +332,11 @@ export class JobManager extends BaseJobManager {
|
|
|
289
332
|
try {
|
|
290
333
|
if (fs.existsSync(this.persistenceFile)) {
|
|
291
334
|
const data = fs.readFileSync(this.persistenceFile, 'utf8');
|
|
335
|
+
// Handle empty file
|
|
336
|
+
if (!data || data.trim() === '') {
|
|
337
|
+
this.logger.info('Persistence file is empty, starting fresh');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
292
340
|
const persistedJobs = JSON.parse(data);
|
|
293
341
|
for (const job of persistedJobs) {
|
|
294
342
|
// Convert date strings back to Date objects
|
package/dist/lib/lshrc-init.js
CHANGED
|
@@ -11,7 +11,9 @@ const __dirname = path.dirname(__filename);
|
|
|
11
11
|
export class LshrcManager {
|
|
12
12
|
lshrcPath;
|
|
13
13
|
constructor(lshrcPath) {
|
|
14
|
-
|
|
14
|
+
// Use process.env.HOME if set (for testability), fallback to os.homedir()
|
|
15
|
+
const homeDir = process.env.HOME || os.homedir();
|
|
16
|
+
this.lshrcPath = lshrcPath || path.join(homeDir, '.lshrc');
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Initialize .lshrc if it doesn't exist
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Sync .env files across machines using encrypted Supabase storage
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
6
7
|
import * as crypto from 'crypto';
|
|
7
8
|
import DatabasePersistence from './database-persistence.js';
|
|
8
9
|
import { createLogger } from './logger.js';
|
|
@@ -212,5 +213,131 @@ export class SecretsManager {
|
|
|
212
213
|
}
|
|
213
214
|
console.log();
|
|
214
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Get status of secrets for an environment
|
|
218
|
+
*/
|
|
219
|
+
async status(envFilePath = '.env', environment = 'dev') {
|
|
220
|
+
const status = {
|
|
221
|
+
localExists: false,
|
|
222
|
+
localKeys: 0,
|
|
223
|
+
localModified: undefined,
|
|
224
|
+
cloudExists: false,
|
|
225
|
+
cloudKeys: 0,
|
|
226
|
+
cloudModified: undefined,
|
|
227
|
+
keySet: !!process.env.LSH_SECRETS_KEY,
|
|
228
|
+
keyMatches: undefined,
|
|
229
|
+
suggestions: [],
|
|
230
|
+
};
|
|
231
|
+
// Check local file
|
|
232
|
+
if (fs.existsSync(envFilePath)) {
|
|
233
|
+
status.localExists = true;
|
|
234
|
+
const stat = fs.statSync(envFilePath);
|
|
235
|
+
status.localModified = stat.mtime;
|
|
236
|
+
const content = fs.readFileSync(envFilePath, 'utf8');
|
|
237
|
+
const env = this.parseEnvFile(content);
|
|
238
|
+
status.localKeys = Object.keys(env).length;
|
|
239
|
+
}
|
|
240
|
+
// Check cloud storage
|
|
241
|
+
try {
|
|
242
|
+
const jobs = await this.persistence.getActiveJobs();
|
|
243
|
+
const secretsJobs = jobs
|
|
244
|
+
.filter(j => j.command === 'secrets_sync' && j.job_id.includes(environment))
|
|
245
|
+
.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime());
|
|
246
|
+
if (secretsJobs.length > 0) {
|
|
247
|
+
status.cloudExists = true;
|
|
248
|
+
const latestSecret = secretsJobs[0];
|
|
249
|
+
status.cloudModified = new Date(latestSecret.completed_at || latestSecret.started_at);
|
|
250
|
+
// Try to decrypt to check if key matches
|
|
251
|
+
if (latestSecret.output) {
|
|
252
|
+
try {
|
|
253
|
+
const decrypted = this.decrypt(latestSecret.output);
|
|
254
|
+
const env = this.parseEnvFile(decrypted);
|
|
255
|
+
status.cloudKeys = Object.keys(env).length;
|
|
256
|
+
status.keyMatches = true;
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
status.keyMatches = false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
// Cloud check failed, likely no connection
|
|
266
|
+
}
|
|
267
|
+
return status;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Sync command - check status and suggest actions
|
|
271
|
+
*/
|
|
272
|
+
async sync(envFilePath = '.env', environment = 'dev') {
|
|
273
|
+
console.log(`\n🔍 Checking secrets status for environment: ${environment}\n`);
|
|
274
|
+
const status = await this.status(envFilePath, environment);
|
|
275
|
+
// Display status
|
|
276
|
+
console.log('📊 Status:');
|
|
277
|
+
console.log(` Encryption key set: ${status.keySet ? '✅' : '❌'}`);
|
|
278
|
+
console.log(` Local .env file: ${status.localExists ? `✅ (${status.localKeys} keys)` : '❌'}`);
|
|
279
|
+
console.log(` Cloud storage: ${status.cloudExists ? `✅ (${status.cloudKeys} keys)` : '❌'}`);
|
|
280
|
+
if (status.cloudExists && status.keyMatches !== undefined) {
|
|
281
|
+
console.log(` Key matches cloud: ${status.keyMatches ? '✅' : '❌'}`);
|
|
282
|
+
}
|
|
283
|
+
console.log();
|
|
284
|
+
// Generate suggestions
|
|
285
|
+
const suggestions = [];
|
|
286
|
+
if (!status.keySet) {
|
|
287
|
+
suggestions.push('⚠️ No encryption key set!');
|
|
288
|
+
suggestions.push(' Generate a key: lsh lib secrets key');
|
|
289
|
+
suggestions.push(' Add it to .env: LSH_SECRETS_KEY=<your-key>');
|
|
290
|
+
suggestions.push(' Load it: export $(cat .env | xargs)');
|
|
291
|
+
}
|
|
292
|
+
if (status.cloudExists && status.keyMatches === false) {
|
|
293
|
+
suggestions.push('⚠️ Encryption key does not match cloud storage!');
|
|
294
|
+
suggestions.push(' Either use the original key, or push new secrets:');
|
|
295
|
+
suggestions.push(` lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
296
|
+
}
|
|
297
|
+
if (!status.localExists && status.cloudExists && status.keyMatches) {
|
|
298
|
+
suggestions.push('💡 Cloud secrets available but no local file');
|
|
299
|
+
suggestions.push(` Pull from cloud: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
|
|
300
|
+
}
|
|
301
|
+
if (status.localExists && !status.cloudExists) {
|
|
302
|
+
suggestions.push('💡 Local .env exists but not in cloud');
|
|
303
|
+
suggestions.push(` Push to cloud: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
304
|
+
}
|
|
305
|
+
if (status.localExists && status.cloudExists && status.keyMatches) {
|
|
306
|
+
if (status.localModified && status.cloudModified) {
|
|
307
|
+
const localNewer = status.localModified > status.cloudModified;
|
|
308
|
+
const timeDiff = Math.abs(status.localModified.getTime() - status.cloudModified.getTime());
|
|
309
|
+
const daysDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
|
|
310
|
+
if (localNewer && daysDiff > 0) {
|
|
311
|
+
suggestions.push('💡 Local file is newer than cloud');
|
|
312
|
+
suggestions.push(` Push to cloud: lsh lib secrets push -f ${envFilePath} -e ${environment}`);
|
|
313
|
+
}
|
|
314
|
+
else if (!localNewer && daysDiff > 0) {
|
|
315
|
+
suggestions.push('💡 Cloud is newer than local file');
|
|
316
|
+
suggestions.push(` Pull from cloud: lsh lib secrets pull -f ${envFilePath} -e ${environment}`);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
suggestions.push('✅ Local and cloud are in sync!');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Show how to load secrets in current shell
|
|
324
|
+
if (status.localExists && status.keySet) {
|
|
325
|
+
suggestions.push('');
|
|
326
|
+
suggestions.push('📝 To load secrets in your current shell:');
|
|
327
|
+
suggestions.push(` export $(cat ${envFilePath} | grep -v '^#' | xargs)`);
|
|
328
|
+
suggestions.push('');
|
|
329
|
+
suggestions.push(' Or for safer loading (with quotes):');
|
|
330
|
+
suggestions.push(` set -a; source ${envFilePath}; set +a`);
|
|
331
|
+
suggestions.push('');
|
|
332
|
+
suggestions.push('💡 Add to your shell profile for auto-loading:');
|
|
333
|
+
suggestions.push(` echo "set -a; source ${path.resolve(envFilePath)}; set +a" >> ~/.zshrc`);
|
|
334
|
+
}
|
|
335
|
+
// Display suggestions
|
|
336
|
+
if (suggestions.length > 0) {
|
|
337
|
+
console.log('📋 Recommendations:\n');
|
|
338
|
+
suggestions.forEach(s => console.log(s));
|
|
339
|
+
console.log();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
215
342
|
}
|
|
216
343
|
export default SecretsManager;
|
|
@@ -154,6 +154,39 @@ API_KEY=
|
|
|
154
154
|
process.exit(1);
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
|
+
// Sync command - check status and suggest actions
|
|
158
|
+
secretsCmd
|
|
159
|
+
.command('sync')
|
|
160
|
+
.description('Check secrets sync status and show recommended actions')
|
|
161
|
+
.option('-f, --file <path>', 'Path to .env file', '.env')
|
|
162
|
+
.option('-e, --env <name>', 'Environment name', 'dev')
|
|
163
|
+
.action(async (options) => {
|
|
164
|
+
try {
|
|
165
|
+
const manager = new SecretsManager();
|
|
166
|
+
await manager.sync(options.file, options.env);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
console.error('❌ Failed to check sync status:', error.message);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
// Status command - get detailed status info
|
|
174
|
+
secretsCmd
|
|
175
|
+
.command('status')
|
|
176
|
+
.description('Get detailed secrets status (JSON output)')
|
|
177
|
+
.option('-f, --file <path>', 'Path to .env file', '.env')
|
|
178
|
+
.option('-e, --env <name>', 'Environment name', 'dev')
|
|
179
|
+
.action(async (options) => {
|
|
180
|
+
try {
|
|
181
|
+
const manager = new SecretsManager();
|
|
182
|
+
const status = await manager.status(options.file, options.env);
|
|
183
|
+
console.log(JSON.stringify(status, null, 2));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.error('❌ Failed to get status:', error.message);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
157
190
|
// Delete .env file with confirmation
|
|
158
191
|
secretsCmd
|
|
159
192
|
.command('delete')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lsh-framework",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Encrypted secrets manager with automatic rotation, team sync, and multi-environment support. Built on a powerful shell with daemon scheduling and CI/CD integration.",
|
|
5
5
|
"main": "dist/app.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"lsh": "./dist/cli.js"
|
|
@@ -36,21 +36,25 @@
|
|
|
36
36
|
"audit:security": "npm audit --audit-level moderate"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [
|
|
39
|
-
"
|
|
39
|
+
"secrets-manager",
|
|
40
|
+
"secrets",
|
|
41
|
+
"env-manager",
|
|
42
|
+
"dotenv",
|
|
43
|
+
"environment-variables",
|
|
44
|
+
"encryption",
|
|
45
|
+
"credential-management",
|
|
46
|
+
"team-sync",
|
|
47
|
+
"secrets-rotation",
|
|
48
|
+
"multi-environment",
|
|
49
|
+
"devops",
|
|
50
|
+
"security",
|
|
40
51
|
"shell",
|
|
41
|
-
"
|
|
42
|
-
"job-manager",
|
|
52
|
+
"automation",
|
|
43
53
|
"cron",
|
|
44
54
|
"daemon",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"database-persistence",
|
|
49
|
-
"task-scheduler",
|
|
50
|
-
"command-line",
|
|
51
|
-
"automation",
|
|
52
|
-
"devops",
|
|
53
|
-
"cicd"
|
|
55
|
+
"job-scheduler",
|
|
56
|
+
"cicd",
|
|
57
|
+
"cli"
|
|
54
58
|
],
|
|
55
59
|
"engines": {
|
|
56
60
|
"node": ">=20.18.0",
|
|
@@ -138,7 +142,7 @@
|
|
|
138
142
|
"eslint-plugin-react": "^7.37.5",
|
|
139
143
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
140
144
|
"supertest": "^7.1.4",
|
|
141
|
-
"ts-jest": "^29.
|
|
145
|
+
"ts-jest": "^29.2.5",
|
|
142
146
|
"typescript": "^5.4.5"
|
|
143
147
|
}
|
|
144
148
|
}
|