@wipcomputer/wip-ldm-os 0.4.85-alpha.12 → 0.4.85-alpha.14

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/bin/ldm.js CHANGED
@@ -22,7 +22,7 @@
22
22
 
23
23
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync, readlinkSync, renameSync, statSync, lstatSync, symlinkSync } from 'node:fs';
24
24
  import { join, basename, resolve, dirname } from 'node:path';
25
- import { execSync } from 'node:child_process';
25
+ import { execSync, spawnSync } from 'node:child_process';
26
26
  import { fileURLToPath } from 'node:url';
27
27
 
28
28
  const __filename = fileURLToPath(import.meta.url);
@@ -229,6 +229,62 @@ function checkCliVersion() {
229
229
  }
230
230
  }
231
231
 
232
+ function selectedLdmNpmTrack() {
233
+ return ALPHA_FLAG ? 'alpha' : BETA_FLAG ? 'beta' : 'latest';
234
+ }
235
+
236
+ function selectedLdmTrackLabel(npmTag) {
237
+ return npmTag === 'latest' ? '' : ` (${npmTag} track)`;
238
+ }
239
+
240
+ function latestLdmCliForSelectedTrack() {
241
+ const npmTag = selectedLdmNpmTrack();
242
+ const npmViewCmd = npmTag === 'latest'
243
+ ? 'npm view @wipcomputer/wip-ldm-os version 2>/dev/null'
244
+ : `npm view @wipcomputer/wip-ldm-os dist-tags.${npmTag} 2>/dev/null`;
245
+ const latest = execSync(npmViewCmd, {
246
+ encoding: 'utf8',
247
+ timeout: 15000,
248
+ }).trim();
249
+ return { latest, npmTag };
250
+ }
251
+
252
+ function maybeSelfUpdateLdmCliBeforeInstall() {
253
+ // This shared preflight covers both bare `ldm install` and targeted
254
+ // `ldm install <app>`. Dry runs never update, but still disclose skew.
255
+ if (process.env.LDM_SELF_UPDATED) return;
256
+
257
+ try {
258
+ const { latest, npmTag } = latestLdmCliForSelectedTrack();
259
+ if (!latest || !semverNewer(latest, PKG_VERSION)) return;
260
+
261
+ const trackLabel = selectedLdmTrackLabel(npmTag);
262
+
263
+ if (DRY_RUN) {
264
+ console.log(` LDM OS CLI v${PKG_VERSION} -> v${latest}${trackLabel} is available.`);
265
+ console.log(` Dry run only: continuing with v${PKG_VERSION}.`);
266
+ console.log('');
267
+ return;
268
+ }
269
+
270
+ console.log(` LDM OS CLI v${PKG_VERSION} -> v${latest}${trackLabel}. Updating first...`);
271
+ try {
272
+ execSync(`npm install -g @wipcomputer/wip-ldm-os@${latest}`, { stdio: 'inherit', timeout: 60000 });
273
+ console.log(` CLI updated to v${latest}. Re-running with new code...`);
274
+ console.log('');
275
+ const reArgs = process.argv.slice(2);
276
+ const child = spawnSync('ldm', reArgs.length > 0 ? reArgs : ['install'], {
277
+ stdio: 'inherit',
278
+ env: { ...process.env, LDM_SELF_UPDATED: '1' },
279
+ });
280
+ if (child.error) throw child.error;
281
+ process.exit(child.status ?? 1);
282
+ } catch (e) {
283
+ console.log(` ! Self-update failed: ${e.message}. Continuing with v${PKG_VERSION}.`);
284
+ }
285
+ } catch {}
286
+ }
287
+
232
288
  // ── Dead backup trigger cleanup (#207) ──
233
289
  // Three backup systems were competing. Only ai.openclaw.ldm-backup (3am) works.
234
290
  // This removes: broken cron entry (LDMDevTools.app), old com.wipcomputer.daily-backup.
@@ -1440,23 +1496,6 @@ async function showCatalogPicker() {
1440
1496
  // ── ldm install ──
1441
1497
 
1442
1498
  async function cmdInstall() {
1443
- if (!DRY_RUN && !acquireInstallLock()) return;
1444
-
1445
- // Ensure LDM is initialized
1446
- if (!existsSync(VERSION_PATH)) {
1447
- console.log(' LDM OS not initialized. Running init first...');
1448
- console.log('');
1449
- cmdInit();
1450
- }
1451
-
1452
- const { setFlags, installFromPath, installSingleTool, installToolbox, detectHarnesses } = await import('../lib/deploy.mjs');
1453
- const { detectInterfacesJSON } = await import('../lib/detect.mjs');
1454
-
1455
- // Refresh harness detection (catches newly installed harnesses)
1456
- detectHarnesses();
1457
-
1458
- setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT, origin: 'manual' });
1459
-
1460
1499
  // --help flag (#81)
1461
1500
  if (args.includes('--help') || args.includes('-h')) {
1462
1501
  console.log(`
@@ -1476,6 +1515,25 @@ async function cmdInstall() {
1476
1515
  process.exit(0);
1477
1516
  }
1478
1517
 
1518
+ maybeSelfUpdateLdmCliBeforeInstall();
1519
+
1520
+ if (!DRY_RUN && !acquireInstallLock()) return;
1521
+
1522
+ // Ensure LDM is initialized
1523
+ if (!existsSync(VERSION_PATH)) {
1524
+ console.log(' LDM OS not initialized. Running init first...');
1525
+ console.log('');
1526
+ cmdInit();
1527
+ }
1528
+
1529
+ const { setFlags, installFromPath, installSingleTool, installToolbox, detectHarnesses } = await import('../lib/deploy.mjs');
1530
+ const { detectInterfacesJSON } = await import('../lib/detect.mjs');
1531
+
1532
+ // Refresh harness detection (catches newly installed harnesses)
1533
+ detectHarnesses();
1534
+
1535
+ setFlags({ dryRun: DRY_RUN, jsonOutput: JSON_OUTPUT, origin: 'manual' });
1536
+
1479
1537
  // Find the target (skip flags)
1480
1538
  const target = args.slice(1).find(a => !a.startsWith('--'));
1481
1539
 
@@ -1921,38 +1979,6 @@ async function cmdInstallCatalog() {
1921
1979
  // No lock here. cmdInstall() already holds it when calling this.
1922
1980
  installLog(`ldm install started (v${PKG_VERSION}, DRY_RUN=${DRY_RUN})`);
1923
1981
 
1924
- // Self-update: check if CLI itself is outdated. Update first, then re-exec.
1925
- // This breaks the chicken-and-egg: new features in ldm install are always
1926
- // available because the installer upgrades itself before doing anything else.
1927
- // --alpha and --beta flags check the corresponding npm dist-tag instead of @latest.
1928
- if (!DRY_RUN && !process.env.LDM_SELF_UPDATED) {
1929
- try {
1930
- const npmTag = ALPHA_FLAG ? 'alpha' : BETA_FLAG ? 'beta' : 'latest';
1931
- const trackLabel = npmTag === 'latest' ? '' : ` (${npmTag} track)`;
1932
- const npmViewCmd = npmTag === 'latest'
1933
- ? 'npm view @wipcomputer/wip-ldm-os version 2>/dev/null'
1934
- : `npm view @wipcomputer/wip-ldm-os dist-tags.${npmTag} 2>/dev/null`;
1935
- const latest = execSync(npmViewCmd, {
1936
- encoding: 'utf8', timeout: 15000,
1937
- }).trim();
1938
- if (latest && semverNewer(latest, PKG_VERSION)) {
1939
- console.log(` LDM OS CLI v${PKG_VERSION} -> v${latest}${trackLabel}. Updating first...`);
1940
- try {
1941
- execSync(`npm install -g @wipcomputer/wip-ldm-os@${latest}`, { stdio: 'inherit', timeout: 60000 });
1942
- console.log(` CLI updated to v${latest}. Re-running with new code...`);
1943
- console.log('');
1944
- // Re-exec with the new binary. LDM_SELF_UPDATED prevents infinite loop.
1945
- // process.argv.slice(2) skips 'node' and the script path, keeps just 'install' + flags
1946
- const reArgs = process.argv.slice(2).join(' ') || 'install';
1947
- execSync(`LDM_SELF_UPDATED=1 ldm ${reArgs}`, { stdio: 'inherit' });
1948
- process.exit(0);
1949
- } catch (e) {
1950
- console.log(` ! Self-update failed: ${e.message}. Continuing with v${PKG_VERSION}.`);
1951
- }
1952
- }
1953
- } catch {}
1954
- }
1955
-
1956
1982
  autoDetectExtensions();
1957
1983
 
1958
1984
  // Migrate old registry entries to v2 format (#262)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.85-alpha.12",
3
+ "version": "0.4.85-alpha.14",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {
@@ -23,6 +23,7 @@
23
23
  "test:skill-frontmatter": "node scripts/test-skill-frontmatter.mjs",
24
24
  "test:installer-update-tracks": "node scripts/test-installer-update-tracks.mjs",
25
25
  "test:installer-hook-toolname": "node scripts/test-installer-hook-toolname.mjs",
26
+ "test:installer-target-self-update": "node scripts/test-installer-target-self-update.mjs",
26
27
  "test:installer-skill-directory": "node scripts/test-installer-skill-directory.mjs",
27
28
  "test:installer-skill-dry-run-destinations": "node scripts/test-installer-skill-dry-run-destinations.mjs",
28
29
  "test:ldm-install-bin-shim": "node scripts/test-ldm-install-preserves-foreign-bin.mjs",
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
9
+ const cli = readFileSync(join(root, 'bin', 'ldm.js'), 'utf8');
10
+
11
+ const helperName = 'function maybeSelfUpdateLdmCliBeforeInstall()';
12
+ const helperIdx = cli.indexOf(helperName);
13
+ if (helperIdx === -1) {
14
+ throw new Error('Missing shared LDM OS self-update preflight helper');
15
+ }
16
+
17
+ const helperBlock = cli.slice(helperIdx, cli.indexOf('// ── Dead backup trigger cleanup', helperIdx));
18
+ if (!helperBlock.includes('Dry run only: continuing with v${PKG_VERSION}.')) {
19
+ throw new Error('Self-update helper must warn on dry run without updating');
20
+ }
21
+
22
+ if (!helperBlock.includes("execSync(`npm install -g @wipcomputer/wip-ldm-os@${latest}`")) {
23
+ throw new Error('Self-update helper must update LDM OS before real installs');
24
+ }
25
+
26
+ if (!helperBlock.includes("spawnSync('ldm'")) {
27
+ throw new Error('Self-update helper must re-run the original install command without shell joining args');
28
+ }
29
+
30
+ if (helperBlock.includes('process.argv.slice(2).join')) {
31
+ throw new Error('Self-update helper must preserve argv boundaries when re-running install');
32
+ }
33
+
34
+ const cmdInstallIdx = cli.indexOf('async function cmdInstall()');
35
+ const lockIdx = cli.indexOf('acquireInstallLock()', cmdInstallIdx);
36
+ const initIdx = cli.indexOf('LDM OS not initialized. Running init first', cmdInstallIdx);
37
+ const targetIdx = cli.indexOf('// Find the target (skip flags)', cmdInstallIdx);
38
+ const preflightCallIdx = cli.indexOf('maybeSelfUpdateLdmCliBeforeInstall();', cmdInstallIdx);
39
+ if (cmdInstallIdx === -1 || targetIdx === -1 || preflightCallIdx === -1) {
40
+ throw new Error('Could not find cmdInstall self-update placement');
41
+ }
42
+
43
+ if (preflightCallIdx > lockIdx || preflightCallIdx > initIdx) {
44
+ throw new Error('Self-update preflight must run before lock acquisition and init work');
45
+ }
46
+
47
+ if (preflightCallIdx > targetIdx) {
48
+ throw new Error('Self-update preflight must run before target resolution so app installs are covered');
49
+ }
50
+
51
+ const catalogIdx = cli.indexOf('async function cmdInstallCatalog()');
52
+ const oldCatalogBlock = cli.indexOf('Self-update: check if CLI itself is outdated', catalogIdx);
53
+ const autoDetectIdx = cli.indexOf('autoDetectExtensions();', catalogIdx);
54
+ if (oldCatalogBlock !== -1 && oldCatalogBlock < autoDetectIdx) {
55
+ throw new Error('Catalog install should not own the only self-update block');
56
+ }
57
+
58
+ const tempRoot = mkdtempSync(join(tmpdir(), 'ldm-target-self-update-'));
59
+ try {
60
+ const home = join(tempRoot, 'home');
61
+ const fakeBin = join(tempRoot, 'bin');
62
+ const target = join(tempRoot, 'target skill with spaces');
63
+
64
+ mkdirSync(join(home, '.ldm'), { recursive: true });
65
+ writeFileSync(join(home, '.ldm', 'version.json'), JSON.stringify({
66
+ version: '0.0.0',
67
+ installed: new Date().toISOString(),
68
+ updated: new Date().toISOString(),
69
+ }, null, 2) + '\n');
70
+
71
+ mkdirSync(fakeBin, { recursive: true });
72
+ const fakeNpm = join(fakeBin, 'npm');
73
+ writeFileSync(fakeNpm, `#!/usr/bin/env bash
74
+ if [ "$1" = "view" ] && [ "$2" = "@wipcomputer/wip-ldm-os" ] && [ "$3" = "dist-tags.alpha" ]; then
75
+ echo "99.0.0-alpha.1"
76
+ exit 0
77
+ fi
78
+ echo "unexpected npm command: $*" >&2
79
+ exit 64
80
+ `);
81
+ chmodSync(fakeNpm, 0o755);
82
+
83
+ mkdirSync(target, { recursive: true });
84
+ writeFileSync(join(target, 'SKILL.md'), `---
85
+ name: test-target-skill
86
+ description: Test target skill for installer self-update dry-run checks.
87
+ ---
88
+
89
+ # Test Target Skill
90
+ `);
91
+
92
+ const result = spawnSync(process.execPath, [
93
+ join(root, 'bin', 'ldm.js'),
94
+ 'install',
95
+ '--alpha',
96
+ '--dry-run',
97
+ target,
98
+ ], {
99
+ cwd: root,
100
+ encoding: 'utf8',
101
+ env: {
102
+ ...process.env,
103
+ HOME: home,
104
+ PATH: `${fakeBin}:${process.env.PATH || ''}`,
105
+ },
106
+ });
107
+
108
+ if (result.status !== 0) {
109
+ throw new Error(`Runtime dry-run exited ${result.status}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
110
+ }
111
+
112
+ if (!result.stdout.includes('LDM OS CLI v')) {
113
+ throw new Error(`Runtime dry-run did not print the LDM OS skew warning\nstdout:\n${result.stdout}`);
114
+ }
115
+
116
+ if (!result.stdout.includes('-> v99.0.0-alpha.1 (alpha track) is available.')) {
117
+ throw new Error(`Runtime dry-run did not include the selected alpha track version\nstdout:\n${result.stdout}`);
118
+ }
119
+
120
+ if (!result.stdout.includes('Dry run only: continuing with v')) {
121
+ throw new Error(`Runtime dry-run did not say it would continue without updating\nstdout:\n${result.stdout}`);
122
+ }
123
+
124
+ if (!result.stdout.includes('Installing: target skill with spaces (dry run)')) {
125
+ throw new Error(`Runtime dry-run did not continue to the targeted install preview\nstdout:\n${result.stdout}`);
126
+ }
127
+ } finally {
128
+ rmSync(tempRoot, { recursive: true, force: true });
129
+ }
130
+
131
+ console.log('targeted install self-update regression checks passed');