@dexto/image-bundler 1.5.7 → 1.6.0

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/dist/cli.js CHANGED
@@ -5,153 +5,168 @@ import { Command } from "commander";
5
5
 
6
6
  // src/bundler.ts
7
7
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from "fs";
8
+ import { mkdtemp, rm } from "fs/promises";
9
+ import { createRequire } from "module";
8
10
  import { dirname, join, resolve, extname } from "path";
9
11
  import { pathToFileURL } from "url";
10
- import { validateImageDefinition } from "@dexto/core";
12
+
13
+ // src/image-definition/validate-image-definition.ts
14
+ function validateImageDefinition(definition) {
15
+ if (!definition.name || typeof definition.name !== "string") {
16
+ throw new Error("Image name must be a non-empty string");
17
+ }
18
+ if (!definition.version || typeof definition.version !== "string") {
19
+ throw new Error("Image version must be a non-empty string");
20
+ }
21
+ if (!definition.description || typeof definition.description !== "string") {
22
+ throw new Error("Image description must be a non-empty string");
23
+ }
24
+ const versionRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$/;
25
+ if (!versionRegex.test(definition.version)) {
26
+ throw new Error(
27
+ `Image version '${definition.version}' is not valid semver. Expected format: x.y.z`
28
+ );
29
+ }
30
+ if (definition.target !== void 0) {
31
+ if (typeof definition.target !== "string" || definition.target.trim().length === 0) {
32
+ throw new Error(`Image target must be a non-empty string when provided`);
33
+ }
34
+ }
35
+ if (definition.constraints) {
36
+ if (!Array.isArray(definition.constraints)) {
37
+ throw new Error("Image constraints must be an array");
38
+ }
39
+ for (const constraint of definition.constraints) {
40
+ if (typeof constraint !== "string" || constraint.trim().length === 0) {
41
+ throw new Error(`Image constraint must be a non-empty string`);
42
+ }
43
+ }
44
+ }
45
+ if (definition.utils) {
46
+ for (const [name, path] of Object.entries(definition.utils)) {
47
+ if (typeof path !== "string") {
48
+ throw new Error(`Utility '${name}' path must be a string`);
49
+ }
50
+ if (!path.startsWith("./")) {
51
+ throw new Error(
52
+ `Utility '${name}' path must be relative (start with './'). Got: ${path}`
53
+ );
54
+ }
55
+ }
56
+ }
57
+ if (definition.extends) {
58
+ if (typeof definition.extends !== "string") {
59
+ throw new Error("Image extends must be a string (parent image name)");
60
+ }
61
+ }
62
+ }
11
63
 
12
64
  // src/generator.ts
13
- function generateEntryPoint(definition, coreVersion, discoveredProviders) {
14
- const imports = generateImports(definition, discoveredProviders);
15
- const registrations = generateProviderRegistrations(definition, discoveredProviders);
16
- const factory = generateFactory();
65
+ function generateEntryPoint(definition, discoveredFactories) {
66
+ const imports = generateImports(definition, discoveredFactories);
67
+ const helpers = definition.extends ? generateHelpers() : "";
68
+ const imageModule = generateImageModule(definition, discoveredFactories);
17
69
  const utilityExports = generateUtilityExports(definition);
18
- const metadata = generateMetadata(definition, coreVersion);
19
- const js = `// AUTO-GENERATED by @dexto/bundler
70
+ const js = `// AUTO-GENERATED by @dexto/image-bundler
20
71
  // Do not edit this file directly. Edit dexto.image.ts instead.
21
72
 
22
73
  ${imports}
23
74
 
24
- ${registrations}
75
+ ${helpers}
25
76
 
26
- ${factory}
77
+ ${imageModule}
27
78
 
28
79
  ${utilityExports}
29
-
30
- ${metadata}
31
80
  `;
32
81
  const dts = generateTypeDefinitions(definition);
33
82
  return { js, dts };
34
83
  }
35
- function generateImports(definition, discoveredProviders) {
84
+ function sanitizeIdentifier(value) {
85
+ const sanitized = value.replace(/[^a-zA-Z0-9_$]/g, "_");
86
+ if (/^[a-zA-Z_$]/.test(sanitized)) {
87
+ return sanitized;
88
+ }
89
+ return `_${sanitized}`;
90
+ }
91
+ function toFactoryImportSymbol(prefix, type) {
92
+ return sanitizeIdentifier(`${prefix}_${type}`);
93
+ }
94
+ function generateImports(definition, discoveredFactories) {
36
95
  const imports = [];
37
96
  if (definition.extends) {
38
- imports.push(`// Import base image for provider registration (side effect)`);
39
- imports.push(`import '${definition.extends}';`);
40
- imports.push(``);
97
+ imports.push(`import baseImage from '${definition.extends}';`);
41
98
  }
42
- imports.push(`import { DextoAgent } from '@dexto/core';`);
43
- imports.push(
44
- `import { customToolRegistry, pluginRegistry, compactionRegistry, blobStoreRegistry } from '@dexto/core';`
99
+ imports.push(`import { defaultLoggerFactory } from '@dexto/core';`);
100
+ const toolProviders = [...discoveredFactories.tools].sort(
101
+ (a, b) => a.type.localeCompare(b.type)
45
102
  );
46
- if (discoveredProviders) {
47
- const categories = [
48
- { key: "blobStore", label: "Blob Storage" },
49
- { key: "customTools", label: "Custom Tools" },
50
- { key: "compaction", label: "Compaction" },
51
- { key: "plugins", label: "Plugins" }
52
- ];
53
- for (const { key, label } of categories) {
54
- const providers = discoveredProviders[key];
55
- if (providers.length > 0) {
56
- imports.push(``);
57
- imports.push(`// ${label} providers (auto-discovered)`);
58
- providers.forEach((path, index) => {
59
- const varName = `${key}Provider${index}`;
60
- imports.push(`import * as ${varName} from '${path}';`);
61
- });
62
- }
63
- }
103
+ const hookProviders = [...discoveredFactories.hooks].sort(
104
+ (a, b) => a.type.localeCompare(b.type)
105
+ );
106
+ const compactionProviders = [...discoveredFactories.compaction].sort(
107
+ (a, b) => a.type.localeCompare(b.type)
108
+ );
109
+ const blobProviders = [...discoveredFactories.storage.blob].sort(
110
+ (a, b) => a.type.localeCompare(b.type)
111
+ );
112
+ const databaseProviders = [...discoveredFactories.storage.database].sort(
113
+ (a, b) => a.type.localeCompare(b.type)
114
+ );
115
+ const cacheProviders = [...discoveredFactories.storage.cache].sort(
116
+ (a, b) => a.type.localeCompare(b.type)
117
+ );
118
+ if (toolProviders.length > 0 || hookProviders.length > 0 || compactionProviders.length > 0 || blobProviders.length > 0 || databaseProviders.length > 0 || cacheProviders.length > 0) {
119
+ imports.push("");
120
+ imports.push("// Factories (convention folders; each must `export const factory = ...`)");
64
121
  }
65
- return imports.join("\n");
66
- }
67
- function generateProviderRegistrations(definition, discoveredProviders) {
68
- const registrations = [];
69
- if (definition.extends) {
70
- registrations.push(
71
- `// Base image providers already registered via import of '${definition.extends}'`
72
- );
73
- registrations.push("");
74
- }
75
- registrations.push("// SIDE EFFECT: Register providers on import");
76
- registrations.push("");
77
- if (discoveredProviders) {
78
- const categoryMap = [
79
- { key: "blobStore", registry: "blobStoreRegistry", label: "Blob Storage" },
80
- { key: "customTools", registry: "customToolRegistry", label: "Custom Tools" },
81
- { key: "compaction", registry: "compactionRegistry", label: "Compaction" },
82
- { key: "plugins", registry: "pluginRegistry", label: "Plugins" }
83
- ];
84
- for (const { key, registry, label } of categoryMap) {
85
- const providers = discoveredProviders[key];
86
- if (providers.length === 0) continue;
87
- registrations.push(`// Auto-register ${label} providers`);
88
- providers.forEach((path, index) => {
89
- const varName = `${key}Provider${index}`;
90
- registrations.push(`// From ${path}`);
91
- registrations.push(`for (const exported of Object.values(${varName})) {`);
92
- registrations.push(
93
- ` if (exported && typeof exported === 'object' && 'type' in exported && 'create' in exported) {`
94
- );
95
- registrations.push(` try {`);
96
- registrations.push(` ${registry}.register(exported);`);
97
- registrations.push(
98
- ` console.log(\`\u2713 Registered ${key}: \${exported.type}\`);`
99
- );
100
- registrations.push(` } catch (err) {`);
101
- registrations.push(` // Ignore duplicate registration errors`);
102
- registrations.push(
103
- ` if (!err.message?.includes('already registered')) throw err;`
104
- );
105
- registrations.push(` }`);
106
- registrations.push(` }`);
107
- registrations.push(`}`);
108
- });
109
- registrations.push("");
110
- }
122
+ for (const entry of toolProviders) {
123
+ const symbol = toFactoryImportSymbol("tools", entry.type);
124
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
111
125
  }
112
- for (const [category, config] of Object.entries(definition.providers)) {
113
- if (!config) continue;
114
- if (config.register) {
115
- registrations.push(`// Register ${category} via custom function (from dexto.image.ts)`);
116
- registrations.push(`await (async () => {`);
117
- registrations.push(` try {`);
118
- registrations.push(
119
- ` ${config.register.toString().replace(/^async\s*\(\)\s*=>\s*{/, "").replace(/}$/, "")}`
120
- );
121
- registrations.push(` } catch (err) {`);
122
- registrations.push(` // Ignore duplicate registration errors`);
123
- registrations.push(` if (!err.message?.includes('already registered')) {`);
124
- registrations.push(` throw err;`);
125
- registrations.push(` }`);
126
- registrations.push(` }`);
127
- registrations.push(`})();`);
128
- registrations.push("");
129
- }
126
+ for (const entry of hookProviders) {
127
+ const symbol = toFactoryImportSymbol("hooks", entry.type);
128
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
129
+ }
130
+ for (const entry of compactionProviders) {
131
+ const symbol = toFactoryImportSymbol("compaction", entry.type);
132
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
133
+ }
134
+ for (const entry of blobProviders) {
135
+ const symbol = toFactoryImportSymbol("storage_blob", entry.type);
136
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
137
+ }
138
+ for (const entry of databaseProviders) {
139
+ const symbol = toFactoryImportSymbol("storage_database", entry.type);
140
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
130
141
  }
131
- return registrations.join("\n");
142
+ for (const entry of cacheProviders) {
143
+ const symbol = toFactoryImportSymbol("storage_cache", entry.type);
144
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
145
+ }
146
+ return imports.join("\n");
132
147
  }
133
- function generateFactory() {
134
- return `/**
135
- * Create a Dexto agent using this image's registered providers.
136
- *
137
- * @param config - Agent configuration
138
- * @param configPath - Optional path to config file
139
- * @returns DextoAgent instance with providers already registered
140
- */
141
- export function createAgent(config, configPath) {
142
- return new DextoAgent(config, configPath);
148
+ function generateHelpers() {
149
+ return `function isPlainObject(value) {
150
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
143
151
  }
144
152
 
145
- /**
146
- * Re-export registries for runtime customization.
147
- * This allows apps to add custom providers without depending on @dexto/core directly.
148
- */
149
- export {
150
- customToolRegistry,
151
- pluginRegistry,
152
- compactionRegistry,
153
- blobStoreRegistry,
154
- } from '@dexto/core';`;
153
+ function mergeImageDefaults(baseDefaults, overrideDefaults) {
154
+ if (!baseDefaults) return overrideDefaults;
155
+ if (!overrideDefaults) return baseDefaults;
156
+
157
+ const merged = { ...baseDefaults, ...overrideDefaults };
158
+ for (const [key, baseValue] of Object.entries(baseDefaults)) {
159
+ const overrideValue = overrideDefaults[key];
160
+ if (!isPlainObject(baseValue) || !isPlainObject(overrideValue)) {
161
+ continue;
162
+ }
163
+ merged[key] = {
164
+ ...baseValue,
165
+ ...overrideValue,
166
+ };
167
+ }
168
+ return merged;
169
+ }`;
155
170
  }
156
171
  function generateUtilityExports(definition) {
157
172
  const sections = [];
@@ -182,27 +197,92 @@ function generateUtilityExports(definition) {
182
197
  }
183
198
  return sections.join("\n");
184
199
  }
185
- function generateMetadata(definition, coreVersion) {
186
- const metadata = {
187
- name: definition.name,
188
- version: definition.version,
189
- description: definition.description,
190
- target: definition.target || "custom",
191
- constraints: definition.constraints || [],
192
- builtAt: (/* @__PURE__ */ new Date()).toISOString(),
193
- coreVersion
194
- };
195
- if (definition.extends) {
196
- metadata.extends = definition.extends;
200
+ function generateImageModule(definition, discoveredFactories) {
201
+ const derivedDefaults = definition.defaults !== void 0 ? JSON.stringify(definition.defaults, null, 4) : "undefined";
202
+ const toolsEntries = discoveredFactories.tools.slice().sort((a, b) => a.type.localeCompare(b.type)).map((entry) => {
203
+ const symbol = toFactoryImportSymbol("tools", entry.type);
204
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
205
+ });
206
+ const hookEntries = discoveredFactories.hooks.slice().sort((a, b) => a.type.localeCompare(b.type)).map((entry) => {
207
+ const symbol = toFactoryImportSymbol("hooks", entry.type);
208
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
209
+ });
210
+ const compactionEntries = discoveredFactories.compaction.slice().sort((a, b) => a.type.localeCompare(b.type)).map((entry) => {
211
+ const symbol = toFactoryImportSymbol("compaction", entry.type);
212
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
213
+ });
214
+ const blobEntries = discoveredFactories.storage.blob.slice().sort((a, b) => a.type.localeCompare(b.type)).map((entry) => {
215
+ const symbol = toFactoryImportSymbol("storage_blob", entry.type);
216
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
217
+ });
218
+ const databaseEntries = discoveredFactories.storage.database.slice().sort((a, b) => a.type.localeCompare(b.type)).map((entry) => {
219
+ const symbol = toFactoryImportSymbol("storage_database", entry.type);
220
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
221
+ });
222
+ const cacheEntries = discoveredFactories.storage.cache.slice().sort((a, b) => a.type.localeCompare(b.type)).map((entry) => {
223
+ const symbol = toFactoryImportSymbol("storage_cache", entry.type);
224
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
225
+ });
226
+ const metadataLines = [];
227
+ metadataLines.push(` name: ${JSON.stringify(definition.name)},`);
228
+ metadataLines.push(` version: ${JSON.stringify(definition.version)},`);
229
+ metadataLines.push(` description: ${JSON.stringify(definition.description)},`);
230
+ if (definition.target !== void 0) {
231
+ metadataLines.push(` target: ${JSON.stringify(definition.target)},`);
232
+ } else if (definition.extends) {
233
+ metadataLines.push(` target: baseImage.metadata.target,`);
197
234
  }
198
- if (definition.bundledPlugins && definition.bundledPlugins.length > 0) {
199
- metadata.bundledPlugins = definition.bundledPlugins;
235
+ if (definition.extends) {
236
+ const derivedConstraints = JSON.stringify(definition.constraints ?? []);
237
+ metadataLines.push(
238
+ ` constraints: Array.from(new Set([...(baseImage.metadata.constraints ?? []), ...${derivedConstraints}])),`
239
+ );
240
+ } else if (definition.constraints !== void 0) {
241
+ metadataLines.push(` constraints: ${JSON.stringify(definition.constraints)},`);
200
242
  }
201
- return `/**
202
- * Image metadata
203
- * Generated at build time
204
- */
205
- export const imageMetadata = ${JSON.stringify(metadata, null, 4)};`;
243
+ const defaultsExpression = definition.extends ? `mergeImageDefaults(baseImage.defaults, ${derivedDefaults})` : derivedDefaults;
244
+ const toolsSpread = definition.extends ? ` ...baseImage.tools,
245
+ ` : "";
246
+ const hooksSpread = definition.extends ? ` ...baseImage.hooks,
247
+ ` : "";
248
+ const compactionSpread = definition.extends ? ` ...baseImage.compaction,
249
+ ` : "";
250
+ const blobSpread = definition.extends ? ` ...baseImage.storage.blob,
251
+ ` : "";
252
+ const databaseSpread = definition.extends ? ` ...baseImage.storage.database,
253
+ ` : "";
254
+ const cacheSpread = definition.extends ? ` ...baseImage.storage.cache,
255
+ ` : "";
256
+ const loggerExpression = definition.extends ? `baseImage.logger ?? defaultLoggerFactory` : `defaultLoggerFactory`;
257
+ return `const image = {
258
+ metadata: {
259
+ ${metadataLines.join("\n")}
260
+ },
261
+ defaults: ${defaultsExpression},
262
+ tools: {
263
+ ${toolsSpread}${toolsEntries.join("\n")}
264
+ },
265
+ storage: {
266
+ blob: {
267
+ ${blobSpread}${blobEntries.join("\n")}
268
+ },
269
+ database: {
270
+ ${databaseSpread}${databaseEntries.join("\n")}
271
+ },
272
+ cache: {
273
+ ${cacheSpread}${cacheEntries.join("\n")}
274
+ },
275
+ },
276
+ hooks: {
277
+ ${hooksSpread}${hookEntries.join("\n")}
278
+ },
279
+ compaction: {
280
+ ${compactionSpread}${compactionEntries.join("\n")}
281
+ },
282
+ logger: ${loggerExpression},
283
+ };
284
+
285
+ export default image;`;
206
286
  }
207
287
  function generateTypeDefinitions(definition) {
208
288
  const sections = [];
@@ -229,31 +309,19 @@ function generateTypeDefinitions(definition) {
229
309
  return `// AUTO-GENERATED TypeScript definitions
230
310
  // Do not edit this file directly
231
311
 
232
- import type { DextoAgent, AgentConfig, ImageMetadata } from '@dexto/core';
233
-
234
- /**
235
- * Create a Dexto agent using this image's registered providers.
236
- */
237
- export declare function createAgent(config: AgentConfig, configPath?: string): DextoAgent;
238
-
239
- /**
240
- * Image metadata
241
- */
242
- export declare const imageMetadata: ImageMetadata;
312
+ import type { DextoImage } from '@dexto/agent-config';
243
313
 
244
- /**
245
- * Re-exported registries for runtime customization
246
- */
247
- export {
248
- customToolRegistry,
249
- pluginRegistry,
250
- compactionRegistry,
251
- blobStoreRegistry,
252
- } from '@dexto/core';${utilityExports}
253
- `;
314
+ /**
315
+ * Typed image module (no side effects)
316
+ */
317
+ declare const image: DextoImage;
318
+ export default image;
319
+ ${utilityExports}
320
+ `;
254
321
  }
255
322
 
256
323
  // src/bundler.ts
324
+ import { build } from "esbuild";
257
325
  import ts from "typescript";
258
326
  async function bundle(options) {
259
327
  const warnings = [];
@@ -268,31 +336,55 @@ async function bundle(options) {
268
336
  throw new Error(`Image validation failed: ${error}`);
269
337
  }
270
338
  const coreVersion = getCoreVersion();
271
- console.log(`\u{1F50D} Discovering providers from folders...`);
339
+ console.log(`\u{1F50D} Discovering factories from folders...`);
272
340
  const imageDir = dirname(options.imagePath);
273
- const discoveredProviders = discoverProviders(imageDir);
274
- console.log(`\u2705 Discovered ${discoveredProviders.totalCount} provider(s)`);
341
+ const discoveredFactories = discoverFactories(imageDir, warnings);
342
+ console.log(`\u2705 Discovered ${discoveredFactories.totalCount} factory(ies)`);
275
343
  console.log(`\u{1F528} Generating entry point...`);
276
- const generated = generateEntryPoint(definition, coreVersion, discoveredProviders);
344
+ const generated = generateEntryPoint(definition, discoveredFactories);
277
345
  const outDir = resolve(options.outDir);
278
346
  if (!existsSync(outDir)) {
279
347
  mkdirSync(outDir, { recursive: true });
280
348
  }
281
- console.log(`\u{1F528} Compiling provider source files...`);
282
- const categories = ["blob-store", "tools", "compaction", "plugins"];
349
+ console.log(`\u{1F528} Compiling factory source files...`);
283
350
  let compiledCount = 0;
284
- for (const category of categories) {
285
- const categoryDir = join(imageDir, category);
286
- if (existsSync(categoryDir)) {
287
- compileSourceFiles(categoryDir, join(outDir, category));
288
- compiledCount++;
289
- }
351
+ const toolsDir = join(imageDir, "tools");
352
+ if (existsSync(toolsDir)) {
353
+ compileSourceFiles(toolsDir, join(outDir, "tools"));
354
+ compiledCount++;
355
+ }
356
+ const hooksDir = join(imageDir, "hooks");
357
+ if (existsSync(hooksDir)) {
358
+ compileSourceFiles(hooksDir, join(outDir, "hooks"));
359
+ compiledCount++;
360
+ }
361
+ const compactionDir = join(imageDir, "compaction");
362
+ if (existsSync(compactionDir)) {
363
+ compileSourceFiles(compactionDir, join(outDir, "compaction"));
364
+ compiledCount++;
365
+ }
366
+ const storageBlobDir = join(imageDir, "storage", "blob");
367
+ if (existsSync(storageBlobDir)) {
368
+ compileSourceFiles(storageBlobDir, join(outDir, "storage", "blob"));
369
+ compiledCount++;
370
+ }
371
+ const storageDatabaseDir = join(imageDir, "storage", "database");
372
+ if (existsSync(storageDatabaseDir)) {
373
+ compileSourceFiles(storageDatabaseDir, join(outDir, "storage", "database"));
374
+ compiledCount++;
375
+ }
376
+ const storageCacheDir = join(imageDir, "storage", "cache");
377
+ if (existsSync(storageCacheDir)) {
378
+ compileSourceFiles(storageCacheDir, join(outDir, "storage", "cache"));
379
+ compiledCount++;
290
380
  }
291
381
  if (compiledCount > 0) {
292
382
  console.log(
293
- `\u2705 Compiled ${compiledCount} provider categor${compiledCount === 1 ? "y" : "ies"}`
383
+ `\u2705 Compiled ${compiledCount} factory categor${compiledCount === 1 ? "y" : "ies"}`
294
384
  );
295
385
  }
386
+ console.log(`\u{1F50D} Validating factory exports...`);
387
+ await validateDiscoveredFactories(outDir, discoveredFactories);
296
388
  const entryFile = join(outDir, "index.js");
297
389
  const typesFile = join(outDir, "index.d.ts");
298
390
  console.log(`\u{1F4DD} Writing ${entryFile}...`);
@@ -325,13 +417,29 @@ async function loadImageDefinition(imagePath) {
325
417
  throw new Error(`Image file not found: ${absolutePath}`);
326
418
  }
327
419
  try {
328
- const fileUrl = pathToFileURL(absolutePath).href;
329
- const module = await import(fileUrl);
330
- const definition = module.default;
331
- if (!definition) {
332
- throw new Error("Image file must have a default export");
420
+ const imageDir = dirname(absolutePath);
421
+ const tempDir = await mkdtemp(join(imageDir, ".dexto-image-definition-"));
422
+ const compiledPath = join(tempDir, "dexto.image.mjs");
423
+ try {
424
+ await build({
425
+ entryPoints: [absolutePath],
426
+ outfile: compiledPath,
427
+ bundle: true,
428
+ platform: "node",
429
+ format: "esm",
430
+ target: "node20",
431
+ packages: "external",
432
+ logLevel: "silent"
433
+ });
434
+ const module = await import(pathToFileURL(compiledPath).href);
435
+ const definition = module.default;
436
+ if (!definition) {
437
+ throw new Error("Image file must have a default export");
438
+ }
439
+ return definition;
440
+ } finally {
441
+ await rm(tempDir, { recursive: true, force: true });
333
442
  }
334
- return definition;
335
443
  } catch (error) {
336
444
  if (error instanceof Error) {
337
445
  throw new Error(`Failed to load image definition: ${error.message}`);
@@ -341,12 +449,9 @@ async function loadImageDefinition(imagePath) {
341
449
  }
342
450
  function getCoreVersion() {
343
451
  try {
344
- const corePackageJson = join(process.cwd(), "node_modules/@dexto/core/package.json");
345
- if (existsSync(corePackageJson)) {
346
- const pkg = JSON.parse(readFileSync(corePackageJson, "utf-8"));
347
- return pkg.version;
348
- }
349
- return "1.0.0";
452
+ const require2 = createRequire(import.meta.url);
453
+ const pkg = require2("@dexto/core/package.json");
454
+ return typeof pkg.version === "string" ? pkg.version : "1.0.0";
350
455
  } catch {
351
456
  return "1.0.0";
352
457
  }
@@ -435,58 +540,144 @@ function findTypeScriptFiles(dir) {
435
540
  walk(dir);
436
541
  return files;
437
542
  }
438
- function discoverProviders(imageDir) {
543
+ function discoverFactories(imageDir, warnings) {
439
544
  const result = {
440
- blobStore: [],
441
- customTools: [],
545
+ tools: [],
546
+ storage: {
547
+ blob: [],
548
+ database: [],
549
+ cache: []
550
+ },
551
+ hooks: [],
442
552
  compaction: [],
443
- plugins: [],
444
553
  totalCount: 0
445
554
  };
446
- const categories = {
447
- "blob-store": "blobStore",
448
- tools: "customTools",
449
- compaction: "compaction",
450
- plugins: "plugins"
451
- };
452
- for (const [folderName, propName] of Object.entries(categories)) {
453
- const categoryDir = join(imageDir, folderName);
454
- if (!existsSync(categoryDir)) {
455
- continue;
555
+ const discoverFolder = (options) => {
556
+ const { srcDir, importBase, label } = options;
557
+ if (!existsSync(srcDir)) {
558
+ return [];
456
559
  }
457
- const providerFolders = readdirSync(categoryDir).filter((entry) => {
458
- const entryPath = join(categoryDir, entry);
560
+ const factoryFolders = readdirSync(srcDir).filter((entry) => {
561
+ const entryPath = join(srcDir, entry);
459
562
  const stat = statSync(entryPath);
460
563
  if (!stat.isDirectory()) {
461
564
  return false;
462
565
  }
463
566
  const indexPath = join(entryPath, "index.ts");
464
567
  return existsSync(indexPath);
465
- }).map((folder) => {
466
- return `./${folderName}/${folder}/index.js`;
467
568
  });
468
- if (providerFolders.length > 0) {
469
- result[propName].push(
470
- ...providerFolders
471
- );
472
- result.totalCount += providerFolders.length;
473
- console.log(` Found ${providerFolders.length} provider(s) in ${folderName}/`);
569
+ if (factoryFolders.length > 0) {
570
+ console.log(` Found ${factoryFolders.length} factory(ies) in ${label}`);
474
571
  }
572
+ return factoryFolders.map((type) => ({
573
+ type,
574
+ importPath: `./${importBase}/${type}/index.js`
575
+ }));
576
+ };
577
+ result.tools = discoverFolder({
578
+ srcDir: join(imageDir, "tools"),
579
+ importBase: "tools",
580
+ label: "tools/"
581
+ });
582
+ result.hooks = discoverFolder({
583
+ srcDir: join(imageDir, "hooks"),
584
+ importBase: "hooks",
585
+ label: "hooks/"
586
+ });
587
+ result.compaction = discoverFolder({
588
+ srcDir: join(imageDir, "compaction"),
589
+ importBase: "compaction",
590
+ label: "compaction/"
591
+ });
592
+ result.storage.blob = discoverFolder({
593
+ srcDir: join(imageDir, "storage", "blob"),
594
+ importBase: "storage/blob",
595
+ label: "storage/blob/"
596
+ });
597
+ result.storage.database = discoverFolder({
598
+ srcDir: join(imageDir, "storage", "database"),
599
+ importBase: "storage/database",
600
+ label: "storage/database/"
601
+ });
602
+ result.storage.cache = discoverFolder({
603
+ srcDir: join(imageDir, "storage", "cache"),
604
+ importBase: "storage/cache",
605
+ label: "storage/cache/"
606
+ });
607
+ result.totalCount = result.tools.length + result.hooks.length + result.compaction.length + result.storage.blob.length + result.storage.database.length + result.storage.cache.length;
608
+ if (result.totalCount === 0) {
609
+ warnings.push(
610
+ "No factories discovered from convention folders. This image will not be able to resolve tools/storage unless it extends a base image."
611
+ );
475
612
  }
476
613
  return result;
477
614
  }
615
+ async function validateFactoryExport(options) {
616
+ const { outDir, kind, entry } = options;
617
+ const absolutePath = resolve(outDir, entry.importPath);
618
+ const fileUrl = pathToFileURL(absolutePath).href;
619
+ let module;
620
+ try {
621
+ module = await import(fileUrl);
622
+ } catch (error) {
623
+ const message = error instanceof Error ? error.message : String(error);
624
+ throw new Error(
625
+ `Failed to import ${kind} factory '${entry.type}' (${entry.importPath}): ${message}`
626
+ );
627
+ }
628
+ if (!module || typeof module !== "object") {
629
+ throw new Error(
630
+ `Invalid ${kind} factory '${entry.type}' (${entry.importPath}): expected an object module export`
631
+ );
632
+ }
633
+ const factory = module.factory;
634
+ if (!factory || typeof factory !== "object") {
635
+ throw new Error(
636
+ `Invalid ${kind} factory '${entry.type}' (${entry.importPath}): missing 'factory' export`
637
+ );
638
+ }
639
+ const configSchema = factory.configSchema;
640
+ const create = factory.create;
641
+ const parse = configSchema?.parse;
642
+ if (!configSchema || typeof configSchema !== "object" || typeof parse !== "function") {
643
+ throw new Error(
644
+ `Invalid ${kind} factory '${entry.type}' (${entry.importPath}): factory.configSchema must be a Zod schema`
645
+ );
646
+ }
647
+ if (typeof create !== "function") {
648
+ throw new Error(
649
+ `Invalid ${kind} factory '${entry.type}' (${entry.importPath}): factory.create must be a function`
650
+ );
651
+ }
652
+ }
653
+ async function validateDiscoveredFactories(outDir, discovered) {
654
+ const validations = [];
655
+ for (const entry of discovered.tools) {
656
+ validations.push(validateFactoryExport({ outDir, kind: "tool", entry }));
657
+ }
658
+ for (const entry of discovered.hooks) {
659
+ validations.push(validateFactoryExport({ outDir, kind: "hook", entry }));
660
+ }
661
+ for (const entry of discovered.compaction) {
662
+ validations.push(validateFactoryExport({ outDir, kind: "compaction", entry }));
663
+ }
664
+ for (const entry of discovered.storage.blob) {
665
+ validations.push(validateFactoryExport({ outDir, kind: "storage.blob", entry }));
666
+ }
667
+ for (const entry of discovered.storage.database) {
668
+ validations.push(validateFactoryExport({ outDir, kind: "storage.database", entry }));
669
+ }
670
+ for (const entry of discovered.storage.cache) {
671
+ validations.push(validateFactoryExport({ outDir, kind: "storage.cache", entry }));
672
+ }
673
+ await Promise.all(validations);
674
+ }
478
675
 
479
676
  // src/cli.ts
480
677
  import { readFileSync as readFileSync2 } from "fs";
481
678
  import { join as join2, dirname as dirname2 } from "path";
482
679
  import { fileURLToPath } from "url";
483
680
  import pc from "picocolors";
484
- process.removeAllListeners("warning");
485
- process.on("warning", (warning) => {
486
- if (warning.name !== "ExperimentalWarning") {
487
- console.warn(warning);
488
- }
489
- });
490
681
  var __filename2 = fileURLToPath(import.meta.url);
491
682
  var __dirname2 = dirname2(__filename2);
492
683
  var packageJson = JSON.parse(readFileSync2(join2(__dirname2, "../package.json"), "utf-8"));
@@ -525,15 +716,14 @@ program.command("build").description("Build a base image from dexto.image.ts").o
525
716
  } catch {
526
717
  }
527
718
  console.log(pc.green("\n\u2705 Image is ready to use!"));
528
- console.log(" To use this image in an app:");
719
+ console.log(" Install into the Dexto CLI:");
720
+ console.log(pc.dim(" 1. Install: dexto image install ."));
529
721
  console.log(
530
722
  pc.dim(
531
- ` 1. Install it: pnpm add ${packageName}@file:../${packageName.split("/").pop()}`
723
+ ` 2. Use it: set \`image: "${packageName}"\` in your agent config (or pass --image in the CLI)`
532
724
  )
533
725
  );
534
- console.log(pc.dim(` 2. Import it: import { createAgent } from '${packageName}';`));
535
- console.log(pc.dim(`
536
- Or publish to npm and install normally.`));
726
+ console.log(pc.dim("\n Or publish to npm and install by package name."));
537
727
  } catch (error) {
538
728
  console.error(pc.red("\n\u274C Build failed:"), error);
539
729
  process.exit(1);