lsh-framework 3.1.24 → 3.1.26
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 +3 -0
- package/dist/constants/ui.js +1 -0
- package/dist/lib/ipfs-client-manager.js +54 -2
- package/dist/lib/ipfs-sync.js +5 -3
- package/dist/services/secrets/secrets.js +125 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { registerSyncHistoryCommands } from './commands/sync-history.js';
|
|
|
13
13
|
import { registerSyncCommands } from './commands/sync.js';
|
|
14
14
|
import { registerMigrateCommand } from './commands/migrate.js';
|
|
15
15
|
import { registerContextCommand } from './commands/context.js';
|
|
16
|
+
import { registerIPFSCommands } from './commands/ipfs.js';
|
|
16
17
|
import { init_secrets } from './services/secrets/secrets.js';
|
|
17
18
|
import { loadGlobalConfigSync } from './lib/config-manager.js';
|
|
18
19
|
import { CLI_TEXT, CLI_HELP } from './constants/ui.js';
|
|
@@ -62,6 +63,7 @@ program
|
|
|
62
63
|
console.log(CLI_HELP.CMD_GET);
|
|
63
64
|
console.log(CLI_HELP.CMD_SET);
|
|
64
65
|
console.log(CLI_HELP.CMD_DELETE);
|
|
66
|
+
console.log(CLI_HELP.CMD_CP);
|
|
65
67
|
console.log(CLI_HELP.CMD_STATUS);
|
|
66
68
|
console.log('');
|
|
67
69
|
console.log(CLI_HELP.SECTION_IPFS);
|
|
@@ -146,6 +148,7 @@ function findSimilarCommands(input, validCommands) {
|
|
|
146
148
|
registerSyncCommands(program);
|
|
147
149
|
registerMigrateCommand(program);
|
|
148
150
|
registerContextCommand(program);
|
|
151
|
+
registerIPFSCommands(program);
|
|
149
152
|
// Secrets management (primary feature)
|
|
150
153
|
await init_secrets(program);
|
|
151
154
|
// Shell completion
|
package/dist/constants/ui.js
CHANGED
|
@@ -232,6 +232,7 @@ export const CLI_HELP = {
|
|
|
232
232
|
CMD_GET: ' get <key> Get a specific secret value (--all for all)',
|
|
233
233
|
CMD_SET: ' set <key> <value> Set a specific secret value',
|
|
234
234
|
CMD_DELETE: ' delete Delete .env file',
|
|
235
|
+
CMD_CP: ' cp <from> <to> Copy env variables between files (--name to copy one variable)',
|
|
235
236
|
CMD_STATUS: ' status Get detailed secrets status',
|
|
236
237
|
// IPFS commands
|
|
237
238
|
CMD_SYNC_INIT: ' sync init Full IPFS setup (install, init, start)',
|
|
@@ -171,6 +171,12 @@ export class IPFSClientManager {
|
|
|
171
171
|
throw error;
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Get the IPFS repo path used by lsh
|
|
176
|
+
*/
|
|
177
|
+
getRepoPath() {
|
|
178
|
+
return path.join(this.ipfsDir, 'repo');
|
|
179
|
+
}
|
|
174
180
|
/**
|
|
175
181
|
* Start IPFS daemon
|
|
176
182
|
*/
|
|
@@ -179,9 +185,23 @@ export class IPFSClientManager {
|
|
|
179
185
|
if (!clientInfo.installed) {
|
|
180
186
|
throw new Error('IPFS client not installed. Run: lsh ipfs install');
|
|
181
187
|
}
|
|
182
|
-
|
|
183
|
-
const ipfsRepoPath = path.join(this.ipfsDir, 'repo');
|
|
188
|
+
const ipfsRepoPath = this.getRepoPath();
|
|
184
189
|
const ipfsCmd = clientInfo.path || 'ipfs';
|
|
190
|
+
// Auto-initialize repo if it doesn't exist
|
|
191
|
+
if (!fs.existsSync(path.join(ipfsRepoPath, 'config'))) {
|
|
192
|
+
logger.info('🔧 IPFS repository not found, initializing...');
|
|
193
|
+
try {
|
|
194
|
+
await execAsync(`${ipfsCmd} init`, {
|
|
195
|
+
env: { ...process.env, IPFS_PATH: ipfsRepoPath },
|
|
196
|
+
});
|
|
197
|
+
logger.info('✅ IPFS repository initialized');
|
|
198
|
+
}
|
|
199
|
+
catch (initError) {
|
|
200
|
+
const err = initError;
|
|
201
|
+
throw new Error(`Failed to auto-initialize IPFS repo: ${err.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
logger.info('🚀 Starting IPFS daemon...');
|
|
185
205
|
try {
|
|
186
206
|
// Start daemon as fully detached background process
|
|
187
207
|
// Using spawn with detached:true and stdio:'ignore' allows parent to exit
|
|
@@ -197,6 +217,17 @@ export class IPFSClientManager {
|
|
|
197
217
|
if (daemon.pid) {
|
|
198
218
|
fs.writeFileSync(pidPath, daemon.pid.toString(), 'utf8');
|
|
199
219
|
}
|
|
220
|
+
// Wait for daemon to actually be ready (poll the API)
|
|
221
|
+
const ready = await this.waitForDaemon(10000);
|
|
222
|
+
if (!ready) {
|
|
223
|
+
// Clean up PID file since daemon didn't start
|
|
224
|
+
if (fs.existsSync(pidPath)) {
|
|
225
|
+
fs.unlinkSync(pidPath);
|
|
226
|
+
}
|
|
227
|
+
throw new Error('IPFS daemon process started but API is not responding. ' +
|
|
228
|
+
'The daemon may have crashed. Check if IPFS repo is properly initialized: ' +
|
|
229
|
+
`IPFS_PATH=${ipfsRepoPath}`);
|
|
230
|
+
}
|
|
200
231
|
logger.info('✅ IPFS daemon started');
|
|
201
232
|
logger.info(` PID: ${daemon.pid}`);
|
|
202
233
|
logger.info(' API: http://localhost:5001');
|
|
@@ -208,6 +239,27 @@ export class IPFSClientManager {
|
|
|
208
239
|
throw error;
|
|
209
240
|
}
|
|
210
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Wait for daemon API to become ready
|
|
244
|
+
*/
|
|
245
|
+
async waitForDaemon(timeoutMs) {
|
|
246
|
+
const start = Date.now();
|
|
247
|
+
while (Date.now() - start < timeoutMs) {
|
|
248
|
+
try {
|
|
249
|
+
const response = await fetch('http://127.0.0.1:5001/api/v0/id', {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
signal: AbortSignal.timeout(2000),
|
|
252
|
+
});
|
|
253
|
+
if (response.ok)
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Daemon not ready yet, keep polling
|
|
258
|
+
}
|
|
259
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
211
263
|
/**
|
|
212
264
|
* Stop IPFS daemon
|
|
213
265
|
*/
|
package/dist/lib/ipfs-sync.js
CHANGED
|
@@ -130,14 +130,16 @@ export class IPFSSync {
|
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
132
|
* Download data from IPFS
|
|
133
|
-
* Tries local daemon first
|
|
133
|
+
* Tries local daemon first (with longer timeout for DHT discovery),
|
|
134
|
+
* then falls back to public gateways
|
|
134
135
|
*/
|
|
135
136
|
async download(cid) {
|
|
136
|
-
// Try local daemon first
|
|
137
|
+
// Try local daemon first — use a longer timeout (60s) because
|
|
138
|
+
// the local node may need time for DHT content discovery
|
|
137
139
|
try {
|
|
138
140
|
const localResponse = await fetch(`${this.LOCAL_IPFS_API}/cat?arg=${cid}`, {
|
|
139
141
|
method: 'POST',
|
|
140
|
-
signal: AbortSignal.timeout(
|
|
142
|
+
signal: AbortSignal.timeout(60000),
|
|
141
143
|
});
|
|
142
144
|
if (localResponse.ok) {
|
|
143
145
|
const arrayBuffer = await localResponse.arrayBuffer();
|
|
@@ -1090,5 +1090,130 @@ API_KEY=
|
|
|
1090
1090
|
process.exit(1);
|
|
1091
1091
|
}
|
|
1092
1092
|
});
|
|
1093
|
+
// Copy env variables from one file to another
|
|
1094
|
+
program
|
|
1095
|
+
.command('cp <from> <to>')
|
|
1096
|
+
.description('Copy env variables from <from> to <to> (like cp, but for .env files)')
|
|
1097
|
+
.option('--merge', 'Merge into destination instead of overwriting; source takes precedence on conflicts')
|
|
1098
|
+
.option('--no-overwrite', 'With --merge, skip keys that already exist in destination')
|
|
1099
|
+
.option('-n, --name <key>', 'Copy only the specified variable (implies --merge into destination)')
|
|
1100
|
+
.action(async (from, to, options) => {
|
|
1101
|
+
try {
|
|
1102
|
+
const fromPath = path.resolve(from);
|
|
1103
|
+
const toPath = path.resolve(to);
|
|
1104
|
+
if (!fs.existsSync(fromPath)) {
|
|
1105
|
+
console.error(`❌ Source file not found: ${fromPath}`);
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
}
|
|
1108
|
+
// Parse source file
|
|
1109
|
+
const srcContent = fs.readFileSync(fromPath, 'utf8');
|
|
1110
|
+
const srcLines = srcContent.split('\n');
|
|
1111
|
+
const srcVars = new Map();
|
|
1112
|
+
for (const line of srcLines) {
|
|
1113
|
+
if (line.trim().startsWith('#') || !line.trim())
|
|
1114
|
+
continue;
|
|
1115
|
+
const match = line.match(/^(?:export\s+)?([^=]+)=(.*)$/);
|
|
1116
|
+
if (match) {
|
|
1117
|
+
const key = match[1].trim();
|
|
1118
|
+
let value = match[2].trim();
|
|
1119
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
1120
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
1121
|
+
value = value.slice(1, -1);
|
|
1122
|
+
}
|
|
1123
|
+
srcVars.set(key, value);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (srcVars.size === 0) {
|
|
1127
|
+
console.error(`❌ No env variables found in: ${fromPath}`);
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
// --name: copy only a single named variable (always merges into destination)
|
|
1131
|
+
if (options.name) {
|
|
1132
|
+
const targetKey = options.name;
|
|
1133
|
+
if (!srcVars.has(targetKey)) {
|
|
1134
|
+
console.error(`❌ Variable '${targetKey}' not found in ${from}`);
|
|
1135
|
+
process.exit(1);
|
|
1136
|
+
}
|
|
1137
|
+
const targetValue = srcVars.get(targetKey);
|
|
1138
|
+
await setSingleSecret(toPath, targetKey, targetValue);
|
|
1139
|
+
console.log(`✅ Copied ${targetKey} from ${from} to ${to}`);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (!options.merge) {
|
|
1143
|
+
// Default: overwrite destination entirely (like regular cp)
|
|
1144
|
+
const newLines = [];
|
|
1145
|
+
for (const [key, value] of srcVars.entries()) {
|
|
1146
|
+
newLines.push(formatEnvLine(key, value, toPath));
|
|
1147
|
+
}
|
|
1148
|
+
fs.writeFileSync(toPath, newLines.join('\n') + '\n', 'utf8');
|
|
1149
|
+
console.log(`✅ Copied ${srcVars.size} variable(s) from ${from} to ${to}`);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
// Merge mode: apply source vars into destination
|
|
1153
|
+
const noOverwrite = options.overwrite === false;
|
|
1154
|
+
const destLines = [];
|
|
1155
|
+
if (fs.existsSync(toPath)) {
|
|
1156
|
+
const destContent = fs.readFileSync(toPath, 'utf8');
|
|
1157
|
+
for (const line of destContent.split('\n')) {
|
|
1158
|
+
destLines.push(line);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
// Build merged content: walk destination lines, update/keep keys
|
|
1162
|
+
const processedKeys = new Set();
|
|
1163
|
+
const mergedLines = [];
|
|
1164
|
+
for (const line of destLines) {
|
|
1165
|
+
if (line.trim().startsWith('#') || !line.trim()) {
|
|
1166
|
+
mergedLines.push(line);
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const match = line.match(/^(?:export\s+)?([^=]+)=(.*)$/);
|
|
1170
|
+
if (match) {
|
|
1171
|
+
const key = match[1].trim();
|
|
1172
|
+
if (srcVars.has(key)) {
|
|
1173
|
+
if (noOverwrite) {
|
|
1174
|
+
mergedLines.push(line);
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
mergedLines.push(formatEnvLine(key, srcVars.get(key), toPath));
|
|
1178
|
+
}
|
|
1179
|
+
processedKeys.add(key);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
mergedLines.push(line);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
mergedLines.push(line);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
// Append new keys from source not in destination
|
|
1190
|
+
let added = 0;
|
|
1191
|
+
for (const [key, value] of srcVars.entries()) {
|
|
1192
|
+
if (!processedKeys.has(key)) {
|
|
1193
|
+
mergedLines.push(formatEnvLine(key, value, toPath));
|
|
1194
|
+
added++;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
let finalContent = mergedLines.join('\n');
|
|
1198
|
+
if (!finalContent.endsWith('\n'))
|
|
1199
|
+
finalContent += '\n';
|
|
1200
|
+
fs.writeFileSync(toPath, finalContent, 'utf8');
|
|
1201
|
+
const updated = srcVars.size - added;
|
|
1202
|
+
const skipped = noOverwrite ? updated : 0;
|
|
1203
|
+
const overwritten = noOverwrite ? 0 : updated;
|
|
1204
|
+
console.log(`✅ Merged ${from} into ${to}:`);
|
|
1205
|
+
if (added > 0)
|
|
1206
|
+
console.log(` Added: ${added} new variable(s)`);
|
|
1207
|
+
if (overwritten > 0)
|
|
1208
|
+
console.log(` Overwritten: ${overwritten} existing variable(s)`);
|
|
1209
|
+
if (skipped > 0)
|
|
1210
|
+
console.log(` Skipped: ${skipped} existing variable(s) (--no-overwrite)`);
|
|
1211
|
+
}
|
|
1212
|
+
catch (error) {
|
|
1213
|
+
const err = error;
|
|
1214
|
+
console.error('❌ Failed to copy env file:', err.message);
|
|
1215
|
+
process.exit(1);
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1093
1218
|
}
|
|
1094
1219
|
export default init_secrets;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lsh-framework",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.26",
|
|
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": {
|