@wipcomputer/wip-ldm-os 0.4.3 → 0.4.5
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/SKILL.md +1 -1
- package/bin/ldm.js +82 -51
- package/package.json +1 -1
package/SKILL.md
CHANGED
package/bin/ldm.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* ldm --version Show version
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync } from 'node:fs';
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync } from 'node:fs';
|
|
21
21
|
import { join, basename, resolve, dirname } from 'node:path';
|
|
22
22
|
import { execSync } from 'node:child_process';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
@@ -58,6 +58,38 @@ if (existsSync(VERSION_PATH)) {
|
|
|
58
58
|
} catch {}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// ── Install lockfile (#57) ──
|
|
62
|
+
|
|
63
|
+
const LOCK_PATH = join(LDM_ROOT, 'state', '.ldm-install.lock');
|
|
64
|
+
|
|
65
|
+
function acquireInstallLock() {
|
|
66
|
+
try {
|
|
67
|
+
if (existsSync(LOCK_PATH)) {
|
|
68
|
+
const lock = JSON.parse(readFileSync(LOCK_PATH, 'utf8'));
|
|
69
|
+
// Check if PID is still alive
|
|
70
|
+
try {
|
|
71
|
+
process.kill(lock.pid, 0); // signal 0 = just check if alive
|
|
72
|
+
console.log(` Another ldm install is running (PID ${lock.pid}, started ${lock.started}).`);
|
|
73
|
+
console.log(` Wait for it to finish, or remove ~/.ldm/state/.ldm-install.lock`);
|
|
74
|
+
return false;
|
|
75
|
+
} catch {
|
|
76
|
+
// PID is dead, stale lock. Clean it up.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
mkdirSync(dirname(LOCK_PATH), { recursive: true });
|
|
80
|
+
writeFileSync(LOCK_PATH, JSON.stringify({ pid: process.pid, started: new Date().toISOString() }));
|
|
81
|
+
|
|
82
|
+
// Clean up on exit
|
|
83
|
+
const cleanup = () => { try { if (existsSync(LOCK_PATH)) { const l = JSON.parse(readFileSync(LOCK_PATH, 'utf8')); if (l.pid === process.pid) unlinkSync(LOCK_PATH); } } catch {} };
|
|
84
|
+
process.on('exit', cleanup);
|
|
85
|
+
process.on('SIGINT', () => { cleanup(); process.exit(1); });
|
|
86
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(1); });
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return true; // if lock fails, allow install anyway
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
61
93
|
const args = process.argv.slice(2);
|
|
62
94
|
const command = args[0];
|
|
63
95
|
const DRY_RUN = args.includes('--dry-run');
|
|
@@ -371,6 +403,8 @@ async function showCatalogPicker() {
|
|
|
371
403
|
// ── ldm install ──
|
|
372
404
|
|
|
373
405
|
async function cmdInstall() {
|
|
406
|
+
if (!DRY_RUN && !acquireInstallLock()) return;
|
|
407
|
+
|
|
374
408
|
// Ensure LDM is initialized
|
|
375
409
|
if (!existsSync(VERSION_PATH)) {
|
|
376
410
|
console.log(' LDM OS not initialized. Running init first...');
|
|
@@ -561,6 +595,8 @@ function autoDetectExtensions() {
|
|
|
561
595
|
// ── ldm install (bare): scan system, show real state, update if needed ──
|
|
562
596
|
|
|
563
597
|
async function cmdInstallCatalog() {
|
|
598
|
+
if (!DRY_RUN && !acquireInstallLock()) return;
|
|
599
|
+
|
|
564
600
|
autoDetectExtensions();
|
|
565
601
|
|
|
566
602
|
const { detectSystemState, reconcileState, formatReconciliation } = await import('../lib/state.mjs');
|
|
@@ -603,54 +639,50 @@ async function cmdInstallCatalog() {
|
|
|
603
639
|
console.log('');
|
|
604
640
|
}
|
|
605
641
|
|
|
606
|
-
// Build the update plan:
|
|
607
|
-
const
|
|
642
|
+
// Build the update plan: check ALL installed extensions against npm (#55)
|
|
643
|
+
const npmUpdates = [];
|
|
608
644
|
|
|
609
|
-
//
|
|
610
|
-
|
|
645
|
+
// Check every installed extension against npm via catalog
|
|
646
|
+
console.log(' Checking npm for updates...');
|
|
611
647
|
for (const [name, entry] of Object.entries(reconciled)) {
|
|
612
|
-
if (entry.registryHasSource) continue; // already handled above
|
|
613
648
|
if (!entry.deployedLdm && !entry.deployedOc) continue; // not installed
|
|
614
649
|
|
|
615
|
-
//
|
|
650
|
+
// Get npm package name from the installed extension's own package.json
|
|
651
|
+
const extPkgPath = join(LDM_EXTENSIONS, name, 'package.json');
|
|
652
|
+
const extPkg = readJSON(extPkgPath);
|
|
653
|
+
const npmPkg = extPkg?.name;
|
|
654
|
+
if (!npmPkg || !npmPkg.startsWith('@')) continue; // skip unscoped packages
|
|
655
|
+
|
|
656
|
+
// Find catalog entry for the repo URL (used for clone if update needed)
|
|
616
657
|
const catalogEntry = components.find(c => {
|
|
617
658
|
const matches = c.registryMatches || [c.id];
|
|
618
659
|
return matches.includes(name) || c.id === name;
|
|
619
660
|
});
|
|
620
|
-
if (!catalogEntry?.repo) continue;
|
|
621
661
|
|
|
622
|
-
// Check npm for newer version
|
|
623
|
-
const npmPkg = catalogEntry.npm;
|
|
624
662
|
const currentVersion = entry.ldmVersion || entry.ocVersion;
|
|
625
|
-
|
|
663
|
+
if (!currentVersion) continue;
|
|
626
664
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
});
|
|
665
|
+
try {
|
|
666
|
+
const latestVersion = execSync(`npm view ${npmPkg} version 2>/dev/null`, {
|
|
667
|
+
encoding: 'utf8', timeout: 10000,
|
|
668
|
+
}).trim();
|
|
669
|
+
|
|
670
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
671
|
+
npmUpdates.push({
|
|
672
|
+
...entry,
|
|
673
|
+
catalogRepo: catalogEntry?.repo || null,
|
|
674
|
+
catalogNpm: npmPkg,
|
|
675
|
+
currentVersion,
|
|
676
|
+
latestVersion,
|
|
677
|
+
hasUpdate: true,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
} catch {}
|
|
644
681
|
}
|
|
645
682
|
|
|
646
|
-
const
|
|
647
|
-
const npmUpdates = fromCatalog.filter(e => e.hasUpdate);
|
|
648
|
-
const totalUpdates = updatable.length + npmUpdates.length;
|
|
683
|
+
const totalUpdates = npmUpdates.length;
|
|
649
684
|
|
|
650
685
|
if (DRY_RUN) {
|
|
651
|
-
if (updatable.length > 0) {
|
|
652
|
-
console.log(` Would update ${updatable.length} extension(s) from source repos.`);
|
|
653
|
-
}
|
|
654
686
|
if (npmUpdates.length > 0) {
|
|
655
687
|
console.log(` Would update ${npmUpdates.length} extension(s) from npm:`);
|
|
656
688
|
for (const e of npmUpdates) {
|
|
@@ -712,18 +744,13 @@ async function cmdInstallCatalog() {
|
|
|
712
744
|
const { createRevertManifest } = await import('../lib/safe.mjs');
|
|
713
745
|
const manifestPath = createRevertManifest(
|
|
714
746
|
`ldm install (update ${totalUpdates} extensions)`,
|
|
715
|
-
|
|
716
|
-
action: 'update-from-source',
|
|
717
|
-
name: e.name,
|
|
718
|
-
currentVersion: e.ldmVersion || e.registryVersion,
|
|
719
|
-
source: e.registrySource,
|
|
720
|
-
})), ...npmUpdates.map(e => ({
|
|
747
|
+
npmUpdates.map(e => ({
|
|
721
748
|
action: 'update-from-catalog',
|
|
722
749
|
name: e.name,
|
|
723
750
|
currentVersion: e.currentVersion,
|
|
724
751
|
latestVersion: e.latestVersion,
|
|
725
752
|
repo: e.catalogRepo,
|
|
726
|
-
}))
|
|
753
|
+
}))
|
|
727
754
|
);
|
|
728
755
|
console.log(` Revert plan saved: ${manifestPath}`);
|
|
729
756
|
console.log('');
|
|
@@ -733,13 +760,7 @@ async function cmdInstallCatalog() {
|
|
|
733
760
|
|
|
734
761
|
let updated = 0;
|
|
735
762
|
|
|
736
|
-
// Update from
|
|
737
|
-
for (const entry of updatable) {
|
|
738
|
-
await installFromPath(entry.registrySource);
|
|
739
|
-
updated++;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// Update from catalog repos (clone from GitHub for extensions without valid source) (#55)
|
|
763
|
+
// Update from npm via catalog repos (#55)
|
|
743
764
|
for (const entry of npmUpdates) {
|
|
744
765
|
console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from ${entry.catalogRepo})...`);
|
|
745
766
|
try {
|
|
@@ -1258,11 +1279,21 @@ function cmdStackList() {
|
|
|
1258
1279
|
|
|
1259
1280
|
for (const [id, stack] of Object.entries(stacks)) {
|
|
1260
1281
|
const resolved = resolveStack(id);
|
|
1261
|
-
const compCount = resolved.components.length;
|
|
1262
|
-
const mcpCount = resolved.mcpServers.length;
|
|
1263
1282
|
console.log(` ${id}: ${stack.name}`);
|
|
1264
1283
|
console.log(` ${stack.description}`);
|
|
1265
|
-
|
|
1284
|
+
if (resolved.components.length > 0) {
|
|
1285
|
+
console.log(` Components:`);
|
|
1286
|
+
for (const compId of resolved.components) {
|
|
1287
|
+
const entry = findInCatalog(compId);
|
|
1288
|
+
console.log(` - ${entry?.name || compId}`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (resolved.mcpServers.length > 0) {
|
|
1292
|
+
console.log(` MCP Servers:`);
|
|
1293
|
+
for (const mcp of resolved.mcpServers) {
|
|
1294
|
+
console.log(` - ${mcp.name}`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1266
1297
|
console.log('');
|
|
1267
1298
|
}
|
|
1268
1299
|
|