lsh-framework 2.2.5 → 2.3.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.
@@ -0,0 +1,139 @@
1
+ /**
2
+ * LSH Configuration Manager
3
+ *
4
+ * Manages encryption keys and configuration for different repositories.
5
+ * Keys are stored in ~/.lsh/config.json separate from the .env files being synced.
6
+ *
7
+ * This prevents sync conflicts where different hosts overwrite each other's keys.
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as os from 'os';
12
+ import { logger } from './logger.js';
13
+ export class LshConfigManager {
14
+ configPath;
15
+ config;
16
+ constructor(configPath) {
17
+ this.configPath = configPath || path.join(os.homedir(), '.lsh', 'config.json');
18
+ this.config = this.loadConfig();
19
+ }
20
+ /**
21
+ * Load config from disk, or create default if it doesn't exist
22
+ */
23
+ loadConfig() {
24
+ try {
25
+ if (fs.existsSync(this.configPath)) {
26
+ const data = fs.readFileSync(this.configPath, 'utf-8');
27
+ return JSON.parse(data);
28
+ }
29
+ }
30
+ catch (error) {
31
+ const err = error;
32
+ logger.warn(`Failed to load config: ${err.message}`);
33
+ }
34
+ // Return default config
35
+ return {
36
+ version: '1.0.0',
37
+ keys: {},
38
+ };
39
+ }
40
+ /**
41
+ * Save config to disk
42
+ */
43
+ saveConfig() {
44
+ try {
45
+ const dir = path.dirname(this.configPath);
46
+ if (!fs.existsSync(dir)) {
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ }
49
+ fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
50
+ }
51
+ catch (error) {
52
+ const err = error;
53
+ logger.error(`Failed to save config: ${err.message}`);
54
+ throw error;
55
+ }
56
+ }
57
+ /**
58
+ * Get encryption key for a specific repository
59
+ */
60
+ getKey(repoName) {
61
+ // Check config file first
62
+ if (this.config.keys[repoName]) {
63
+ // Update last used timestamp
64
+ this.config.keys[repoName].lastUsed = new Date().toISOString();
65
+ this.saveConfig();
66
+ return this.config.keys[repoName].key;
67
+ }
68
+ // Fall back to environment variables
69
+ const envKey = process.env.LSH_SECRETS_KEY || process.env.LSH_MASTER_KEY;
70
+ if (envKey) {
71
+ logger.debug(`Using encryption key from environment for ${repoName}`);
72
+ return envKey;
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * Set encryption key for a specific repository
78
+ */
79
+ setKey(repoName, key) {
80
+ this.config.keys[repoName] = {
81
+ key,
82
+ createdAt: this.config.keys[repoName]?.createdAt || new Date().toISOString(),
83
+ lastUsed: new Date().toISOString(),
84
+ };
85
+ this.saveConfig();
86
+ logger.debug(`Saved encryption key for ${repoName}`);
87
+ }
88
+ /**
89
+ * Check if a key exists for a repository
90
+ */
91
+ hasKey(repoName) {
92
+ return !!this.config.keys[repoName];
93
+ }
94
+ /**
95
+ * Remove encryption key for a repository
96
+ */
97
+ removeKey(repoName) {
98
+ delete this.config.keys[repoName];
99
+ this.saveConfig();
100
+ logger.debug(`Removed encryption key for ${repoName}`);
101
+ }
102
+ /**
103
+ * List all repositories with stored keys
104
+ */
105
+ listKeys() {
106
+ return Object.entries(this.config.keys).map(([repoName, data]) => ({
107
+ repoName,
108
+ createdAt: data.createdAt,
109
+ lastUsed: data.lastUsed,
110
+ }));
111
+ }
112
+ /**
113
+ * Export key for sharing with other hosts
114
+ */
115
+ exportKey(repoName) {
116
+ const key = this.getKey(repoName);
117
+ if (!key) {
118
+ return null;
119
+ }
120
+ return `export LSH_SECRETS_KEY='${key}'`;
121
+ }
122
+ /**
123
+ * Get config file path (for debugging/migration)
124
+ */
125
+ getConfigPath() {
126
+ return this.configPath;
127
+ }
128
+ }
129
+ // Singleton instance
130
+ let _configManager = null;
131
+ /**
132
+ * Get the global LSH config manager instance
133
+ */
134
+ export function getLshConfig() {
135
+ if (!_configManager) {
136
+ _configManager = new LshConfigManager();
137
+ }
138
+ return _configManager;
139
+ }
@@ -112,6 +112,32 @@ export class SecretsManager {
112
112
  }
113
113
  return env;
114
114
  }
115
+ /**
116
+ * Filter out LSH-internal keys that should not be synced
117
+ * These keys are host-specific and syncing them would cause conflicts
118
+ */
119
+ filterLshInternalKeys(env) {
120
+ const filtered = {};
121
+ const excludedKeys = new Set([
122
+ 'LSH_SECRETS_KEY', // Encryption key - host-specific, managed separately
123
+ 'LSH_MASTER_KEY', // Alternative encryption key name
124
+ 'LSH_INTERNAL_', // Any other LSH internal keys
125
+ ]);
126
+ for (const [key, value] of Object.entries(env)) {
127
+ // Skip exact matches
128
+ if (excludedKeys.has(key)) {
129
+ logger.debug(`Filtering out LSH-internal key: ${key}`);
130
+ continue;
131
+ }
132
+ // Skip keys starting with LSH_INTERNAL_
133
+ if (key.startsWith('LSH_INTERNAL_')) {
134
+ logger.debug(`Filtering out LSH-internal key: ${key}`);
135
+ continue;
136
+ }
137
+ filtered[key] = value;
138
+ }
139
+ return filtered;
140
+ }
115
141
  /**
116
142
  * Format env vars as .env file content
117
143
  */
@@ -186,13 +212,20 @@ export class SecretsManager {
186
212
  }
187
213
  logger.info(`Pushing ${envFilePath} to IPFS (${effectiveEnv})...`);
188
214
  const content = fs.readFileSync(envFilePath, 'utf8');
189
- const env = this.parseEnvFile(content);
215
+ const envParsed = this.parseEnvFile(content);
216
+ // Filter out LSH-internal keys (encryption keys, etc.) that should not be synced
217
+ const env = this.filterLshInternalKeys(envParsed);
218
+ const filteredCount = Object.keys(envParsed).length - Object.keys(env).length;
219
+ if (filteredCount > 0) {
220
+ logger.debug(`Filtered out ${filteredCount} LSH-internal key(s) from sync`);
221
+ }
190
222
  // Check for destructive changes unless force is true
191
223
  if (!force) {
192
224
  try {
193
225
  // Check if secrets already exist for this environment
194
- if (this.storage.exists(effectiveEnv, this.gitInfo?.repoName)) {
195
- const existingSecrets = await this.storage.pull(effectiveEnv, this.encryptionKey, this.gitInfo?.repoName);
226
+ // Use raw environment to match storage.push() behavior
227
+ if (this.storage.exists(environment, this.gitInfo?.repoName)) {
228
+ const existingSecrets = await this.storage.pull(environment, this.encryptionKey, this.gitInfo?.repoName);
196
229
  const cloudEnv = {};
197
230
  existingSecrets.forEach(s => {
198
231
  cloudEnv[s.key] = s.value;
@@ -240,22 +273,50 @@ export class SecretsManager {
240
273
  const effectiveEnv = this.getRepoAwareEnvironment(environment);
241
274
  logger.info(`Pulling ${filename} (${effectiveEnv}) from IPFS...`);
242
275
  // Get secrets from IPFS storage
243
- const secrets = await this.storage.pull(effectiveEnv, this.encryptionKey, this.gitInfo?.repoName);
276
+ // Use raw environment parameter to match how push() stores them
277
+ const secrets = await this.storage.pull(environment, // Use raw environment, not effectiveEnv
278
+ this.encryptionKey, this.gitInfo?.repoName);
244
279
  if (secrets.length === 0) {
245
280
  throw new Error(`No secrets found for environment: ${effectiveEnv}\n\n` +
246
281
  `💡 Tip: Check available environments with: lsh env\n` +
247
282
  ` Or push secrets first with: lsh push --env ${environment}`);
248
283
  }
249
- // Backup existing .env if it exists (unless force is true)
250
- if (fs.existsSync(envFilePath) && !force) {
251
- const backup = `${envFilePath}.backup.${Date.now()}`;
252
- fs.copyFileSync(envFilePath, backup);
253
- logger.info(`Backed up existing .env to ${backup}`);
254
- }
255
- // Convert secrets back to .env format
256
- const envContent = secrets
257
- .map(s => `${s.key}=${s.value}`)
258
- .join('\n') + '\n';
284
+ // Preserve local LSH-internal keys before overwriting
285
+ const localLshKeys = {};
286
+ if (fs.existsSync(envFilePath)) {
287
+ const existingContent = fs.readFileSync(envFilePath, 'utf8');
288
+ const existingEnv = this.parseEnvFile(existingContent);
289
+ // Extract LSH-internal keys to preserve
290
+ const lshKeyNames = ['LSH_SECRETS_KEY', 'LSH_MASTER_KEY'];
291
+ for (const keyName of lshKeyNames) {
292
+ if (existingEnv[keyName]) {
293
+ localLshKeys[keyName] = existingEnv[keyName];
294
+ logger.debug(`Preserving local ${keyName}`);
295
+ }
296
+ }
297
+ // Also preserve any LSH_INTERNAL_* keys
298
+ for (const [key, value] of Object.entries(existingEnv)) {
299
+ if (key.startsWith('LSH_INTERNAL_')) {
300
+ localLshKeys[key] = value;
301
+ logger.debug(`Preserving local ${key}`);
302
+ }
303
+ }
304
+ // Backup existing .env (unless force is true)
305
+ if (!force) {
306
+ const backup = `${envFilePath}.backup.${Date.now()}`;
307
+ fs.copyFileSync(envFilePath, backup);
308
+ logger.info(`Backed up existing ${filename} to ${backup}`);
309
+ }
310
+ }
311
+ // Convert secrets back to env object
312
+ const pulledEnv = {};
313
+ secrets.forEach(s => {
314
+ pulledEnv[s.key] = s.value;
315
+ });
316
+ // Merge pulled secrets with preserved local LSH keys
317
+ const finalEnv = { ...pulledEnv, ...localLshKeys };
318
+ // Convert to .env format
319
+ const envContent = this.formatEnvFile(finalEnv);
259
320
  // Write new .env
260
321
  fs.writeFileSync(envFilePath, envContent, 'utf8');
261
322
  logger.info(`✅ Pulled ${secrets.length} secrets from IPFS`);
@@ -244,12 +244,24 @@ export class StorachaClient {
244
244
  if (!await this.isAuthenticated()) {
245
245
  throw new Error('Not authenticated');
246
246
  }
247
+ // Get the latest registry version and increment it
248
+ let registryVersion = 1;
249
+ try {
250
+ const latestRegistry = await this.getLatestRegistry(repoName);
251
+ if (latestRegistry && latestRegistry.registryVersion) {
252
+ registryVersion = latestRegistry.registryVersion + 1;
253
+ }
254
+ }
255
+ catch (err) {
256
+ logger.debug(`Could not fetch latest registry version, using version 1: ${err.message}`);
257
+ }
247
258
  const registry = {
248
259
  repoName,
249
260
  environment,
250
261
  cid: secretsCid, // Include the secrets CID
251
262
  timestamp: new Date().toISOString(),
252
- version: '2.2.2',
263
+ version: '2.3.0', // LSH version
264
+ registryVersion, // Incremental version counter
253
265
  };
254
266
  const content = JSON.stringify(registry, null, 2);
255
267
  const buffer = Buffer.from(content, 'utf-8');
@@ -259,7 +271,7 @@ export class StorachaClient {
259
271
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
260
272
  const file = new File([uint8Array], filename, { type: 'application/json' });
261
273
  const cid = await client.uploadFile(file);
262
- logger.debug(`📝 Uploaded registry for ${repoName} (secrets CID: ${secretsCid}): ${cid}`);
274
+ logger.debug(`📝 Uploaded registry v${registryVersion} for ${repoName} (secrets CID: ${secretsCid}): ${cid}`);
263
275
  return cid.toString();
264
276
  }
265
277
  /**
@@ -303,6 +315,7 @@ export class StorachaClient {
303
315
  cid: cid,
304
316
  timestamp: json.timestamp,
305
317
  secretsCid: json.cid,
318
+ registryVersion: json.registryVersion || 0, // Default to 0 for old registries without version
306
319
  });
307
320
  }
308
321
  }
@@ -311,11 +324,18 @@ export class StorachaClient {
311
324
  continue;
312
325
  }
313
326
  }
314
- // Sort by timestamp (newest first) and return the most recent secrets CID
327
+ // Sort by registryVersion (highest first), then timestamp as tie-breaker
315
328
  if (registries.length > 0) {
316
- registries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
329
+ registries.sort((a, b) => {
330
+ // First compare by registryVersion (higher is newer)
331
+ if (b.registryVersion !== a.registryVersion) {
332
+ return b.registryVersion - a.registryVersion;
333
+ }
334
+ // If versions match, use timestamp as tie-breaker
335
+ return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
336
+ });
317
337
  const latest = registries[0];
318
- logger.debug(`✅ Found latest CID for ${repoName}: ${latest.secretsCid} (timestamp: ${latest.timestamp})`);
338
+ logger.debug(`✅ Found latest CID for ${repoName}: ${latest.secretsCid} (v${latest.registryVersion}, timestamp: ${latest.timestamp})`);
319
339
  return latest.secretsCid;
320
340
  }
321
341
  // No registry found
@@ -327,6 +347,76 @@ export class StorachaClient {
327
347
  return null;
328
348
  }
329
349
  }
350
+ /**
351
+ * Get the latest registry object for a repo
352
+ * Returns the full registry object including registryVersion
353
+ */
354
+ async getLatestRegistry(repoName) {
355
+ if (!this.isEnabled()) {
356
+ return null;
357
+ }
358
+ if (!await this.isAuthenticated()) {
359
+ return null;
360
+ }
361
+ try {
362
+ const client = await this.getClient();
363
+ // Only check recent uploads (limit to 20 for performance)
364
+ const pageSize = 20;
365
+ // Get first page of uploads
366
+ const results = await client.capability.upload.list({
367
+ cursor: '',
368
+ size: pageSize,
369
+ });
370
+ // Collect all registry files for this repo
371
+ const registries = [];
372
+ for (const upload of results.results) {
373
+ try {
374
+ const cid = upload.root.toString();
375
+ // Download with timeout
376
+ const downloadPromise = this.download(cid);
377
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000));
378
+ const content = await Promise.race([downloadPromise, timeoutPromise]);
379
+ // Skip large files (registry should be < 1KB)
380
+ if (content.length > 1024) {
381
+ continue;
382
+ }
383
+ // Try to parse as JSON
384
+ const json = JSON.parse(content.toString('utf-8'));
385
+ // Check if it's an LSH registry file for our repo
386
+ if (json.repoName === repoName && json.version && json.cid && json.timestamp) {
387
+ registries.push({
388
+ repoName: json.repoName,
389
+ environment: json.environment,
390
+ cid: json.cid,
391
+ timestamp: json.timestamp,
392
+ version: json.version,
393
+ registryVersion: json.registryVersion || 0,
394
+ });
395
+ }
396
+ }
397
+ catch {
398
+ // Not an LSH registry file or failed to download
399
+ continue;
400
+ }
401
+ }
402
+ // Sort by registryVersion (highest first), then timestamp as tie-breaker
403
+ if (registries.length > 0) {
404
+ registries.sort((a, b) => {
405
+ if (b.registryVersion !== a.registryVersion) {
406
+ return b.registryVersion - a.registryVersion;
407
+ }
408
+ return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
409
+ });
410
+ return registries[0];
411
+ }
412
+ return null;
413
+ }
414
+ catch (error) {
415
+ const err = error;
416
+ logger.debug(`Failed to get latest registry: ${err.message}`);
417
+ return null;
418
+ }
419
+ }
330
420
  /**
331
421
  * Check if registry exists for a repo by listing uploads
332
422
  * Returns true if a registry file for this repo exists in Storacha
@@ -842,5 +842,208 @@ API_KEY=
842
842
  process.exit(1);
843
843
  }
844
844
  });
845
+ // Clear stuck registries and local metadata
846
+ program
847
+ .command('clear')
848
+ .description('Clear local metadata and cache to resolve stuck registries')
849
+ .option('--repo <name>', 'Clear metadata for specific repo only')
850
+ .option('--cache', 'Also clear local encrypted secrets cache')
851
+ .option('--storacha', 'Also delete old Storacha uploads (registries and secrets)')
852
+ .option('--all', 'Clear all metadata and cache (requires confirmation)')
853
+ .option('-y, --yes', 'Skip confirmation prompts')
854
+ .action(async (options) => {
855
+ try {
856
+ const lshDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.lsh');
857
+ const metadataPath = path.join(lshDir, 'secrets-metadata.json');
858
+ const cacheDir = path.join(lshDir, 'secrets-cache');
859
+ // Determine what we're clearing
860
+ if (!options.repo && !options.all) {
861
+ console.error('❌ Please specify either --repo <name> or --all');
862
+ console.log('');
863
+ console.log('Examples:');
864
+ console.log(' lsh clear --repo lsh_test_repo # Clear metadata for specific repo');
865
+ console.log(' lsh clear --all # Clear all metadata');
866
+ console.log(' lsh clear --all --cache # Clear metadata and cache');
867
+ process.exit(1);
868
+ }
869
+ // Load metadata
870
+ if (!fs.existsSync(metadataPath)) {
871
+ console.log('ℹ️ No metadata file found - nothing to clear');
872
+ return;
873
+ }
874
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
875
+ const keys = Object.keys(metadata);
876
+ if (keys.length === 0) {
877
+ console.log('ℹ️ Metadata is already empty');
878
+ return;
879
+ }
880
+ // Show what will be cleared
881
+ console.log('📋 Current metadata entries:');
882
+ console.log('');
883
+ if (options.repo) {
884
+ const repoKeys = keys.filter(k => metadata[k].git_repo === options.repo);
885
+ if (repoKeys.length === 0) {
886
+ console.log(`ℹ️ No metadata found for repo: ${options.repo}`);
887
+ return;
888
+ }
889
+ console.log(`Repo: ${options.repo}`);
890
+ repoKeys.forEach(key => {
891
+ console.log(` - ${key} (CID: ${metadata[key].cid.substring(0, 12)}...)`);
892
+ });
893
+ console.log('');
894
+ console.log(`Will clear ${repoKeys.length} ${repoKeys.length === 1 ? 'entry' : 'entries'}`);
895
+ }
896
+ else {
897
+ const repoCount = new Set(keys.map(k => metadata[k].git_repo)).size;
898
+ console.log(`Total entries: ${keys.length} across ${repoCount} ${repoCount === 1 ? 'repo' : 'repos'}`);
899
+ }
900
+ if (options.cache) {
901
+ if (fs.existsSync(cacheDir)) {
902
+ const cacheFiles = fs.readdirSync(cacheDir);
903
+ console.log(`Cache files: ${cacheFiles.length}`);
904
+ }
905
+ }
906
+ console.log('');
907
+ // Confirmation
908
+ if (!options.yes) {
909
+ console.log('⚠️ WARNING: This will clear local metadata!');
910
+ console.log('');
911
+ console.log('This is useful when:');
912
+ console.log(' • Registry is returning stale/old CIDs');
913
+ console.log(' • Pull fails with "bad decrypt" errors');
914
+ console.log(' • You need to force a fresh sync');
915
+ console.log('');
916
+ console.log('After clearing, you will need to push secrets again.');
917
+ console.log('');
918
+ const rl = readline.createInterface({
919
+ input: process.stdin,
920
+ output: process.stdout,
921
+ });
922
+ const answer = await new Promise((resolve) => {
923
+ rl.question('Continue? (yes/no): ', (ans) => {
924
+ rl.close();
925
+ resolve(ans.trim().toLowerCase());
926
+ });
927
+ });
928
+ if (answer !== 'yes' && answer !== 'y') {
929
+ console.log('');
930
+ console.log('❌ Cancelled');
931
+ return;
932
+ }
933
+ }
934
+ console.log('');
935
+ // Clear metadata
936
+ if (options.repo) {
937
+ const repoKeys = keys.filter(k => metadata[k].git_repo === options.repo);
938
+ repoKeys.forEach(key => delete metadata[key]);
939
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
940
+ console.log(`✅ Cleared ${repoKeys.length} metadata ${repoKeys.length === 1 ? 'entry' : 'entries'} for ${options.repo}`);
941
+ }
942
+ else {
943
+ fs.writeFileSync(metadataPath, '{}');
944
+ console.log(`✅ Cleared all ${keys.length} metadata ${keys.length === 1 ? 'entry' : 'entries'}`);
945
+ }
946
+ // Clear cache if requested
947
+ if (options.cache && fs.existsSync(cacheDir)) {
948
+ const cacheFiles = fs.readdirSync(cacheDir);
949
+ let cleared = 0;
950
+ for (const file of cacheFiles) {
951
+ const filePath = path.join(cacheDir, file);
952
+ if (fs.statSync(filePath).isFile()) {
953
+ fs.unlinkSync(filePath);
954
+ cleared++;
955
+ }
956
+ }
957
+ console.log(`✅ Cleared ${cleared} cache ${cleared === 1 ? 'file' : 'files'}`);
958
+ }
959
+ // Clear Storacha uploads if requested
960
+ if (options.storacha && options.repo) {
961
+ console.log('');
962
+ console.log('🌐 Clearing Storacha uploads...');
963
+ try {
964
+ const { StorachaClient } = await import('../../lib/storacha-client.js');
965
+ const storacha = new StorachaClient();
966
+ if (!storacha.isEnabled()) {
967
+ console.log('ℹ️ Storacha is not enabled - skipping cloud cleanup');
968
+ }
969
+ else if (!(await storacha.isAuthenticated())) {
970
+ console.log('ℹ️ Not authenticated with Storacha - skipping cloud cleanup');
971
+ }
972
+ else {
973
+ // Get all uploads
974
+ const client = await storacha.getClient();
975
+ const pageSize = 50;
976
+ const results = await client.capability.upload.list({
977
+ cursor: '',
978
+ size: pageSize,
979
+ });
980
+ // Find LSH-related uploads for this repo
981
+ const toDelete = [];
982
+ for (const upload of results.results) {
983
+ try {
984
+ const cid = upload.root.toString();
985
+ // Download with timeout
986
+ const downloadPromise = storacha.download(cid);
987
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000));
988
+ const content = await Promise.race([downloadPromise, timeoutPromise]);
989
+ // Check if it's a registry file for this repo
990
+ if (content.length < 2048) {
991
+ try {
992
+ const json = JSON.parse(content.toString('utf-8'));
993
+ if (json.repoName === options.repo) {
994
+ toDelete.push({ cid, type: 'registry', size: content.length });
995
+ }
996
+ }
997
+ catch {
998
+ // Not JSON, might be encrypted secrets
999
+ // Check filename pattern
1000
+ const _filename = `lsh-secrets-${options.repo}`;
1001
+ if (cid.includes(options.repo) || content.toString().includes(options.repo)) {
1002
+ toDelete.push({ cid, type: 'secrets', size: content.length });
1003
+ }
1004
+ }
1005
+ }
1006
+ }
1007
+ catch {
1008
+ // Failed to download or parse, skip
1009
+ continue;
1010
+ }
1011
+ }
1012
+ if (toDelete.length > 0) {
1013
+ console.log(`Found ${toDelete.length} Storacha ${toDelete.length === 1 ? 'upload' : 'uploads'} for ${options.repo}:`);
1014
+ toDelete.forEach((item) => {
1015
+ console.log(` - ${item.type}: ${item.cid.substring(0, 16)}... (${item.size} bytes)`);
1016
+ });
1017
+ // Note: Storacha doesn't currently support deletion via SDK
1018
+ // The uploads will remain but won't be used after metadata is cleared
1019
+ console.log('');
1020
+ console.log('⚠️ Note: Storacha uploads cannot be deleted programmatically.');
1021
+ console.log(' These files will remain in Storacha but won\'t be used after metadata is cleared.');
1022
+ console.log(' To fully remove them, use the Storacha web console:');
1023
+ console.log(' https://console.storacha.network/');
1024
+ }
1025
+ else {
1026
+ console.log(`ℹ️ No Storacha uploads found for ${options.repo}`);
1027
+ }
1028
+ }
1029
+ }
1030
+ catch (storageError) {
1031
+ const storErr = storageError;
1032
+ console.error(`⚠️ Failed to check Storacha uploads: ${storErr.message}`);
1033
+ console.log(' Local metadata has been cleared, but cloud uploads may remain.');
1034
+ }
1035
+ }
1036
+ console.log('');
1037
+ console.log('💡 Next steps:');
1038
+ console.log(' 1. lsh push .env # Push secrets with current key');
1039
+ console.log(' 2. lsh pull .env # Verify pull works');
1040
+ console.log('');
1041
+ }
1042
+ catch (error) {
1043
+ const err = error;
1044
+ console.error('❌ Failed to clear metadata:', err.message);
1045
+ process.exit(1);
1046
+ }
1047
+ });
845
1048
  }
846
1049
  export default init_secrets;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "2.2.5",
3
+ "version": "2.3.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": {