@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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +66 -0
- package/README.md +45 -0
- package/dist/cli.js +411 -221
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +66 -2
- package/dist/index.js +407 -210
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/bundler.ts +278 -92
- package/src/cli.ts +4 -12
- package/src/generator.ts +223 -204
- package/src/image-definition/types.ts +72 -0
- package/src/image-definition/validate-image-definition.ts +70 -0
- package/src/index.ts +2 -1
- package/src/types.ts +1 -1
- package/test/bundle.integration.test.ts +253 -0
- package/tsconfig.json +5 -1
- package/tsup.config.ts +6 -1
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
|
-
|
|
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,
|
|
14
|
-
const imports = generateImports(definition,
|
|
15
|
-
const
|
|
16
|
-
const
|
|
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
|
|
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
|
-
${
|
|
75
|
+
${helpers}
|
|
25
76
|
|
|
26
|
-
${
|
|
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
|
|
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(
|
|
39
|
-
imports.push(`import '${definition.extends}';`);
|
|
40
|
-
imports.push(``);
|
|
97
|
+
imports.push(`import baseImage from '${definition.extends}';`);
|
|
41
98
|
}
|
|
42
|
-
imports.push(`import {
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
|
134
|
-
return
|
|
135
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
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
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
|
|
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.
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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 {
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
339
|
+
console.log(`\u{1F50D} Discovering factories from folders...`);
|
|
272
340
|
const imageDir = dirname(options.imagePath);
|
|
273
|
-
const
|
|
274
|
-
console.log(`\u2705 Discovered ${
|
|
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,
|
|
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
|
|
282
|
-
const categories = ["blob-store", "tools", "compaction", "plugins"];
|
|
349
|
+
console.log(`\u{1F528} Compiling factory source files...`);
|
|
283
350
|
let compiledCount = 0;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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}
|
|
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
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
543
|
+
function discoverFactories(imageDir, warnings) {
|
|
439
544
|
const result = {
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
458
|
-
const entryPath = join(
|
|
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 (
|
|
469
|
-
|
|
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("
|
|
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
|
-
`
|
|
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(
|
|
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);
|