@larkup/marketplace 0.1.3 → 0.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @larkup/marketplace
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 19767f8: Fix isolated Marketplace tool loading in Docker, improve Video & Audio setup guidance, and reliably index PDF images.
8
+
9
+ ## 0.1.4
10
+
11
+ ### Patch Changes
12
+
13
+ - c769d08: Retry marketplace installs against the latest published package when a stale catalog version is unavailable, and improve server and analytics UI behavior.
14
+
3
15
  ## 0.1.3
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larkup/marketplace",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -147,8 +147,15 @@ export async function checkSystemDeps(toolId: string): Promise<string[]> {
147
147
  const descriptor = await getToolById(toolId);
148
148
  if (!descriptor?.systemDeps?.length) return [];
149
149
 
150
+ const target = detectDeploymentTarget();
151
+
150
152
  const missing: string[] = [];
151
153
  for (const dep of descriptor.systemDeps) {
154
+ // A container cannot normally talk to the host Docker daemon. Do not block
155
+ // installation of tools that use Docker only for optional capabilities
156
+ // (for example DOCX/PPTX editing); those capabilities report their own
157
+ // actionable error when actually used.
158
+ if (target === 'docker' && dep === 'docker') continue;
152
159
  try {
153
160
  await execAsync(`which ${dep}`);
154
161
  } catch {
@@ -226,7 +233,7 @@ async function execInstall(
226
233
  version: string,
227
234
  target: DeploymentTarget,
228
235
  onProgress?: (message: string) => void,
229
- ): Promise<string> {
236
+ ): Promise<{ resolvedPath: string; version: string }> {
230
237
  const toolsDir = getToolsDir();
231
238
 
232
239
  switch (target) {
@@ -235,22 +242,39 @@ async function execInstall(
235
242
  // Initialize the isolated tools directory
236
243
  await ensureToolsPackageJson();
237
244
 
238
- const installCmd = `npm install ${packageName}@${version} --prefix "${toolsDir}" --save --no-audit --no-fund`;
239
- onProgress?.(`Running: npm install ${packageName}@${version}`);
240
-
241
- try {
242
- const { stdout, stderr } = await execAsync(installCmd, {
245
+ const install = async (specifier: string) => {
246
+ const installCmd = `npm install ${specifier} --prefix "${toolsDir}" --save --no-audit --no-fund`;
247
+ onProgress?.(`Running: npm install ${specifier}`);
248
+ const { stderr } = await execAsync(installCmd, {
243
249
  cwd: toolsDir,
244
250
  timeout: 120_000, // 2 minute timeout
245
251
  env: { ...process.env, NODE_ENV: 'production' },
246
252
  });
247
-
248
253
  if (stderr && !stderr.includes('npm warn')) {
249
254
  console.warn(`[marketplace] Install stderr: ${stderr}`);
250
255
  }
256
+ };
257
+
258
+ try {
259
+ await install(`${packageName}@${version}`);
251
260
  } catch (err) {
252
261
  const message = err instanceof Error ? err.message : 'Install command failed';
253
- throw new Error(`Failed to install ${packageName}: ${message}`);
262
+ if (!message.includes('ETARGET') && !message.includes('No matching version found')) {
263
+ throw new Error(`Failed to install ${packageName}: ${message}`);
264
+ }
265
+
266
+ // Hub entries can be cached longer than npm releases. Recover from an
267
+ // obsolete catalog version rather than leaving the marketplace unusable.
268
+ onProgress?.(
269
+ `Catalog version ${version} is unavailable; installing the latest published version…`,
270
+ );
271
+ try {
272
+ await install(`${packageName}@latest`);
273
+ } catch (latestErr) {
274
+ const latestMessage =
275
+ latestErr instanceof Error ? latestErr.message : 'Install command failed';
276
+ throw new Error(`Failed to install ${packageName}: ${latestMessage}`);
277
+ }
254
278
  }
255
279
 
256
280
  // Resolve the actual module path
@@ -263,7 +287,10 @@ async function execInstall(
263
287
  );
264
288
  }
265
289
 
266
- return resolvedPath;
290
+ const packageJson = JSON.parse(
291
+ await fs.readFile(path.join(resolvedPath, 'package.json'), 'utf8'),
292
+ ) as { version?: string };
293
+ return { resolvedPath, version: packageJson.version ?? version };
267
294
  }
268
295
 
269
296
  case 'serverless': {
@@ -282,7 +309,7 @@ async function execInstall(
282
309
  await fs.writeFile(pendingPath, JSON.stringify(pending, null, 2), 'utf8');
283
310
 
284
311
  // Return the expected path (will be available after redeploy)
285
- return path.join(toolsDir, 'node_modules', packageName);
312
+ return { resolvedPath: path.join(toolsDir, 'node_modules', packageName), version };
286
313
  }
287
314
 
288
315
  case 'sandbox': {
@@ -291,7 +318,7 @@ async function execInstall(
291
318
  await ensureToolsPackageJson();
292
319
  const installCmd = `npm install ${packageName}@${version} --prefix "${toolsDir}" --save --no-audit --no-fund`;
293
320
  await execAsync(installCmd, { cwd: toolsDir, timeout: 120_000 });
294
- return path.join(toolsDir, 'node_modules', packageName);
321
+ return { resolvedPath: path.join(toolsDir, 'node_modules', packageName), version };
295
322
  }
296
323
 
297
324
  default:
@@ -374,6 +401,7 @@ export async function installTool(
374
401
  const isWorkspace = await isWorkspaceTool(descriptor.packageName);
375
402
  const target = detectDeploymentTarget();
376
403
  let resolvedPath: string;
404
+ let installedVersion = descriptor.version;
377
405
  let source: ToolSource;
378
406
 
379
407
  if (isWorkspace) {
@@ -394,9 +422,14 @@ export async function installTool(
394
422
  } else {
395
423
  // Remote install — download from npm into isolated directory
396
424
  report('downloading', 30, `Downloading ${descriptor.packageName}@${descriptor.version}…`);
397
- resolvedPath = await execInstall(descriptor.packageName, descriptor.version, target, (msg) =>
398
- report('downloading', 50, msg),
425
+ const installed = await execInstall(
426
+ descriptor.packageName,
427
+ descriptor.version,
428
+ target,
429
+ (msg) => report('downloading', 50, msg),
399
430
  );
431
+ resolvedPath = installed.resolvedPath;
432
+ installedVersion = installed.version;
400
433
  source = target === 'sandbox' ? 'sandbox' : 'registry';
401
434
  report('installing', 70, 'Package installed.');
402
435
  }
@@ -409,7 +442,7 @@ export async function installTool(
409
442
 
410
443
  const entry: InstalledTool = {
411
444
  id: toolId,
412
- version: descriptor.version,
445
+ version: installedVersion,
413
446
  installedAt: new Date().toISOString(),
414
447
  packageName: descriptor.packageName,
415
448
  resolvedPath,
@@ -466,6 +499,8 @@ export async function uninstallTool(toolId: string): Promise<void> {
466
499
 
467
500
  // Refresh registry cache
468
501
  invalidateRegistryCache();
502
+ const { unloadTool } = await import('./tool-loader');
503
+ unloadTool(toolId);
469
504
  }
470
505
 
471
506
  export async function updateToolConfig(toolId: string, config: Record<string, any>): Promise<void> {
@@ -1,4 +1,6 @@
1
+ import { promises as fs } from 'node:fs';
1
2
  import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
2
4
  import type { InstalledTool } from './types';
3
5
  import { getInstalledTool } from './tool-installer';
4
6
 
@@ -37,7 +39,7 @@ export async function loadTool<T = any>(toolId: string): Promise<T | null> {
37
39
 
38
40
  try {
39
41
  // Determine what to import
40
- const importPath = resolveImportPath(installed);
42
+ const importPath = await resolveImportPath(installed);
41
43
  const mod = await import(/* webpackIgnore: true */ importPath);
42
44
  moduleCache.set(toolId, mod);
43
45
  return mod as T;
@@ -54,7 +56,7 @@ export async function loadTool<T = any>(toolId: string): Promise<T | null> {
54
56
  * - `registry` (npm): import from the isolated node_modules via absolute path
55
57
  * - `sandbox`: import from the sandbox tool path
56
58
  */
57
- function resolveImportPath(installed: InstalledTool): string {
59
+ async function resolveImportPath(installed: InstalledTool): Promise<string> {
58
60
  switch (installed.source) {
59
61
  case 'local':
60
62
  // In monorepo, the package name resolves via pnpm workspace linking
@@ -62,8 +64,10 @@ function resolveImportPath(installed: InstalledTool): string {
62
64
 
63
65
  case 'registry':
64
66
  case 'sandbox':
65
- // Use the absolute resolved path from the isolated install
66
- return installed.resolvedPath;
67
+ // Node ESM cannot import a package directory by absolute path. Resolve
68
+ // the package's declared ESM entry so isolated marketplace installs work
69
+ // the same way as a normal package-name import.
70
+ return resolvePackageEntry(installed.resolvedPath);
67
71
 
68
72
  default:
69
73
  // Fallback: try package name (backwards compat)
@@ -71,6 +75,22 @@ function resolveImportPath(installed: InstalledTool): string {
71
75
  }
72
76
  }
73
77
 
78
+ async function resolvePackageEntry(packageDir: string): Promise<string> {
79
+ const manifestPath = path.join(packageDir, 'package.json');
80
+ const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')) as {
81
+ exports?: string | { '.': string | { import?: string; default?: string } };
82
+ main?: string;
83
+ };
84
+ const rootExport =
85
+ typeof manifest.exports === 'object' ? manifest.exports['.'] : manifest.exports;
86
+ const entry =
87
+ (typeof rootExport === 'object' ? rootExport.import ?? rootExport.default : rootExport) ??
88
+ manifest.main ??
89
+ 'index.js';
90
+
91
+ return pathToFileURL(path.resolve(packageDir, entry)).href;
92
+ }
93
+
74
94
  /**
75
95
  * Check if a tool is loaded and available for use.
76
96
  */
@@ -143,7 +143,7 @@ const FALLBACK_REGISTRY: Record<string, ToolDescriptor> = {
143
143
  name: 'Video & Audio',
144
144
  description: 'Index video and audio files with transcription and frame analysis.',
145
145
  category: 'media',
146
- version: '0.2.0',
146
+ version: '0.2.1',
147
147
  pricing: 'free',
148
148
  emoji: '🎬',
149
149
  icon: 'Film',
@@ -226,7 +226,7 @@ const FALLBACK_REGISTRY: Record<string, ToolDescriptor> = {
226
226
  name: 'Document Editor',
227
227
  description: 'AI-powered form filling and document editing with Canvas-style live preview.',
228
228
  category: 'utility',
229
- version: '0.2.1',
229
+ version: '0.2.3',
230
230
  pricing: 'free',
231
231
  emoji: '📝',
232
232
  icon: 'FileEdit',