@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.
Files changed (3) hide show
  1. package/SKILL.md +1 -1
  2. package/bin/ldm.js +82 -51
  3. package/package.json +1 -1
package/SKILL.md CHANGED
@@ -5,7 +5,7 @@ license: MIT
5
5
  interface: [cli, skill]
6
6
  metadata:
7
7
  display-name: "LDM OS"
8
- version: "0.4.3"
8
+ version: "0.4.5"
9
9
  homepage: "https://github.com/wipcomputer/wip-ldm-os"
10
10
  author: "Parker Todd Brooks"
11
11
  category: infrastructure
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: extensions with valid source paths + catalog items with npm updates (#55)
607
- const fromSource = Object.values(reconciled).filter(e => e.registryHasSource);
642
+ // Build the update plan: check ALL installed extensions against npm (#55)
643
+ const npmUpdates = [];
608
644
 
609
- // For extensions without valid source: check catalog for repo, check npm for newer version
610
- const fromCatalog = [];
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
- // Find this extension in the catalog
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
- let latestVersion = null;
663
+ if (!currentVersion) continue;
626
664
 
627
- if (npmPkg && currentVersion) {
628
- try {
629
- latestVersion = execSync(`npm view ${npmPkg} version 2>/dev/null`, {
630
- encoding: 'utf8', timeout: 10000,
631
- }).trim();
632
- } catch {}
633
- }
634
-
635
- const hasUpdate = latestVersion && currentVersion && latestVersion !== currentVersion;
636
- fromCatalog.push({
637
- ...entry,
638
- catalogRepo: catalogEntry.repo,
639
- catalogNpm: npmPkg,
640
- currentVersion,
641
- latestVersion,
642
- hasUpdate,
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 updatable = fromSource;
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
- [...updatable.map(e => ({
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 source repos (local paths)
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
- console.log(` ${compCount} component(s), ${mcpCount} MCP server(s)`);
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "main": "src/boot/boot-hook.mjs",