amalgm 0.1.137 → 0.1.138

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.
@@ -34,7 +34,7 @@ function writeSampleApp(dir) {
34
34
  fs.writeFileSync(path.join(dir, 'run.sh'), '#!/bin/sh\necho run\n', { mode: 0o755 });
35
35
  }
36
36
 
37
- function registerSampleApp(cwd) {
37
+ function registerSampleApp(cwd, overrides = {}) {
38
38
  const data = loadApps();
39
39
  const app = {
40
40
  id: 'app-sample-test',
@@ -54,6 +54,7 @@ function registerSampleApp(cwd) {
54
54
  status: 'running',
55
55
  createdAt: new Date().toISOString(),
56
56
  updatedAt: new Date().toISOString(),
57
+ ...overrides,
57
58
  };
58
59
  data.apps = data.apps.filter((item) => item.id !== app.id);
59
60
  data.apps.push(app);
@@ -174,7 +175,7 @@ test('app entry export carries the portable record and install materializes a ru
174
175
  const rootDir = makeTempDir('bundle-app-install-root-');
175
176
  const registerInput = materializeAppEntry(entry, { rootDir });
176
177
  assert.equal(registerInput.start_command, entry.app.startCommand);
177
- assert.equal(registerInput.build_command, entry.app.buildCommand);
178
+ assert.equal(registerInput.build_command, 'npm install && npm run build');
178
179
  assert.equal(registerInput.cwd.startsWith(rootDir), true);
179
180
  assert.equal(fs.readFileSync(path.join(registerInput.cwd, 'package.json'), 'utf8'), JSON.stringify({ name: 'sample-app' }));
180
181
 
@@ -183,6 +184,43 @@ test('app entry export carries the portable record and install materializes a ru
183
184
  assert.notEqual(secondInput.cwd, registerInput.cwd);
184
185
  });
185
186
 
187
+ test('materializeAppEntry bootstraps node dependencies for imported apps without duplicating install steps', () => {
188
+ const rootDir = makeTempDir('bundle-app-bootstrap-root-');
189
+
190
+ const buildDir = makeTempDir('bundle-app-bootstrap-src-');
191
+ fs.writeFileSync(path.join(buildDir, 'package.json'), JSON.stringify({ name: 'bootstrap-build' }));
192
+ fs.writeFileSync(path.join(buildDir, 'package-lock.json'), JSON.stringify({ name: 'bootstrap-build', lockfileVersion: 3 }));
193
+ fs.writeFileSync(path.join(buildDir, 'server.js'), 'console.log("serve");\n');
194
+ const buildApp = registerSampleApp(buildDir, {
195
+ buildCommand: 'npm run build',
196
+ startCommand: 'node server.js',
197
+ });
198
+ const buildEntry = exportAppEntry(buildApp.id).entry;
199
+ assert.equal(materializeAppEntry(buildEntry, { rootDir }).build_command, 'npm install && npm run build');
200
+
201
+ const preinstallDir = makeTempDir('bundle-app-bootstrap-preinstall-src-');
202
+ fs.writeFileSync(path.join(preinstallDir, 'package.json'), JSON.stringify({ name: 'bootstrap-preinstall' }));
203
+ fs.writeFileSync(path.join(preinstallDir, 'package-lock.json'), JSON.stringify({ name: 'bootstrap-preinstall', lockfileVersion: 3 }));
204
+ fs.writeFileSync(path.join(preinstallDir, 'server.js'), 'console.log("serve");\n');
205
+ const preinstallApp = registerSampleApp(preinstallDir, {
206
+ buildCommand: 'npm install && npm run build',
207
+ startCommand: 'node server.js',
208
+ });
209
+ const preinstallEntry = exportAppEntry(preinstallApp.id).entry;
210
+ assert.equal(materializeAppEntry(preinstallEntry, { rootDir }).build_command, 'npm install && npm run build');
211
+
212
+ const startOnlyDir = makeTempDir('bundle-app-bootstrap-startonly-src-');
213
+ fs.writeFileSync(path.join(startOnlyDir, 'package.json'), JSON.stringify({ name: 'bootstrap-startonly' }));
214
+ fs.writeFileSync(path.join(startOnlyDir, 'package-lock.json'), JSON.stringify({ name: 'bootstrap-startonly', lockfileVersion: 3 }));
215
+ fs.writeFileSync(path.join(startOnlyDir, 'server.js'), 'console.log("serve");\n');
216
+ const startOnlyApp = registerSampleApp(startOnlyDir, {
217
+ buildCommand: null,
218
+ startCommand: 'node server.js',
219
+ });
220
+ const startOnlyEntry = exportAppEntry(startOnlyApp.id).entry;
221
+ assert.equal(materializeAppEntry(startOnlyEntry, { rootDir }).build_command, 'npm install');
222
+ });
223
+
186
224
  test('mixed bundle carries automations and apps with heads and installs everything', async () => {
187
225
  const dir = makeTempDir('bundle-mixed-app-');
188
226
  writeSampleApp(dir);
@@ -390,3 +428,131 @@ test('rest handlers round-trip a share: preview exports a bundle and install rec
390
428
  await handlePreview({}, empty.sendJson);
391
429
  assert.equal(empty.result.status, 400);
392
430
  });
431
+
432
+ test('app-backed tool paths export as bindings and install rewritten to the installed app', async () => {
433
+ const { defaultToolboxService } = require('../toolbox/service');
434
+ const appDir = makeTempDir('bundle-appbound-src-');
435
+ fs.mkdirSync(path.join(appDir, 'tool'), { recursive: true });
436
+ fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ name: 'appbound' }));
437
+ fs.writeFileSync(path.join(appDir, 'server.js'), 'console.log("serve");\n');
438
+ fs.writeFileSync(path.join(appDir, 'tool', 'cli.mjs'), 'console.log("tool");\n');
439
+ fs.writeFileSync(path.join(appDir, 'tool', 'SKILL.md'), '# Tool skill\n');
440
+
441
+ const app = registerSampleApp(appDir, {
442
+ id: 'app-appbound-test',
443
+ name: 'App Bound',
444
+ appRef: 'appboundref01',
445
+ startCommand: 'PORT={port} node server.js',
446
+ });
447
+ defaultToolboxService.registerTool({
448
+ id: 'appbound.tool',
449
+ name: 'App Bound Tool',
450
+ type: 'cli',
451
+ origin: 'user',
452
+ source: {
453
+ command: 'node',
454
+ args: [path.join(appDir, 'tool', 'cli.mjs')],
455
+ cwd: appDir,
456
+ inputMode: 'json-stdin',
457
+ outputMode: 'json',
458
+ },
459
+ guide: { kind: 'skill', path: path.join(appDir, 'tool', 'SKILL.md') },
460
+ });
461
+
462
+ const { bundle } = createBundle({ appIds: [app.id], toolIds: ['appbound.tool'] });
463
+
464
+ const toolEntry = bundle.tools.find((tool) => tool.id === 'appbound.tool');
465
+ const bindings = toolEntry.appPathBindings;
466
+ assert.equal(Array.isArray(bindings), true);
467
+ const byLabel = Object.fromEntries(bindings.map((b) => [b.path.join('.'), b]));
468
+ assert.deepEqual(byLabel['source.cwd'], { appId: app.id, path: ['source', 'cwd'], relative: '' });
469
+ assert.deepEqual(byLabel['source.args.0'], { appId: app.id, path: ['source', 'args', 0], relative: 'tool/cli.mjs' });
470
+ assert.deepEqual(byLabel['guide.path'], { appId: app.id, path: ['guide', 'path'], relative: 'tool/SKILL.md' });
471
+ // Bound paths are portable — no manual rebinding requirements remain.
472
+ assert.equal(bundle.requires.bindings.some((entry) => entry.includes('appbound.tool')), false);
473
+
474
+ const revived = JSON.parse(JSON.stringify(bundle));
475
+ const rootDir = makeTempDir('bundle-appbound-install-');
476
+ const result = await installBundle(revived, {
477
+ appsRootDir: rootDir,
478
+ registerApp: async (input) => ({ id: 'app-installed-appbound', ...input, status: 'running' }),
479
+ });
480
+
481
+ const installedAppDir = result.installedApps[0].cwd;
482
+ assert.equal(installedAppDir.startsWith(rootDir), true);
483
+ const installedTool = result.installedTools.find((tool) => tool.id === 'appbound.tool');
484
+ assert.equal(installedTool.source.cwd, installedAppDir);
485
+ assert.equal(installedTool.source.args[0], path.join(installedAppDir, 'tool', 'cli.mjs'));
486
+ assert.equal(installedTool.guide.path, path.join(installedAppDir, 'tool', 'SKILL.md'));
487
+ // The rewritten paths point at real materialized files.
488
+ assert.equal(fs.existsSync(installedTool.source.args[0]), true);
489
+ assert.equal(fs.existsSync(installedTool.guide.path), true);
490
+ // Nothing in the installed tool leaks the sharer's directory.
491
+ assert.equal(JSON.stringify(installedTool).includes(appDir), false);
492
+ assert.equal(result.warnings.some((warning) => warning.includes('bound to app')), false);
493
+ });
494
+
495
+ test('app-backed tool paths still bind when the tool uses a symlink alias of the app cwd', () => {
496
+ const { defaultToolboxService } = require('../toolbox/service');
497
+ const appDir = makeTempDir('bundle-appbound-alias-src-');
498
+ const aliasParent = makeTempDir('bundle-appbound-alias-link-');
499
+ const aliasDir = path.join(aliasParent, 'app-alias');
500
+ fs.mkdirSync(path.join(appDir, 'tool'), { recursive: true });
501
+ fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ name: 'appbound-alias' }));
502
+ fs.writeFileSync(path.join(appDir, 'server.js'), 'console.log("serve");\n');
503
+ fs.writeFileSync(path.join(appDir, 'tool', 'cli.mjs'), 'console.log("tool");\n');
504
+ fs.writeFileSync(path.join(appDir, 'tool', 'SKILL.md'), '# Tool skill\n');
505
+ fs.symlinkSync(appDir, aliasDir, 'dir');
506
+
507
+ const app = registerSampleApp(appDir, {
508
+ id: 'app-appbound-alias-test',
509
+ name: 'App Bound Alias',
510
+ appRef: 'appboundalias01',
511
+ startCommand: 'PORT={port} node server.js',
512
+ });
513
+ defaultToolboxService.registerTool({
514
+ id: 'appbound.alias.tool',
515
+ name: 'App Bound Alias Tool',
516
+ type: 'cli',
517
+ origin: 'user',
518
+ source: {
519
+ command: 'node',
520
+ args: [path.join(aliasDir, 'tool', 'cli.mjs')],
521
+ cwd: aliasDir,
522
+ inputMode: 'json-stdin',
523
+ outputMode: 'json',
524
+ },
525
+ guide: { kind: 'skill', path: path.join(aliasDir, 'tool', 'SKILL.md') },
526
+ });
527
+
528
+ const { bundle } = createBundle({ appIds: [app.id], toolIds: ['appbound.alias.tool'] });
529
+ const toolEntry = bundle.tools.find((tool) => tool.id === 'appbound.alias.tool');
530
+ const bindings = toolEntry.appPathBindings;
531
+ assert.equal(Array.isArray(bindings), true);
532
+ const byLabel = Object.fromEntries(bindings.map((binding) => [binding.path.join('.'), binding]));
533
+ assert.deepEqual(byLabel['source.cwd'], { appId: app.id, path: ['source', 'cwd'], relative: '' });
534
+ assert.deepEqual(byLabel['source.args.0'], { appId: app.id, path: ['source', 'args', 0], relative: 'tool/cli.mjs' });
535
+ assert.deepEqual(byLabel['guide.path'], { appId: app.id, path: ['guide', 'path'], relative: 'tool/SKILL.md' });
536
+ assert.equal(bundle.requires.bindings.some((entry) => entry.includes('appbound.alias.tool')), false);
537
+ });
538
+
539
+ test('installBundle defaults app materialization to the owned apps root', async () => {
540
+ const appDir = makeTempDir('bundle-appsroot-src-');
541
+ fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ name: 'appsroot' }));
542
+ fs.writeFileSync(path.join(appDir, 'server.js'), 'console.log("serve");\n');
543
+ const app = registerSampleApp(appDir, {
544
+ id: 'app-appsroot-test',
545
+ name: 'Apps Root',
546
+ appRef: 'appsrootref01',
547
+ startCommand: 'PORT={port} node server.js',
548
+ });
549
+
550
+ const { bundle } = createBundle({ appIds: [app.id] });
551
+ const result = await installBundle(JSON.parse(JSON.stringify(bundle)), {
552
+ registerApp: async (input) => ({ id: 'app-installed-appsroot', ...input, status: 'running' }),
553
+ });
554
+
555
+ const expectedRoot = path.join(process.env.AMALGM_DIR, 'apps');
556
+ assert.equal(result.installedApps[0].cwd.startsWith(expectedRoot), true);
557
+ assert.equal(fs.existsSync(path.join(result.installedApps[0].cwd, 'server.js')), true);
558
+ });
@@ -22,7 +22,7 @@ function isWithin(root, target) {
22
22
  }
23
23
 
24
24
  function legacyWorkspaceRoots() {
25
- const home = process.env.AMALGM_HOME || path.join(os.homedir(), '.amalgm');
25
+ const home = require('../amalgm-mcp/lib/layout').resolveAmalgmHome(process.env);
26
26
  return [
27
27
  process.env.AMALGM_LEGACY_WORKSPACES_DIR,
28
28
  path.join(home, 'workspaces'),
@@ -33,7 +33,7 @@ function legacyWorkspaceRoots() {
33
33
  }
34
34
 
35
35
  function workspaceRootPaths() {
36
- const home = process.env.AMALGM_HOME || path.join(os.homedir(), '.amalgm');
36
+ const home = require('../amalgm-mcp/lib/layout').resolveAmalgmHome(process.env);
37
37
  const stateRoot = process.env.AMALGM_DIR;
38
38
  return [
39
39
  process.env.AMALGM_WORKSPACES_DIR,
@@ -11,7 +11,7 @@ function normalizeRuntimeLabel(value) {
11
11
  }
12
12
 
13
13
  function amalgmDir() {
14
- return process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
14
+ return process.env.AMALGM_DIR || require('../amalgm-mcp/lib/layout').resolveAmalgmHome(process.env);
15
15
  }
16
16
 
17
17
  function runtimeLabel() {