@wipcomputer/wip-ldm-os 0.4.2 → 0.4.4
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 +1 -1
- package/SKILL.md +1 -1
- package/bin/ldm.js +136 -29
- package/docs/skills/README.md +1 -1
- package/docs/universal-installer/TECHNICAL.md +43 -0
- package/lib/deploy.mjs +9 -1
- package/package.json +1 -1
- package/docs/skills/TECHNICAL.md +0 -67
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ The OS connects your AIs. Add-ons are what they actually use. Each one is a full
|
|
|
103
103
|
|
|
104
104
|
## More Info
|
|
105
105
|
|
|
106
|
-
- [Architecture, principles, and technical details](
|
|
106
|
+
- [Architecture, principles, and technical details](TECHNICAL.md)
|
|
107
107
|
|
|
108
108
|
## License
|
|
109
109
|
|
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...');
|
|
@@ -522,7 +556,7 @@ function autoDetectExtensions() {
|
|
|
522
556
|
const dirs = readdirSync(LDM_EXTENSIONS, { withFileTypes: true });
|
|
523
557
|
for (const dir of dirs) {
|
|
524
558
|
if (!dir.isDirectory()) continue;
|
|
525
|
-
if (dir.name === '_trash' || dir.name.startsWith('.')) continue;
|
|
559
|
+
if (dir.name === '_trash' || dir.name.startsWith('.') || dir.name.startsWith('ldm-install-')) continue;
|
|
526
560
|
|
|
527
561
|
const extPath = join(LDM_EXTENSIONS, dir.name);
|
|
528
562
|
const pkgPath = join(extPath, 'package.json');
|
|
@@ -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,14 +639,57 @@ async function cmdInstallCatalog() {
|
|
|
603
639
|
console.log('');
|
|
604
640
|
}
|
|
605
641
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
642
|
+
// Build the update plan: check ALL installed extensions against npm (#55)
|
|
643
|
+
const npmUpdates = [];
|
|
644
|
+
|
|
645
|
+
// Check every installed extension against npm via catalog
|
|
646
|
+
console.log(' Checking npm for updates...');
|
|
647
|
+
for (const [name, entry] of Object.entries(reconciled)) {
|
|
648
|
+
if (!entry.deployedLdm && !entry.deployedOc) continue; // not installed
|
|
649
|
+
|
|
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)
|
|
657
|
+
const catalogEntry = components.find(c => {
|
|
658
|
+
const matches = c.registryMatches || [c.id];
|
|
659
|
+
return matches.includes(name) || c.id === name;
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const currentVersion = entry.ldmVersion || entry.ocVersion;
|
|
663
|
+
if (!currentVersion) continue;
|
|
664
|
+
|
|
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 {}
|
|
681
|
+
}
|
|
611
682
|
|
|
612
|
-
|
|
613
|
-
|
|
683
|
+
const totalUpdates = npmUpdates.length;
|
|
684
|
+
|
|
685
|
+
if (DRY_RUN) {
|
|
686
|
+
if (npmUpdates.length > 0) {
|
|
687
|
+
console.log(` Would update ${npmUpdates.length} extension(s) from npm:`);
|
|
688
|
+
for (const e of npmUpdates) {
|
|
689
|
+
console.log(` ${e.name}: v${e.currentVersion} -> v${e.latestVersion} (${e.catalogNpm})`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (totalUpdates > 0) {
|
|
614
693
|
console.log(' No data (crystal.db, agent files) would be touched.');
|
|
615
694
|
console.log(' Old versions would be moved to ~/.ldm/_trash/ (never deleted).');
|
|
616
695
|
} else {
|
|
@@ -623,18 +702,15 @@ async function cmdInstallCatalog() {
|
|
|
623
702
|
return;
|
|
624
703
|
}
|
|
625
704
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
e.registryHasSource
|
|
629
|
-
);
|
|
630
|
-
|
|
631
|
-
if (updatable.length === 0) {
|
|
632
|
-
console.log(' No extensions have linked source repos to update from.');
|
|
633
|
-
console.log(' Link them with: ldm install <org/repo>');
|
|
705
|
+
if (totalUpdates === 0 && available.length === 0) {
|
|
706
|
+
console.log(' Everything is up to date.');
|
|
634
707
|
console.log('');
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
635
710
|
|
|
636
|
-
|
|
637
|
-
|
|
711
|
+
if (totalUpdates === 0 && available.length > 0) {
|
|
712
|
+
// Nothing to update, but catalog items available
|
|
713
|
+
if (!YES_FLAG && !NONE_FLAG && process.stdin.isTTY) {
|
|
638
714
|
const { createInterface } = await import('node:readline');
|
|
639
715
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
640
716
|
const answer = await new Promise((resolve) => {
|
|
@@ -643,7 +719,6 @@ async function cmdInstallCatalog() {
|
|
|
643
719
|
resolve(a.trim().toLowerCase());
|
|
644
720
|
});
|
|
645
721
|
});
|
|
646
|
-
|
|
647
722
|
if (answer && answer !== 'none' && answer !== 'n') {
|
|
648
723
|
let toInstall = [];
|
|
649
724
|
if (answer === 'all' || answer === 'a') {
|
|
@@ -668,12 +743,13 @@ async function cmdInstallCatalog() {
|
|
|
668
743
|
// Write revert manifest before starting
|
|
669
744
|
const { createRevertManifest } = await import('../lib/safe.mjs');
|
|
670
745
|
const manifestPath = createRevertManifest(
|
|
671
|
-
`ldm install (update ${
|
|
672
|
-
|
|
673
|
-
action: 'update',
|
|
746
|
+
`ldm install (update ${totalUpdates} extensions)`,
|
|
747
|
+
npmUpdates.map(e => ({
|
|
748
|
+
action: 'update-from-catalog',
|
|
674
749
|
name: e.name,
|
|
675
|
-
currentVersion: e.
|
|
676
|
-
|
|
750
|
+
currentVersion: e.currentVersion,
|
|
751
|
+
latestVersion: e.latestVersion,
|
|
752
|
+
repo: e.catalogRepo,
|
|
677
753
|
}))
|
|
678
754
|
);
|
|
679
755
|
console.log(` Revert plan saved: ${manifestPath}`);
|
|
@@ -683,9 +759,16 @@ async function cmdInstallCatalog() {
|
|
|
683
759
|
setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT });
|
|
684
760
|
|
|
685
761
|
let updated = 0;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
762
|
+
|
|
763
|
+
// Update from npm via catalog repos (#55)
|
|
764
|
+
for (const entry of npmUpdates) {
|
|
765
|
+
console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from ${entry.catalogRepo})...`);
|
|
766
|
+
try {
|
|
767
|
+
execSync(`ldm install ${entry.catalogRepo}`, { stdio: 'inherit' });
|
|
768
|
+
updated++;
|
|
769
|
+
} catch (e) {
|
|
770
|
+
console.error(` x Failed to update ${entry.name}: ${e.message}`);
|
|
771
|
+
}
|
|
689
772
|
}
|
|
690
773
|
|
|
691
774
|
// Sync boot hook from npm package (#49)
|
|
@@ -694,7 +777,7 @@ async function cmdInstallCatalog() {
|
|
|
694
777
|
}
|
|
695
778
|
|
|
696
779
|
console.log('');
|
|
697
|
-
console.log(` Updated ${updated}/${
|
|
780
|
+
console.log(` Updated ${updated}/${totalUpdates} extension(s).`);
|
|
698
781
|
|
|
699
782
|
// Check if CLI itself is outdated (#29)
|
|
700
783
|
checkCliVersion();
|
|
@@ -780,6 +863,30 @@ async function cmdDoctor() {
|
|
|
780
863
|
}
|
|
781
864
|
}
|
|
782
865
|
|
|
866
|
+
// --fix: clean registry entries with /tmp/ sources or ldm-install- names (#54)
|
|
867
|
+
if (FIX_FLAG) {
|
|
868
|
+
const registry = readJSON(REGISTRY_PATH);
|
|
869
|
+
if (registry?.extensions) {
|
|
870
|
+
const staleNames = [];
|
|
871
|
+
for (const [name, info] of Object.entries(registry.extensions)) {
|
|
872
|
+
const src = info?.source || '';
|
|
873
|
+
const isTmpSource = src.startsWith('/tmp/') || src.startsWith('/private/tmp/');
|
|
874
|
+
const isTmpName = name.startsWith('ldm-install-');
|
|
875
|
+
if (isTmpSource || isTmpName) {
|
|
876
|
+
staleNames.push(name);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
for (const name of staleNames) {
|
|
880
|
+
delete registry.extensions[name];
|
|
881
|
+
console.log(` + Removed stale registry entry: ${name} (/tmp/ clone)`);
|
|
882
|
+
issues = Math.max(0, issues - 1);
|
|
883
|
+
}
|
|
884
|
+
if (staleNames.length > 0) {
|
|
885
|
+
writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
783
890
|
// 4. Check sacred locations
|
|
784
891
|
const sacred = [
|
|
785
892
|
{ path: join(LDM_ROOT, 'memory'), label: 'memory/' },
|
package/docs/skills/README.md
CHANGED
|
@@ -243,6 +243,49 @@ The `ai/` folder is the development process. It is not part of the published pro
|
|
|
243
243
|
|
|
244
244
|
**Public/private split:** If a repo is public, the `ai/` folder should not ship. The recommended pattern is to maintain a private working repo (with `ai/`) and a public repo (everything except `ai/`). The public repo has everything an LLM or human needs to understand and use the tool. The `ai/` folder is operational context for the team building it.
|
|
245
245
|
|
|
246
|
+
## Catalog
|
|
247
|
+
|
|
248
|
+
Skills are defined in `catalog.json` at the LDM OS root. Each entry has:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"id": "memory-crystal",
|
|
253
|
+
"name": "Memory Crystal",
|
|
254
|
+
"description": "Persistent memory for your AI.",
|
|
255
|
+
"npm": "@wipcomputer/memory-crystal",
|
|
256
|
+
"repo": "wipcomputer/memory-crystal",
|
|
257
|
+
"registryMatches": ["memory-crystal"],
|
|
258
|
+
"cliMatches": ["crystal"],
|
|
259
|
+
"recommended": true,
|
|
260
|
+
"status": "stable"
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Stacks
|
|
265
|
+
|
|
266
|
+
Stacks group skills for team installs. Defined in `catalog.json`:
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"stacks": {
|
|
271
|
+
"core": {
|
|
272
|
+
"name": "WIP Core",
|
|
273
|
+
"components": ["memory-crystal", "wip-ai-devops-toolbox", "wip-1password", "wip-markdown-viewer"],
|
|
274
|
+
"mcpServers": []
|
|
275
|
+
},
|
|
276
|
+
"web": {
|
|
277
|
+
"name": "Web Development",
|
|
278
|
+
"components": [],
|
|
279
|
+
"mcpServers": [
|
|
280
|
+
{ "name": "playwright", "command": "npx", "args": ["-y", "@playwright/mcp@latest"] }
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Stacks are composable via the `includes` field.
|
|
288
|
+
|
|
246
289
|
## The Installer
|
|
247
290
|
|
|
248
291
|
`ldm install` scans any repo, detects which interfaces exist, and installs them all. One command.
|
package/lib/deploy.mjs
CHANGED
|
@@ -682,10 +682,18 @@ export function installSingleTool(toolPath) {
|
|
|
682
682
|
}
|
|
683
683
|
|
|
684
684
|
let installed = 0;
|
|
685
|
+
// Don't store /tmp/ clone paths as source (#54). Use the repo URL from package.json if available.
|
|
686
|
+
let source = toolPath;
|
|
687
|
+
const isTmpPath = toolPath.startsWith('/tmp/') || toolPath.startsWith('/private/tmp/');
|
|
688
|
+
if (isTmpPath && pkg?.repository?.url) {
|
|
689
|
+
source = pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
690
|
+
} else if (isTmpPath) {
|
|
691
|
+
source = null; // better than a /tmp/ path
|
|
692
|
+
}
|
|
685
693
|
const registryInfo = {
|
|
686
694
|
name: toolName,
|
|
687
695
|
version: pkg?.version || 'unknown',
|
|
688
|
-
source
|
|
696
|
+
source,
|
|
689
697
|
interfaces: ifaceNames,
|
|
690
698
|
};
|
|
691
699
|
|
package/package.json
CHANGED
package/docs/skills/TECHNICAL.md
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# Skills ... Technical Reference
|
|
2
|
-
|
|
3
|
-
## Catalog
|
|
4
|
-
|
|
5
|
-
Skills are defined in `catalog.json` at the LDM OS root. Each entry has:
|
|
6
|
-
|
|
7
|
-
```json
|
|
8
|
-
{
|
|
9
|
-
"id": "memory-crystal",
|
|
10
|
-
"name": "Memory Crystal",
|
|
11
|
-
"description": "Persistent memory for your AI.",
|
|
12
|
-
"npm": "@wipcomputer/memory-crystal",
|
|
13
|
-
"repo": "wipcomputer/memory-crystal",
|
|
14
|
-
"registryMatches": ["memory-crystal"],
|
|
15
|
-
"cliMatches": ["crystal"],
|
|
16
|
-
"recommended": true,
|
|
17
|
-
"status": "stable"
|
|
18
|
-
}
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Stacks
|
|
22
|
-
|
|
23
|
-
Stacks group skills for team installs. Defined in `catalog.json`:
|
|
24
|
-
|
|
25
|
-
```json
|
|
26
|
-
{
|
|
27
|
-
"stacks": {
|
|
28
|
-
"core": {
|
|
29
|
-
"name": "WIP Core",
|
|
30
|
-
"components": ["memory-crystal", "wip-ai-devops-toolbox", "wip-1password", "wip-markdown-viewer"],
|
|
31
|
-
"mcpServers": []
|
|
32
|
-
},
|
|
33
|
-
"web": {
|
|
34
|
-
"name": "Web Development",
|
|
35
|
-
"components": [],
|
|
36
|
-
"mcpServers": [
|
|
37
|
-
{ "name": "playwright", "command": "npx", "args": ["-y", "@playwright/mcp@latest"] }
|
|
38
|
-
]
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Stacks are composable via the `includes` field.
|
|
45
|
-
|
|
46
|
-
## Interface Detection
|
|
47
|
-
|
|
48
|
-
`ldm install` auto-detects which interfaces a repo supports:
|
|
49
|
-
|
|
50
|
-
| Pattern | Interface | Install Action |
|
|
51
|
-
|---------|-----------|---------------|
|
|
52
|
-
| `package.json` with `bin` | CLI | `npm install -g` from registry |
|
|
53
|
-
| `main` or `exports` | Module | Reports import path |
|
|
54
|
-
| `mcp-server.mjs` | MCP | `claude mcp add --scope user` |
|
|
55
|
-
| `openclaw.plugin.json` | OpenClaw Plugin | Deploy to `~/.ldm/extensions/` + `~/.openclaw/extensions/` |
|
|
56
|
-
| `SKILL.md` | Skill | Deploy to `~/.openclaw/skills/` |
|
|
57
|
-
| `guard.mjs` or `claudeCode.hook` | CC Hook | Add to `~/.claude/settings.json` |
|
|
58
|
-
| `.claude-plugin/plugin.json` | CC Plugin | Register with marketplace |
|
|
59
|
-
|
|
60
|
-
## Key Files
|
|
61
|
-
|
|
62
|
-
| File | What |
|
|
63
|
-
|------|------|
|
|
64
|
-
| `catalog.json` | Component + stack definitions |
|
|
65
|
-
| `lib/detect.mjs` | Interface detection engine |
|
|
66
|
-
| `lib/deploy.mjs` | Deployment engine |
|
|
67
|
-
| `bin/ldm.js` | `ldm stack` and `ldm install` commands |
|