@entrinsik/vite-plugin-informer 2.6.0-beta.1 → 2.6.0-beta.2

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/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import type { Plugin } from 'vite';
2
2
 
3
3
  /**
4
- * Dev-only binding for a `target: app` dependency slot. Points `request()` at
5
- * the target app in dev; overrides the manifest `defaultBinding` when both are
6
- * present.
4
+ * Dev-only binding for a `target: app` or `target: pack` dependency slot.
5
+ * Points `request()` at the target app in dev. For app slots it overrides the
6
+ * manifest `defaultBinding` when both are present; for pack slots it is the
7
+ * only way to bind — the marketplace pin has no install to resolve against in
8
+ * dev, so point it at your locally-installed copy of the pack's app.
7
9
  *
8
10
  * - `app` — the target app (`owner:slug` or UUID). Powers `request()`.
9
11
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entrinsik/vite-plugin-informer",
3
- "version": "2.6.0-beta.1",
3
+ "version": "2.6.0-beta.2",
4
4
  "description": "Vite plugin and deploy tool for Informer App development",
5
5
  "scripts": {
6
6
  "test": "node --test"
package/src/agent-dev.js CHANGED
@@ -29,21 +29,23 @@ async function loadInformerYaml(projectRoot) {
29
29
  }
30
30
 
31
31
  /**
32
- * Scan the tools/ directory for .js files that export a handler.
32
+ * Scan the tools/ and mcp/ directories for .js files that export a handler.
33
+ * Both share one tool namespace; agents may reference tools from either.
33
34
  *
34
35
  * @param {string} projectRoot
35
36
  * @returns {Promise<Array<{ name: string, filePath: string }>>}
36
37
  */
37
38
  async function scanLocalTools(projectRoot) {
38
- const toolsDir = join(projectRoot, 'tools');
39
- try {
40
- await access(toolsDir);
41
- } catch {
42
- return [];
43
- }
44
-
45
39
  const tools = [];
46
- await walkToolFiles(toolsDir, 'tools', tools);
40
+ for (const dirName of ['tools', 'mcp']) {
41
+ const toolsDir = join(projectRoot, dirName);
42
+ try {
43
+ await access(toolsDir);
44
+ } catch {
45
+ continue;
46
+ }
47
+ await walkToolFiles(toolsDir, dirName, tools);
48
+ }
47
49
  return tools;
48
50
  }
49
51
 
@@ -64,7 +66,7 @@ async function walkToolFiles(dir, basePath, results) {
64
66
  await walkToolFiles(full, childPath, results);
65
67
  } else if (item.endsWith('.js')) {
66
68
  const name = childPath
67
- .replace(/^tools\//, '')
69
+ .replace(/^(tools|mcp)\//, '')
68
70
  .replace(/\.js$/, '')
69
71
  .replace(/\//g, '_');
70
72
  results.push({ name, filePath: full });
package/src/assemble.js CHANGED
@@ -15,7 +15,7 @@ import { join, relative, posix } from 'node:path';
15
15
  * (server/…, tools/…) — matching how an app's library is structured in Informer.
16
16
  */
17
17
  const ROOT_CONFIG_FILES = ['informer.yaml', 'data-access.yaml'];
18
- const SOURCE_DIRS = ['migrations', 'tools', 'server', 'webhooks', 'lib', 'shared'];
18
+ const SOURCE_DIRS = ['migrations', 'tools', 'mcp', 'server', 'webhooks', 'lib', 'shared'];
19
19
 
20
20
  // Entries never worth shipping in an app's server-side library: OS/editor
21
21
  // dotfiles (.DS_Store, .env), nested dependency trees, and test files. Applied
package/src/deploy.js CHANGED
@@ -105,7 +105,7 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
105
105
  await api.post(`${entityPath}/files/_clear`);
106
106
 
107
107
  // 6. Upload the app-library file set: dist output at the library root, plus
108
- // informer.yaml / data-access.yaml and the server/tools/migrations/webhooks
108
+ // informer.yaml / data-access.yaml and the server/tools/mcp/migrations/webhooks
109
109
  // source trees. Sourced from the shared collectAppFiles() so a deploy and a
110
110
  // marketplace publish package byte-identical contents.
111
111
  console.log('Uploading files...');
@@ -160,6 +160,9 @@ export async function deploy({ baseUrl, apiKey, user, pass, distDir, name, descr
160
160
  if (result.tools && result.tools.length > 0) {
161
161
  console.log(` Registered ${result.tools.length} tool(s): ${result.tools.join(', ')}`);
162
162
  }
163
+ if (result.mcpTools && result.mcpTools.length > 0) {
164
+ console.log(` Registered ${result.mcpTools.length} MCP tool(s): ${result.mcpTools.join(', ')}`);
165
+ }
163
166
  if (result.agents && result.agents.length > 0) {
164
167
  console.log(` Deployed ${result.agents.length} agent(s): ${result.agents.join(', ')}`);
165
168
  }
@@ -150,11 +150,13 @@ export function buildDevMessaging(logPrefix = '[app]') {
150
150
  // reject the same shapes with the same wording. Keep these in lockstep.
151
151
  const DEPENDENCY_NAME_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/;
152
152
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
153
- const VALID_TARGETS = new Set(['dataset', 'query', 'datasource', 'integration', 'app']);
153
+ const VALID_TARGETS = new Set(['dataset', 'query', 'datasource', 'integration', 'app', 'pack']);
154
154
  const VALID_RUN_AS = new Set(['user', 'owner']);
155
155
 
156
156
  // Targets that accept `defaultBinding:` in the manifest — mirrors
157
157
  // DEFAULT_BINDING_LOOKUP in deploy.js. All resolve the target under read_access.
158
+ // `pack` is deliberately absent: its identity is the marketplace pin
159
+ // (`pack:` + `requires:`), and the installer consents per instance.
158
160
  const DEFAULT_BINDABLE_TARGETS = new Set(['dataset', 'query', 'datasource', 'integration', 'app']);
159
161
 
160
162
  const METHOD_SURFACE = {
@@ -162,7 +164,8 @@ const METHOD_SURFACE = {
162
164
  query: ['execute'],
163
165
  datasource: ['query'],
164
166
  integration: ['request'],
165
- app: ['request']
167
+ app: ['request'],
168
+ pack: ['request']
166
169
  };
167
170
 
168
171
  // Cross-app request() method allow-list — mirrors REQUEST_METHODS in
@@ -246,6 +249,11 @@ export function validateDependencies(deps) {
246
249
  if (!VALID_RUN_AS.has(runAs)) {
247
250
  errors.push(`dependency "${name}": runAs must be 'user' or 'owner' (got "${runAs}")`);
248
251
  }
252
+ // `target: pack` pins marketplace identity at the top level of the
253
+ // declaration — same check (and wording) as deploy.js.
254
+ if (decl.target === 'pack' && (typeof decl.pack !== 'string' || typeof decl.requires !== 'string')) {
255
+ errors.push(`dependency "${name}" (pack) requires "pack: <marketplace slug>" and "requires: <semver range>"`);
256
+ }
249
257
  if (decl.defaultBinding != null) {
250
258
  if (!DEFAULT_BINDABLE_TARGETS.has(decl.target)) {
251
259
  // Same rejection the deploy performs — surface it at dev boot
@@ -268,9 +276,12 @@ export function validateDependencies(deps) {
268
276
  * the first call reports the slot as unbound — telling the developer to add the
269
277
  * thing they think they already added.
270
278
  *
271
- * A binding key must name a declared `target: app` dependency; its value must be
272
- * a non-empty `owner:slug` string (shorthand for `{ app }`) or an object with a
273
- * non-empty `app` (the target app, for request()).
279
+ * A binding key must name a declared `target: app` or `target: pack`
280
+ * dependency; its value must be a non-empty `owner:slug` string (shorthand for
281
+ * `{ app }`) or an object with a non-empty `app` (the target app, for
282
+ * request()). For a pack slot the value points at your locally-installed copy
283
+ * of the pack's app — dev has no marketplace install to resolve the pin
284
+ * against, so the developer says where it lives.
274
285
  *
275
286
  * @param {Object} deps - The raw `dependencies:` object from informer.yaml
276
287
  * @param {Object} devBindings - The plugin's `devBindings` option
@@ -285,8 +296,8 @@ export function validateDevBindings(deps, devBindings) {
285
296
  errors.push(`devBindings."${name}": no dependency "${name}" is declared in informer.yaml`);
286
297
  continue;
287
298
  }
288
- if (decl.target !== 'app') {
289
- errors.push(`devBindings."${name}": only "target: app" dependencies take a devBinding (got "${decl.target}")`);
299
+ if (decl.target !== 'app' && decl.target !== 'pack') {
300
+ errors.push(`devBindings."${name}": only "target: app" and "target: pack" dependencies take a devBinding (got "${decl.target}")`);
290
301
  continue;
291
302
  }
292
303
  if (typeof binding === 'string') {
@@ -356,17 +367,20 @@ export function buildDevContext({ deps, apiFetch, devBindings = {}, appFetch = n
356
367
  const target = decl.target;
357
368
  if (!VALID_TARGETS.has(target)) continue;
358
369
 
359
- // App slots bind from the plugin's `devBindings` (a human `owner:slug`
360
- // pointing request() at the target app), falling back to the manifest
361
- // `defaultBinding` UUID like every other target. request() is the only
362
- // surface.
363
- if (target === 'app') {
364
- const fallback = (typeof decl.defaultBinding === 'string' && UUID_PATTERN.test(decl.defaultBinding))
370
+ // App and pack slots bind from the plugin's `devBindings` (a human
371
+ // `owner:slug` pointing request() at the target app). App slots fall
372
+ // back to the manifest `defaultBinding` UUID; pack slots have no
373
+ // fallback — the pin resolves via marketplace installs, which dev
374
+ // doesn't have, so the devBinding names the locally-installed app.
375
+ // request() is the only surface either way.
376
+ if (target === 'app' || target === 'pack') {
377
+ const fallback = (target === 'app'
378
+ && typeof decl.defaultBinding === 'string' && UUID_PATTERN.test(decl.defaultBinding))
365
379
  ? decl.defaultBinding
366
380
  : null;
367
381
  const binding = devBindings[name] != null ? devBindings[name] : fallback;
368
382
  context[name] = binding
369
- ? makeAppDevProxy({ name, binding, appFetch })
383
+ ? makeAppDevProxy({ name, binding, appFetch, kind: target })
370
384
  : makeUnboundDevProxy({ name, target });
371
385
  continue;
372
386
  }
@@ -383,7 +397,12 @@ export function buildDevContext({ deps, apiFetch, devBindings = {}, appFetch = n
383
397
  }
384
398
 
385
399
  /**
386
- * Dev proxy for a `target: app` slot. `request()` is the only surface.
400
+ * Dev proxy for a `target: app` or `target: pack` slot. `request()` is the
401
+ * only surface. Pack slots reuse this wholesale — in production the pack
402
+ * driver resolves its marketplace pin and then delegates the runtime to the
403
+ * app driver, and the devBinding IS that resolution done by hand. The prod
404
+ * version gate (pack_dependency_out_of_range) is not emulated: dev has no
405
+ * pack_install to read a version from.
387
406
  *
388
407
  * Production runs `request()` through the target's own /view/_/ dispatch, and
389
408
  * dev injects into the same route:
@@ -404,29 +423,31 @@ export function buildDevContext({ deps, apiFetch, devBindings = {}, appFetch = n
404
423
  * manifest defaultBinding UUID). A bare string is shorthand for `{ app }`.
405
424
  * @param {Function|null} args.appFetch - token-authed fetch, or null when
406
425
  * INFORMER_APP_TOKEN is unset.
426
+ * @param {'app'|'pack'} [args.kind] - slot flavor, for error labels and the
427
+ * structured resourceType guest code branches on.
407
428
  */
408
- function makeAppDevProxy({ name, binding, appFetch }) {
429
+ function makeAppDevProxy({ name, binding, appFetch, kind = 'app' }) {
409
430
  const { app } = resolveAppBinding(binding);
410
431
 
411
432
  return {
412
433
  async request(payload) {
413
434
  if (!app) {
414
435
  throw new Error(
415
- `Dependency "${name}" (app): dev request() needs the target app — set devBindings.${name}.app (e.g. 'admin:kanban')`
436
+ `Dependency "${name}" (${kind}): dev request() needs the target app — set devBindings.${name}.app (e.g. 'admin:kanban')`
416
437
  );
417
438
  }
418
439
  if (!appFetch) {
419
440
  throw new Error(
420
- `Dependency "${name}" (app): dev request() needs a Bearer credential — the target's /view/_/ dispatch does not accept basic auth. Run in API-key mode (INFORMER_API_KEY), or create a token (Admin → Tokens, or POST /api/tokens) and set INFORMER_APP_TOKEN in .env`
441
+ `Dependency "${name}" (${kind}): dev request() needs a Bearer credential — the target's /view/_/ dispatch does not accept basic auth. Run in API-key mode (INFORMER_API_KEY), or create a token (Admin → Tokens, or POST /api/tokens) and set INFORMER_APP_TOKEN in .env`
421
442
  );
422
443
  }
423
444
  const { method = 'GET', url, params, data } = payload || {};
424
445
  const httpMethod = String(method).toUpperCase();
425
446
  if (!REQUEST_METHODS.includes(httpMethod)) {
426
- throw new Error(`Dependency "${name}" (app): unsupported method "${method}"`);
447
+ throw new Error(`Dependency "${name}" (${kind}): unsupported method "${method}"`);
427
448
  }
428
449
  if (!url || typeof url !== 'string') {
429
- throw new Error(`Dependency "${name}" (app): request({ url }) requires the target route path`);
450
+ throw new Error(`Dependency "${name}" (${kind}): request({ url }) requires the target route path`);
430
451
  }
431
452
  const path = url.replace(/^\/+/, '');
432
453
  const search = params ? `?${new URLSearchParams(params).toString()}` : '';
@@ -440,17 +461,17 @@ function makeAppDevProxy({ name, binding, appFetch }) {
440
461
  try {
441
462
  resolvedPath = new URL(`${targetPrefix}${path}${search}`, 'http://localhost').pathname;
442
463
  } catch {
443
- throw new Error(`Dependency "${name}" (app): request({ url }) is malformed`);
464
+ throw new Error(`Dependency "${name}" (${kind}): request({ url }) is malformed`);
444
465
  }
445
466
  if (!resolvedPath.startsWith(targetPrefix)) {
446
- throw new Error(`Dependency "${name}" (app): request({ url }) must not escape the target app with ".." path segments`);
467
+ throw new Error(`Dependency "${name}" (${kind}): request({ url }) must not escape the target app with ".." path segments`);
447
468
  }
448
469
  const { status, body, contentType } = await appFetch(
449
470
  `apps/${encodeURIComponent(app)}/view/_/${path}${search}`,
450
471
  { method: httpMethod, body: data }
451
472
  );
452
473
  if (status >= 400) {
453
- throw dependencyCallError(name, 'app', status, body);
474
+ throw dependencyCallError(name, kind, status, body);
454
475
  }
455
476
  // Success-envelope contract mirrors entity-type/app.js: JSON → parsed
456
477
  // body; non-JSON text/HTML → text envelope; binary → not emulatable
@@ -458,7 +479,7 @@ function makeAppDevProxy({ name, binding, appFetch }) {
458
479
  // silently hand back mangled bytes.
459
480
  if (isBinaryContentType(contentType)) {
460
481
  throw new Error(
461
- `Dependency "${name}" (app): the dev proxy can't return binary responses yet (upstream content-type "${contentType}"). Test binary endpoints against a deployed build.`
482
+ `Dependency "${name}" (${kind}): the dev proxy can't return binary responses yet (upstream content-type "${contentType}"). Test binary endpoints against a deployed build.`
462
483
  );
463
484
  }
464
485
  // (An empty content-type gets the text envelope too, matching prod.)
@@ -514,10 +535,14 @@ function makeDevProxy({ name, target, targetId, apiFetch }) {
514
535
  function makeUnboundDevProxy({ name, target }) {
515
536
  const methods = METHOD_SURFACE[target] || [];
516
537
  // App slots bind via devBindings (a dev-local owner:slug) or a manifest
517
- // defaultBinding UUID, so point at whichever the reader is likelier to want.
538
+ // defaultBinding UUID; pack slots ONLY via devBindings (the pin has no
539
+ // marketplace install to resolve against in dev). Point at whichever the
540
+ // reader is likelier to want.
518
541
  const hint = target === 'app'
519
542
  ? `add \`devBindings: { ${name}: { app: '<owner:slug>' } }\` to the informer() plugin options in vite.config.js, or \`defaultBinding: <uuid>\` to its entry in informer.yaml`
520
- : 'add `defaultBinding: <uuid>` to its entry in informer.yaml';
543
+ : target === 'pack'
544
+ ? `add \`devBindings: { ${name}: { app: '<owner:slug>' } }\` to the informer() plugin options in vite.config.js, pointing at your locally-installed copy of the pack`
545
+ : 'add `defaultBinding: <uuid>` to its entry in informer.yaml';
521
546
  const proxy = {};
522
547
  for (const method of methods) {
523
548
  proxy[method] = async () => {
package/src/index.js CHANGED
@@ -45,8 +45,10 @@ async function writeAppDepTypes (projectRoot, dts) {
45
45
  * @param {Object} [options]
46
46
  * @param {{ report?: object, theme?: 'light'|'dark', roles?: string[] }} [options.mock]
47
47
  * window.__INFORMER__ mock injected in dev.
48
- * @param {Object} [options.devBindings] - dev bindings for `target: app` deps,
49
- * which can't be defaultBound in the manifest. See AppDevBinding in index.d.ts.
48
+ * @param {Object} [options.devBindings] - dev bindings for `target: app` and
49
+ * `target: pack` deps. App slots can't be defaultBound in the manifest; pack
50
+ * slots resolve their marketplace pin via installs dev doesn't have, so the
51
+ * binding names the locally-installed app. See AppDevBinding in index.d.ts.
50
52
  * @param {Object} [options.proxy] - extra Vite proxy options merged onto /api.
51
53
  * @returns {import('vite').Plugin}
52
54
  */
@@ -184,9 +186,9 @@ export default function informer(options = {}) {
184
186
  console.error(`[informer] vite.config.js: ${message}`);
185
187
  }
186
188
 
187
- // Generate .d.ts types for bound `target: app` deps from their
188
- // published OpenAPI docs, so server/ handlers get typed
189
- // context.<slot>.request()/query() autocomplete.
189
+ // Generate .d.ts types for bound `target: app` / `target: pack`
190
+ // deps from their published OpenAPI docs, so server/ handlers get
191
+ // typed context.<slot>.request()/query() autocomplete.
190
192
  try {
191
193
  // A bare string devBinding is shorthand for { app } — request()
192
194
  // works under it, so its types must generate too, not just for
@@ -194,7 +196,7 @@ export default function informer(options = {}) {
194
196
  const boundAppRef = (name) =>
195
197
  resolveAppBinding(options.devBindings && options.devBindings[name]).app;
196
198
  const appSlots = Object.entries(deps).filter(
197
- ([name, decl]) => decl && decl.target === 'app' && boundAppRef(name)
199
+ ([name, decl]) => decl && (decl.target === 'app' || decl.target === 'pack') && boundAppRef(name)
198
200
  );
199
201
  if (appSlots.length) {
200
202
  const specs = {};
@@ -257,8 +259,9 @@ export default function informer(options = {}) {
257
259
  devWorkspaceId,
258
260
  projectRoot,
259
261
  roles: (options.mock && options.mock.roles) || [],
260
- // Dev-only bindings for `target: app` slots (override the
261
- // manifest defaultBinding for local dev). Shape:
262
+ // Dev-only bindings for `target: app` / `target: pack`
263
+ // slots (app: overrides the manifest defaultBinding; pack:
264
+ // names the locally-installed pack app). Shape:
262
265
  // devBindings: { kanban: { app: 'admin:kanban' } }
263
266
  devBindings: options.devBindings || {},
264
267
  appToken
@@ -266,11 +269,12 @@ export default function informer(options = {}) {
266
269
  server.middlewares.use('/api/_server', serverRoutes);
267
270
  }
268
271
 
269
- // Mount agent dev middleware if tools/ or informer.yaml agents exist
272
+ // Mount agent dev middleware if tools/, mcp/, or informer.yaml agents exist
270
273
  const toolsDir = resolve(projectRoot, 'tools');
274
+ const mcpDir = resolve(projectRoot, 'mcp');
271
275
  const yamlPath = resolve(projectRoot, 'informer.yaml');
272
276
 
273
- if (existsSync(toolsDir) || existsSync(yamlPath)) {
277
+ if (existsSync(toolsDir) || existsSync(mcpDir) || existsSync(yamlPath)) {
274
278
  const agentDev = createAgentMiddleware(server, {
275
279
  serverOrigin,
276
280
  authHeader,