lsh-framework 1.8.2 ā 2.0.1
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/dist/cli.js +2 -0
- package/dist/commands/migrate.js +105 -0
- package/dist/lib/secrets-manager.js +34 -8
- package/dist/services/secrets/secrets.js +10 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { registerCompletionCommands } from './commands/completion.js';
|
|
|
11
11
|
import { registerConfigCommands } from './commands/config.js';
|
|
12
12
|
import { registerSyncHistoryCommands } from './commands/sync-history.js';
|
|
13
13
|
import { registerIPFSCommands } from './commands/ipfs.js';
|
|
14
|
+
import { registerMigrateCommand } from './commands/migrate.js';
|
|
14
15
|
import { init_daemon } from './services/daemon/daemon.js';
|
|
15
16
|
import { init_supabase } from './services/supabase/supabase.js';
|
|
16
17
|
import { init_cron } from './services/cron/cron.js';
|
|
@@ -146,6 +147,7 @@ function findSimilarCommands(input, validCommands) {
|
|
|
146
147
|
registerConfigCommands(program);
|
|
147
148
|
registerSyncHistoryCommands(program);
|
|
148
149
|
registerIPFSCommands(program);
|
|
150
|
+
registerMigrateCommand(program);
|
|
149
151
|
// Secrets management (primary feature)
|
|
150
152
|
await init_secrets(program);
|
|
151
153
|
// Supporting services
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Command - Help users migrate from v1.x to v2.0
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { getGitRepoInfo } from '../lib/git-utils.js';
|
|
8
|
+
export function registerMigrateCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('migrate')
|
|
11
|
+
.description('Migrate from v1.x to v2.0 environment naming')
|
|
12
|
+
.option('--dry-run', 'Show what would be migrated without doing it')
|
|
13
|
+
.option('--v1-compat', 'Enable v1.x compatibility mode')
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
if (options.v1Compat) {
|
|
16
|
+
console.log('\nš Enabling v1.x Compatibility Mode\n');
|
|
17
|
+
console.log('Add this to your shell profile (~/.bashrc or ~/.zshrc):');
|
|
18
|
+
console.log('');
|
|
19
|
+
console.log(' export LSH_V1_COMPAT=true');
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log('This will keep v1.x behavior (repo_dev naming).');
|
|
22
|
+
console.log('');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.log('\nš LSH v2.0 Migration Guide\n');
|
|
26
|
+
console.log('ā'.repeat(60));
|
|
27
|
+
console.log('');
|
|
28
|
+
// Check if in git repo
|
|
29
|
+
const gitInfo = getGitRepoInfo(process.cwd());
|
|
30
|
+
if (!gitInfo?.isGitRepo) {
|
|
31
|
+
console.log('ā¹ļø Not in a git repository');
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log('v2.0 only affects git repositories.');
|
|
34
|
+
console.log('Outside git repos, behavior is unchanged (default: dev)');
|
|
35
|
+
console.log('');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log('š Current Repository:', gitInfo.repoName);
|
|
39
|
+
console.log('šæ Branch:', gitInfo.currentBranch);
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log('š Environment Naming Changes:\n');
|
|
42
|
+
console.log(' v1.x Behavior:');
|
|
43
|
+
console.log(` lsh push ā ${gitInfo.repoName}_dev`);
|
|
44
|
+
console.log(` lsh push --env dev ā ${gitInfo.repoName}_dev`);
|
|
45
|
+
console.log(` lsh push --env staging ā ${gitInfo.repoName}_staging`);
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(' v2.0 Behavior:');
|
|
48
|
+
console.log(` lsh push ā ${gitInfo.repoName} (simpler!)`);
|
|
49
|
+
console.log(` lsh push --env dev ā ${gitInfo.repoName}_dev`);
|
|
50
|
+
console.log(` lsh push --env staging ā ${gitInfo.repoName}_staging`);
|
|
51
|
+
console.log('');
|
|
52
|
+
// Check for existing v1.x cache
|
|
53
|
+
const cacheDir = path.join(homedir(), '.lsh', 'secrets-cache');
|
|
54
|
+
const repoName = gitInfo.repoName || 'unknown';
|
|
55
|
+
const v1EnvName = `${repoName}_dev`;
|
|
56
|
+
const v2EnvName = repoName;
|
|
57
|
+
let hasV1Data = false;
|
|
58
|
+
let hasV2Data = false;
|
|
59
|
+
if (fs.existsSync(cacheDir)) {
|
|
60
|
+
const files = fs.readdirSync(cacheDir);
|
|
61
|
+
hasV1Data = files.some(f => f.includes(v1EnvName));
|
|
62
|
+
hasV2Data = files.some(f => f.includes(v2EnvName) && !f.includes(`${v2EnvName}_`));
|
|
63
|
+
}
|
|
64
|
+
console.log('š¾ Your Current Data:\n');
|
|
65
|
+
if (hasV1Data) {
|
|
66
|
+
console.log(` ā
v1.x data found: ${v1EnvName}`);
|
|
67
|
+
}
|
|
68
|
+
if (hasV2Data) {
|
|
69
|
+
console.log(` ā
v2.0 data found: ${v2EnvName}`);
|
|
70
|
+
}
|
|
71
|
+
if (!hasV1Data && !hasV2Data) {
|
|
72
|
+
console.log(' ā¹ļø No local cache found (fresh start)');
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log('š§ Migration Steps:\n');
|
|
76
|
+
if (hasV1Data && !hasV2Data) {
|
|
77
|
+
console.log(' You have v1.x data. To migrate:\n');
|
|
78
|
+
console.log(' 1. Push with explicit environment name:');
|
|
79
|
+
console.log(` lsh push --env dev # Keeps v1.x format (${v1EnvName})`);
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log(' 2. Or start using v2.0 format:');
|
|
82
|
+
console.log(` lsh push # New v2.0 format (${v2EnvName})`);
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log(' š” Both formats can coexist. Use --env dev to access old data.');
|
|
85
|
+
}
|
|
86
|
+
else if (hasV2Data) {
|
|
87
|
+
console.log(' ā
You\'re already using v2.0 format!');
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(` Continue using: lsh push # ā ${v2EnvName}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(' ā
Fresh start - you\'re ready for v2.0!');
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(` Start using: lsh push # ā ${v2EnvName}`);
|
|
95
|
+
}
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log('ā'.repeat(60));
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log('š For more information:');
|
|
100
|
+
console.log(' https://github.com/gwicho38/lsh/blob/main/docs/releases/2.0.0.md');
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('š” Tip: Use --v1-compat flag to keep v1.x behavior');
|
|
103
|
+
console.log('');
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -175,6 +175,8 @@ export class SecretsManager {
|
|
|
175
175
|
if (filename !== '.env' && !filename.startsWith('.env.')) {
|
|
176
176
|
throw new Error(`Invalid filename: ${filename}. Must be '.env' or start with '.env.'`);
|
|
177
177
|
}
|
|
178
|
+
// Get the effective environment name (repo-aware)
|
|
179
|
+
const effectiveEnv = this.getRepoAwareEnvironment(environment);
|
|
178
180
|
// Warn if using default key
|
|
179
181
|
if (!process.env.LSH_SECRETS_KEY) {
|
|
180
182
|
logger.warn('ā ļø Warning: No LSH_SECRETS_KEY set. Using machine-specific key.');
|
|
@@ -182,15 +184,15 @@ export class SecretsManager {
|
|
|
182
184
|
logger.warn(' Then add LSH_SECRETS_KEY=<key> to your .env on all machines');
|
|
183
185
|
console.log();
|
|
184
186
|
}
|
|
185
|
-
logger.info(`Pushing ${envFilePath} to IPFS (${
|
|
187
|
+
logger.info(`Pushing ${envFilePath} to IPFS (${effectiveEnv})...`);
|
|
186
188
|
const content = fs.readFileSync(envFilePath, 'utf8');
|
|
187
189
|
const env = this.parseEnvFile(content);
|
|
188
190
|
// Check for destructive changes unless force is true
|
|
189
191
|
if (!force) {
|
|
190
192
|
try {
|
|
191
193
|
// Check if secrets already exist for this environment
|
|
192
|
-
if (this.storage.exists(
|
|
193
|
-
const existingSecrets = await this.storage.pull(
|
|
194
|
+
if (this.storage.exists(effectiveEnv, this.gitInfo?.repoName)) {
|
|
195
|
+
const existingSecrets = await this.storage.pull(effectiveEnv, this.encryptionKey, this.gitInfo?.repoName);
|
|
194
196
|
const cloudEnv = {};
|
|
195
197
|
existingSecrets.forEach(s => {
|
|
196
198
|
cloudEnv[s.key] = s.value;
|
|
@@ -214,16 +216,16 @@ export class SecretsManager {
|
|
|
214
216
|
const secrets = Object.entries(env).map(([key, value]) => ({
|
|
215
217
|
key,
|
|
216
218
|
value,
|
|
217
|
-
environment,
|
|
219
|
+
environment: effectiveEnv,
|
|
218
220
|
createdAt: new Date(),
|
|
219
221
|
updatedAt: new Date(),
|
|
220
222
|
}));
|
|
221
223
|
// Store on IPFS
|
|
222
|
-
const cid = await this.storage.push(secrets,
|
|
224
|
+
const cid = await this.storage.push(secrets, effectiveEnv, this.encryptionKey, this.gitInfo?.repoName, this.gitInfo?.currentBranch);
|
|
223
225
|
logger.info(`ā
Pushed ${secrets.length} secrets from ${filename} to IPFS`);
|
|
224
226
|
console.log(`š¦ IPFS CID: ${cid}`);
|
|
225
227
|
// Log to IPFS for immutable audit record
|
|
226
|
-
await this.logToIPFS('push',
|
|
228
|
+
await this.logToIPFS('push', effectiveEnv, secrets.length);
|
|
227
229
|
}
|
|
228
230
|
/**
|
|
229
231
|
* Pull .env from IPFS
|
|
@@ -363,15 +365,39 @@ export class SecretsManager {
|
|
|
363
365
|
}
|
|
364
366
|
return status;
|
|
365
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Get the default environment name based on context
|
|
370
|
+
* v2.0: In git repo, default is repo name; otherwise 'dev'
|
|
371
|
+
*/
|
|
372
|
+
getDefaultEnvironment() {
|
|
373
|
+
// Check for v1 compatibility mode
|
|
374
|
+
if (process.env.LSH_V1_COMPAT === 'true') {
|
|
375
|
+
return 'dev'; // v1.x behavior
|
|
376
|
+
}
|
|
377
|
+
// v2.0 behavior: use repo name as default in git repos
|
|
378
|
+
if (this.gitInfo?.repoName) {
|
|
379
|
+
return ''; // Empty string signals "use repo name only"
|
|
380
|
+
}
|
|
381
|
+
return 'dev';
|
|
382
|
+
}
|
|
366
383
|
/**
|
|
367
384
|
* Get repo-aware environment namespace
|
|
368
|
-
* Returns environment name with repo context if in a git repo
|
|
385
|
+
* v2.0: Returns environment name with repo context if in a git repo
|
|
386
|
+
*
|
|
387
|
+
* Behavior:
|
|
388
|
+
* - Empty env in repo: returns just repo name (v2.0 default)
|
|
389
|
+
* - Named env in repo: returns repo_env (e.g., repo_staging)
|
|
390
|
+
* - Any env outside repo: returns env as-is
|
|
369
391
|
*/
|
|
370
392
|
getRepoAwareEnvironment(environment) {
|
|
371
393
|
if (this.gitInfo?.repoName) {
|
|
394
|
+
// v2.0: Empty environment means "use repo name only"
|
|
395
|
+
if (environment === '' || environment === 'default') {
|
|
396
|
+
return this.gitInfo.repoName;
|
|
397
|
+
}
|
|
372
398
|
return `${this.gitInfo.repoName}_${environment}`;
|
|
373
399
|
}
|
|
374
|
-
return environment;
|
|
400
|
+
return environment || 'dev'; // Default to 'dev' outside git repos
|
|
375
401
|
}
|
|
376
402
|
/**
|
|
377
403
|
* Generate encryption key if not set
|
|
@@ -18,7 +18,9 @@ export async function init_secrets(program) {
|
|
|
18
18
|
.action(async (options) => {
|
|
19
19
|
const manager = new SecretsManager();
|
|
20
20
|
try {
|
|
21
|
-
|
|
21
|
+
// v2.0: Use context-aware default environment
|
|
22
|
+
const env = options.env === 'dev' ? manager.getDefaultEnvironment() : options.env;
|
|
23
|
+
await manager.push(options.file, env, options.force);
|
|
22
24
|
}
|
|
23
25
|
catch (error) {
|
|
24
26
|
const err = error;
|
|
@@ -40,7 +42,9 @@ export async function init_secrets(program) {
|
|
|
40
42
|
.action(async (options) => {
|
|
41
43
|
const manager = new SecretsManager();
|
|
42
44
|
try {
|
|
43
|
-
|
|
45
|
+
// v2.0: Use context-aware default environment
|
|
46
|
+
const env = options.env === 'dev' ? manager.getDefaultEnvironment() : options.env;
|
|
47
|
+
await manager.pull(options.file, env, options.force);
|
|
44
48
|
}
|
|
45
49
|
catch (error) {
|
|
46
50
|
const err = error;
|
|
@@ -273,13 +277,15 @@ API_KEY=
|
|
|
273
277
|
.action(async (options) => {
|
|
274
278
|
const manager = new SecretsManager();
|
|
275
279
|
try {
|
|
280
|
+
// v2.0: Use context-aware default environment
|
|
281
|
+
const env = options.env === 'dev' ? manager.getDefaultEnvironment() : options.env;
|
|
276
282
|
if (options.legacy) {
|
|
277
283
|
// Use legacy sync (suggestions only)
|
|
278
|
-
await manager.sync(options.file,
|
|
284
|
+
await manager.sync(options.file, env);
|
|
279
285
|
}
|
|
280
286
|
else {
|
|
281
287
|
// Use new smart sync (auto-execute)
|
|
282
|
-
await manager.smartSync(options.file,
|
|
288
|
+
await manager.smartSync(options.file, env, !options.dryRun, options.load, options.force, options.forceRekey);
|
|
283
289
|
}
|
|
284
290
|
}
|
|
285
291
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lsh-framework",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Simple, cross-platform encrypted secrets manager with automatic sync, IPFS audit logs, and multi-environment support. Just run lsh sync and start managing your secrets.",
|
|
5
5
|
"main": "dist/app.js",
|
|
6
6
|
"bin": {
|