lsh-framework 1.6.0 → 1.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 CHANGED
@@ -815,6 +815,9 @@ lsh daemon start
815
815
  - **[SECRETS_QUICK_REFERENCE.md](docs/features/secrets/SECRETS_QUICK_REFERENCE.md)** - Quick reference for daily use
816
816
  - **[SECRETS_CHEATSHEET.txt](SECRETS_CHEATSHEET.txt)** - Command cheatsheet
817
817
 
818
+ ### IPFS Integration
819
+ - **[IPFS_CLIENT_GUIDE.md](docs/features/ipfs/IPFS_CLIENT_GUIDE.md)** - šŸ†• IPFS client installation and management (v1.6.0+)
820
+
818
821
  ### Installation & Development
819
822
  - **[INSTALL.md](docs/deployment/INSTALL.md)** - Detailed installation instructions
820
823
  - **[CLAUDE.md](CLAUDE.md)** - Developer guide for contributors
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { registerDoctorCommands } from './commands/doctor.js';
10
10
  import { registerCompletionCommands } from './commands/completion.js';
11
11
  import { registerConfigCommands } from './commands/config.js';
12
12
  import { registerSyncHistoryCommands } from './commands/sync-history.js';
13
+ import { registerIPFSCommands } from './commands/ipfs.js';
13
14
  import { init_daemon } from './services/daemon/daemon.js';
14
15
  import { init_supabase } from './services/supabase/supabase.js';
15
16
  import { init_cron } from './services/cron/cron.js';
@@ -144,6 +145,7 @@ function findSimilarCommands(input, validCommands) {
144
145
  registerDoctorCommands(program);
145
146
  registerConfigCommands(program);
146
147
  registerSyncHistoryCommands(program);
148
+ registerIPFSCommands(program);
147
149
  // Secrets management (primary feature)
148
150
  await init_secrets(program);
149
151
  // Supporting services
@@ -7,6 +7,7 @@ import * as fs from 'fs/promises';
7
7
  import * as path from 'path';
8
8
  import { createClient } from '@supabase/supabase-js';
9
9
  import { getPlatformPaths, getPlatformInfo } from '../lib/platform-utils.js';
10
+ import { IPFSClientManager } from '../lib/ipfs-client-manager.js';
10
11
  /**
11
12
  * Register doctor commands
12
13
  */
@@ -48,6 +49,8 @@ async function runHealthCheck(options) {
48
49
  checks.push(...storageChecks);
49
50
  // Git repository check
50
51
  checks.push(await checkGitRepository(options.verbose));
52
+ // IPFS client check
53
+ checks.push(await checkIPFSClient(options.verbose));
51
54
  // Permissions check
52
55
  checks.push(await checkPermissions(options.verbose));
53
56
  // Display results
@@ -294,6 +297,38 @@ async function checkGitRepository(verbose) {
294
297
  };
295
298
  }
296
299
  }
300
+ /**
301
+ * Check IPFS client installation
302
+ */
303
+ async function checkIPFSClient(verbose) {
304
+ try {
305
+ const manager = new IPFSClientManager();
306
+ const info = await manager.detect();
307
+ if (info.installed) {
308
+ return {
309
+ name: 'IPFS Client',
310
+ status: 'pass',
311
+ message: `${info.type} v${info.version} installed`,
312
+ details: verbose ? `Path: ${info.path}` : undefined,
313
+ };
314
+ }
315
+ return {
316
+ name: 'IPFS Client',
317
+ status: 'warn',
318
+ message: 'Not installed (optional for local storage)',
319
+ details: 'Install with: lsh ipfs install',
320
+ };
321
+ }
322
+ catch (error) {
323
+ const err = error;
324
+ return {
325
+ name: 'IPFS Client',
326
+ status: 'warn',
327
+ message: 'Could not check IPFS client',
328
+ details: err.message,
329
+ };
330
+ }
331
+ }
297
332
  /**
298
333
  * Check file permissions
299
334
  */
@@ -0,0 +1,146 @@
1
+ /**
2
+ * IPFS Commands
3
+ * Manage IPFS client installation and configuration
4
+ */
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import { IPFSClientManager } from '../lib/ipfs-client-manager.js';
8
+ /**
9
+ * Register IPFS commands
10
+ */
11
+ export function registerIPFSCommands(program) {
12
+ const ipfsCommand = program
13
+ .command('ipfs')
14
+ .description('Manage IPFS client installation and configuration');
15
+ // lsh ipfs status
16
+ ipfsCommand
17
+ .command('status')
18
+ .description('Check IPFS client installation status')
19
+ .option('--json', 'Output as JSON')
20
+ .action(async (options) => {
21
+ try {
22
+ const manager = new IPFSClientManager();
23
+ const info = await manager.detect();
24
+ if (options.json) {
25
+ console.log(JSON.stringify(info, null, 2));
26
+ return;
27
+ }
28
+ console.log(chalk.bold.cyan('\nšŸ“¦ IPFS Client Status'));
29
+ console.log(chalk.gray('━'.repeat(50)));
30
+ console.log('');
31
+ if (info.installed) {
32
+ console.log(chalk.green('āœ… IPFS client installed'));
33
+ console.log(` Type: ${info.type}`);
34
+ console.log(` Version: ${info.version}`);
35
+ console.log(` Path: ${info.path}`);
36
+ }
37
+ else {
38
+ console.log(chalk.yellow('āš ļø IPFS client not installed'));
39
+ console.log('');
40
+ console.log(chalk.gray(' Install with: lsh ipfs install'));
41
+ }
42
+ console.log('');
43
+ }
44
+ catch (error) {
45
+ const err = error;
46
+ console.error(chalk.red('\nāŒ Failed to check status:'), err.message);
47
+ process.exit(1);
48
+ }
49
+ });
50
+ // lsh ipfs install
51
+ ipfsCommand
52
+ .command('install')
53
+ .description('Install IPFS client (Kubo)')
54
+ .option('-f, --force', 'Force reinstall even if already installed')
55
+ .option('-v, --version <version>', 'Install specific version')
56
+ .action(async (options) => {
57
+ const spinner = ora('Installing IPFS client...').start();
58
+ try {
59
+ const manager = new IPFSClientManager();
60
+ await manager.install({
61
+ force: options.force,
62
+ version: options.version,
63
+ });
64
+ spinner.succeed(chalk.green('IPFS client installed successfully!'));
65
+ console.log('');
66
+ console.log(chalk.gray('Next steps:'));
67
+ console.log(chalk.cyan(' lsh ipfs init # Initialize IPFS repository'));
68
+ console.log(chalk.cyan(' lsh ipfs start # Start IPFS daemon'));
69
+ console.log('');
70
+ }
71
+ catch (error) {
72
+ const err = error;
73
+ spinner.fail(chalk.red('Installation failed'));
74
+ console.error(chalk.red(err.message));
75
+ process.exit(1);
76
+ }
77
+ });
78
+ // lsh ipfs uninstall
79
+ ipfsCommand
80
+ .command('uninstall')
81
+ .description('Uninstall LSH-managed IPFS client')
82
+ .action(async () => {
83
+ try {
84
+ const manager = new IPFSClientManager();
85
+ await manager.uninstall();
86
+ }
87
+ catch (error) {
88
+ const err = error;
89
+ console.error(chalk.red('\nāŒ Uninstallation failed:'), err.message);
90
+ process.exit(1);
91
+ }
92
+ });
93
+ // lsh ipfs init
94
+ ipfsCommand
95
+ .command('init')
96
+ .description('Initialize IPFS repository')
97
+ .action(async () => {
98
+ const spinner = ora('Initializing IPFS repository...').start();
99
+ try {
100
+ const manager = new IPFSClientManager();
101
+ await manager.init();
102
+ spinner.succeed(chalk.green('IPFS repository initialized!'));
103
+ console.log('');
104
+ console.log(chalk.gray('Next step:'));
105
+ console.log(chalk.cyan(' lsh ipfs start # Start IPFS daemon'));
106
+ console.log('');
107
+ }
108
+ catch (error) {
109
+ const err = error;
110
+ spinner.fail(chalk.red('Initialization failed'));
111
+ console.error(chalk.red(err.message));
112
+ process.exit(1);
113
+ }
114
+ });
115
+ // lsh ipfs start
116
+ ipfsCommand
117
+ .command('start')
118
+ .description('Start IPFS daemon')
119
+ .action(async () => {
120
+ try {
121
+ const manager = new IPFSClientManager();
122
+ await manager.start();
123
+ }
124
+ catch (error) {
125
+ const err = error;
126
+ console.error(chalk.red('\nāŒ Failed to start daemon:'), err.message);
127
+ process.exit(1);
128
+ }
129
+ });
130
+ // lsh ipfs stop
131
+ ipfsCommand
132
+ .command('stop')
133
+ .description('Stop IPFS daemon')
134
+ .action(async () => {
135
+ try {
136
+ const manager = new IPFSClientManager();
137
+ await manager.stop();
138
+ }
139
+ catch (error) {
140
+ const err = error;
141
+ console.error(chalk.red('\nāŒ Failed to stop daemon:'), err.message);
142
+ process.exit(1);
143
+ }
144
+ });
145
+ }
146
+ export default registerIPFSCommands;
@@ -0,0 +1,321 @@
1
+ /**
2
+ * IPFS Client Manager
3
+ * Detects, installs, and manages IPFS clients across platforms
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+ import { createLogger } from './logger.js';
11
+ import { getPlatformInfo } from './platform-utils.js';
12
+ const execAsync = promisify(exec);
13
+ const logger = createLogger('IPFSClientManager');
14
+ /**
15
+ * IPFS Client Manager
16
+ *
17
+ * Manages IPFS client installation and configuration:
18
+ * 1. Kubo (formerly go-ipfs) - Official Go implementation
19
+ * 2. Helia - Modern JavaScript implementation
20
+ * 3. js-ipfs - Legacy JavaScript implementation (deprecated)
21
+ */
22
+ export class IPFSClientManager {
23
+ lshDir;
24
+ ipfsDir;
25
+ binDir;
26
+ constructor() {
27
+ this.lshDir = path.join(os.homedir(), '.lsh');
28
+ this.ipfsDir = path.join(this.lshDir, 'ipfs');
29
+ this.binDir = path.join(this.ipfsDir, 'bin');
30
+ // Ensure directories exist
31
+ this.ensureDirectories();
32
+ }
33
+ /**
34
+ * Detect if IPFS client is installed
35
+ */
36
+ async detect() {
37
+ // Check for system-wide Kubo installation
38
+ try {
39
+ const { stdout: kuboVersion } = await execAsync('ipfs version');
40
+ const versionMatch = kuboVersion.match(/ipfs version ([0-9.]+)/);
41
+ if (versionMatch) {
42
+ const { stdout: kuboPath } = await execAsync('which ipfs');
43
+ return {
44
+ installed: true,
45
+ version: versionMatch[1],
46
+ path: kuboPath.trim(),
47
+ type: 'kubo',
48
+ };
49
+ }
50
+ }
51
+ catch {
52
+ // Kubo not found in system PATH
53
+ }
54
+ // Check for local LSH-managed installation
55
+ const localIpfsPath = path.join(this.binDir, 'ipfs');
56
+ if (fs.existsSync(localIpfsPath)) {
57
+ try {
58
+ const { stdout: localVersion } = await execAsync(`${localIpfsPath} version`);
59
+ const versionMatch = localVersion.match(/ipfs version ([0-9.]+)/);
60
+ if (versionMatch) {
61
+ return {
62
+ installed: true,
63
+ version: versionMatch[1],
64
+ path: localIpfsPath,
65
+ type: 'kubo',
66
+ };
67
+ }
68
+ }
69
+ catch {
70
+ // Local binary exists but not working
71
+ }
72
+ }
73
+ return {
74
+ installed: false,
75
+ };
76
+ }
77
+ /**
78
+ * Install IPFS client
79
+ */
80
+ async install(options = {}) {
81
+ const platformInfo = getPlatformInfo();
82
+ const clientInfo = await this.detect();
83
+ // Check if already installed
84
+ if (clientInfo.installed && !options.force) {
85
+ logger.info(`āœ… IPFS already installed: ${clientInfo.type} v${clientInfo.version}`);
86
+ logger.info(` Path: ${clientInfo.path}`);
87
+ return;
88
+ }
89
+ logger.info('šŸ“¦ Installing IPFS client (Kubo)...');
90
+ // Determine version to install
91
+ const version = options.version || await this.getLatestKuboVersion();
92
+ logger.info(` Version: ${version}`);
93
+ logger.info(` Platform: ${platformInfo.platformName} ${platformInfo.arch}`);
94
+ // Download and install based on platform
95
+ try {
96
+ if (platformInfo.platform === 'darwin') {
97
+ await this.installKuboMacOS(version);
98
+ }
99
+ else if (platformInfo.platform === 'linux') {
100
+ await this.installKuboLinux(version);
101
+ }
102
+ else if (platformInfo.platform === 'win32') {
103
+ await this.installKuboWindows(version);
104
+ }
105
+ else {
106
+ throw new Error(`Unsupported platform: ${platformInfo.platform}`);
107
+ }
108
+ logger.info('āœ… IPFS client installed successfully!');
109
+ // Verify installation
110
+ const verifyInfo = await this.detect();
111
+ if (verifyInfo.installed) {
112
+ logger.info(` Version: ${verifyInfo.version}`);
113
+ logger.info(` Path: ${verifyInfo.path}`);
114
+ }
115
+ }
116
+ catch (error) {
117
+ const err = error;
118
+ logger.error(`āŒ Installation failed: ${err.message}`);
119
+ throw error;
120
+ }
121
+ }
122
+ /**
123
+ * Uninstall LSH-managed IPFS client
124
+ */
125
+ async uninstall() {
126
+ const clientInfo = await this.detect();
127
+ if (!clientInfo.installed) {
128
+ logger.info('ā„¹ļø No IPFS client installed');
129
+ return;
130
+ }
131
+ // Only uninstall if it's LSH-managed
132
+ if (clientInfo.path?.startsWith(this.binDir)) {
133
+ logger.info('šŸ—‘ļø Uninstalling LSH-managed IPFS client...');
134
+ if (fs.existsSync(this.ipfsDir)) {
135
+ fs.rmSync(this.ipfsDir, { recursive: true });
136
+ }
137
+ logger.info('āœ… IPFS client uninstalled');
138
+ }
139
+ else {
140
+ logger.info('ā„¹ļø System-wide IPFS installation detected');
141
+ logger.info(' LSH cannot uninstall system packages');
142
+ logger.info(` Path: ${clientInfo.path}`);
143
+ }
144
+ }
145
+ /**
146
+ * Initialize IPFS repository
147
+ */
148
+ async init() {
149
+ const clientInfo = await this.detect();
150
+ if (!clientInfo.installed) {
151
+ throw new Error('IPFS client not installed. Run: lsh ipfs install');
152
+ }
153
+ const ipfsRepoPath = path.join(this.ipfsDir, 'repo');
154
+ // Check if already initialized
155
+ if (fs.existsSync(path.join(ipfsRepoPath, 'config'))) {
156
+ logger.info('āœ… IPFS repository already initialized');
157
+ return;
158
+ }
159
+ logger.info('šŸ”§ Initializing IPFS repository...');
160
+ try {
161
+ const ipfsCmd = clientInfo.path || 'ipfs';
162
+ await execAsync(`${ipfsCmd} init`, {
163
+ env: { ...process.env, IPFS_PATH: ipfsRepoPath },
164
+ });
165
+ logger.info('āœ… IPFS repository initialized');
166
+ logger.info(` Path: ${ipfsRepoPath}`);
167
+ }
168
+ catch (error) {
169
+ const err = error;
170
+ logger.error(`āŒ Initialization failed: ${err.message}`);
171
+ throw error;
172
+ }
173
+ }
174
+ /**
175
+ * Start IPFS daemon
176
+ */
177
+ async start() {
178
+ const clientInfo = await this.detect();
179
+ if (!clientInfo.installed) {
180
+ throw new Error('IPFS client not installed. Run: lsh ipfs install');
181
+ }
182
+ logger.info('šŸš€ Starting IPFS daemon...');
183
+ const ipfsRepoPath = path.join(this.ipfsDir, 'repo');
184
+ const ipfsCmd = clientInfo.path || 'ipfs';
185
+ try {
186
+ // Start daemon in background
187
+ const daemon = exec(`${ipfsCmd} daemon`, {
188
+ env: { ...process.env, IPFS_PATH: ipfsRepoPath },
189
+ });
190
+ // Log PID for management
191
+ const pidPath = path.join(this.ipfsDir, 'daemon.pid');
192
+ if (daemon.pid) {
193
+ fs.writeFileSync(pidPath, daemon.pid.toString(), 'utf8');
194
+ }
195
+ logger.info('āœ… IPFS daemon started');
196
+ logger.info(` PID: ${daemon.pid}`);
197
+ logger.info(' API: http://localhost:5001');
198
+ logger.info(' Gateway: http://localhost:8080');
199
+ }
200
+ catch (error) {
201
+ const err = error;
202
+ logger.error(`āŒ Failed to start daemon: ${err.message}`);
203
+ throw error;
204
+ }
205
+ }
206
+ /**
207
+ * Stop IPFS daemon
208
+ */
209
+ async stop() {
210
+ const pidPath = path.join(this.ipfsDir, 'daemon.pid');
211
+ if (!fs.existsSync(pidPath)) {
212
+ logger.info('ā„¹ļø IPFS daemon not running (no PID file)');
213
+ return;
214
+ }
215
+ const pid = parseInt(fs.readFileSync(pidPath, 'utf8'), 10);
216
+ logger.info(`šŸ›‘ Stopping IPFS daemon (PID: ${pid})...`);
217
+ try {
218
+ process.kill(pid, 'SIGTERM');
219
+ fs.unlinkSync(pidPath);
220
+ logger.info('āœ… IPFS daemon stopped');
221
+ }
222
+ catch (error) {
223
+ const err = error;
224
+ logger.error(`āŒ Failed to stop daemon: ${err.message}`);
225
+ throw error;
226
+ }
227
+ }
228
+ /**
229
+ * Get latest Kubo version from GitHub releases
230
+ */
231
+ async getLatestKuboVersion() {
232
+ try {
233
+ // Use GitHub API to get latest release
234
+ const response = await fetch('https://api.github.com/repos/ipfs/kubo/releases/latest');
235
+ const data = await response.json();
236
+ // Remove 'v' prefix if present
237
+ return data.tag_name.replace(/^v/, '');
238
+ }
239
+ catch {
240
+ // Fallback to known stable version
241
+ return '0.26.0';
242
+ }
243
+ }
244
+ /**
245
+ * Install Kubo on macOS
246
+ */
247
+ async installKuboMacOS(version) {
248
+ const arch = os.arch() === 'arm64' ? 'arm64' : 'amd64';
249
+ const downloadUrl = `https://dist.ipfs.tech/kubo/v${version}/kubo_v${version}_darwin-${arch}.tar.gz`;
250
+ const tarPath = path.join(this.ipfsDir, 'kubo.tar.gz');
251
+ logger.info(' Downloading Kubo...');
252
+ // Download
253
+ await execAsync(`curl -L -o ${tarPath} ${downloadUrl}`);
254
+ logger.info(' Extracting...');
255
+ // Extract
256
+ await execAsync(`tar -xzf ${tarPath} -C ${this.ipfsDir}`);
257
+ // Move binary
258
+ const extractedBinPath = path.join(this.ipfsDir, 'kubo', 'ipfs');
259
+ fs.mkdirSync(this.binDir, { recursive: true });
260
+ fs.renameSync(extractedBinPath, path.join(this.binDir, 'ipfs'));
261
+ // Make executable
262
+ fs.chmodSync(path.join(this.binDir, 'ipfs'), 0o755);
263
+ // Cleanup
264
+ fs.unlinkSync(tarPath);
265
+ fs.rmSync(path.join(this.ipfsDir, 'kubo'), { recursive: true });
266
+ }
267
+ /**
268
+ * Install Kubo on Linux
269
+ */
270
+ async installKuboLinux(version) {
271
+ const arch = os.arch() === 'arm64' ? 'arm64' : 'amd64';
272
+ const downloadUrl = `https://dist.ipfs.tech/kubo/v${version}/kubo_v${version}_linux-${arch}.tar.gz`;
273
+ const tarPath = path.join(this.ipfsDir, 'kubo.tar.gz');
274
+ logger.info(' Downloading Kubo...');
275
+ // Download
276
+ await execAsync(`curl -L -o ${tarPath} ${downloadUrl}`);
277
+ logger.info(' Extracting...');
278
+ // Extract
279
+ await execAsync(`tar -xzf ${tarPath} -C ${this.ipfsDir}`);
280
+ // Move binary
281
+ const extractedBinPath = path.join(this.ipfsDir, 'kubo', 'ipfs');
282
+ fs.mkdirSync(this.binDir, { recursive: true });
283
+ fs.renameSync(extractedBinPath, path.join(this.binDir, 'ipfs'));
284
+ // Make executable
285
+ fs.chmodSync(path.join(this.binDir, 'ipfs'), 0o755);
286
+ // Cleanup
287
+ fs.unlinkSync(tarPath);
288
+ fs.rmSync(path.join(this.ipfsDir, 'kubo'), { recursive: true });
289
+ }
290
+ /**
291
+ * Install Kubo on Windows
292
+ */
293
+ async installKuboWindows(version) {
294
+ const downloadUrl = `https://dist.ipfs.tech/kubo/v${version}/kubo_v${version}_windows-amd64.zip`;
295
+ const zipPath = path.join(this.ipfsDir, 'kubo.zip');
296
+ logger.info(' Downloading Kubo...');
297
+ // Download
298
+ await execAsync(`curl -L -o ${zipPath} ${downloadUrl}`);
299
+ logger.info(' Extracting...');
300
+ // Extract (Windows has built-in tar that supports zip)
301
+ await execAsync(`tar -xf ${zipPath} -C ${this.ipfsDir}`);
302
+ // Move binary
303
+ const extractedBinPath = path.join(this.ipfsDir, 'kubo', 'ipfs.exe');
304
+ fs.mkdirSync(this.binDir, { recursive: true });
305
+ fs.renameSync(extractedBinPath, path.join(this.binDir, 'ipfs.exe'));
306
+ // Cleanup
307
+ fs.unlinkSync(zipPath);
308
+ fs.rmSync(path.join(this.ipfsDir, 'kubo'), { recursive: true });
309
+ }
310
+ /**
311
+ * Ensure required directories exist
312
+ */
313
+ ensureDirectories() {
314
+ if (!fs.existsSync(this.lshDir)) {
315
+ fs.mkdirSync(this.lshDir, { recursive: true });
316
+ }
317
+ if (!fs.existsSync(this.ipfsDir)) {
318
+ fs.mkdirSync(this.ipfsDir, { recursive: true });
319
+ }
320
+ }
321
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
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": {