@telorun/kernel 0.2.4 → 0.2.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/dist/boot-context-registry.d.ts.map +1 -1
- package/dist/boot-context-registry.js +6 -6
- package/dist/boot-context-registry.js.map +1 -1
- package/dist/capabilities/capabilities/component.yaml +2 -1
- package/dist/capabilities/capabilities/executable.yaml +2 -1
- package/dist/capabilities/capabilities/handler.yaml +2 -1
- package/dist/capabilities/capabilities/listener.yaml +2 -1
- package/dist/capabilities/capabilities/provider.yaml +2 -1
- package/dist/capabilities/capabilities/template.yaml +2 -1
- package/dist/capabilities/capabilities/type.yaml +2 -1
- package/dist/capabilities/component.yaml +1 -1
- package/dist/capabilities/executable.yaml +1 -1
- package/dist/capabilities/handler.yaml +1 -1
- package/dist/capabilities/listener.yaml +1 -1
- package/dist/capabilities/provider.yaml +1 -1
- package/dist/capabilities/template.yaml +1 -1
- package/dist/capabilities/type.yaml +1 -1
- package/dist/controller-loader.d.ts +1 -1
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +4 -2
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-registry.d.ts +1 -2
- package/dist/controller-registry.d.ts.map +1 -1
- package/dist/controller-registry.js.map +1 -1
- package/dist/controllers/module/import-controller.d.ts +38 -0
- package/dist/controllers/module/import-controller.d.ts.map +1 -0
- package/dist/controllers/module/import-controller.js +119 -0
- package/dist/controllers/module/import-controller.js.map +1 -0
- package/dist/controllers/module/module-controller.d.ts +57 -11
- package/dist/controllers/module/module-controller.d.ts.map +1 -1
- package/dist/controllers/module/module-controller.js +46 -82
- package/dist/controllers/module/module-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +12 -4
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +91 -0
- package/dist/evaluation-context.d.ts.map +1 -0
- package/dist/evaluation-context.js +220 -0
- package/dist/evaluation-context.js.map +1 -0
- package/dist/execution-context.d.ts +13 -0
- package/dist/execution-context.d.ts.map +1 -0
- package/dist/execution-context.js +14 -0
- package/dist/execution-context.js.map +1 -0
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +23 -31
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +212 -333
- package/dist/kernel.js.map +1 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +29 -9
- package/dist/loader.js.map +1 -1
- package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -1
- package/dist/manifest-adapters/local-file-adapter.js +21 -12
- package/dist/manifest-adapters/local-file-adapter.js.map +1 -1
- package/dist/manifest-schemas.d.ts +1 -25
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +3 -22
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context-registry.d.ts +48 -0
- package/dist/module-context-registry.d.ts.map +1 -0
- package/dist/module-context-registry.js +91 -0
- package/dist/module-context-registry.js.map +1 -0
- package/dist/module-context.d.ts +31 -0
- package/dist/module-context.d.ts.map +1 -0
- package/dist/module-context.js +67 -0
- package/dist/module-context.js.map +1 -0
- package/dist/registry.d.ts +1 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +3 -3
- package/dist/registry.js.map +1 -1
- package/dist/resource-context.d.ts +25 -5
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +74 -28
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-valiator.d.ts.map +1 -1
- package/dist/schema-valiator.js +3 -1
- package/dist/schema-valiator.js.map +1 -1
- package/dist/snapshot-serializer.d.ts +1 -2
- package/dist/snapshot-serializer.d.ts.map +1 -1
- package/dist/snapshot-serializer.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +9 -6
- package/src/boot-context-registry.ts +169 -0
- package/src/capabilities/component.yaml +4 -0
- package/src/capabilities/executable.yaml +8 -0
- package/src/capabilities/handler.yaml +4 -0
- package/src/capabilities/listener.yaml +4 -0
- package/src/capabilities/provider.yaml +4 -0
- package/src/capabilities/template.yaml +4 -0
- package/src/capabilities/type.yaml +4 -0
- package/src/controller-loader.ts +298 -0
- package/src/controller-registry.ts +206 -0
- package/src/controllers/capability/capability-controller.ts +41 -0
- package/src/controllers/module/import-controller.ts +143 -0
- package/src/controllers/module/module-controller.ts +67 -0
- package/src/controllers/module/module.json +48 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +87 -0
- package/src/controllers/resource-definition/resource-definition.json +18 -0
- package/src/event-stream.ts +121 -0
- package/src/events.ts +99 -0
- package/src/index.ts +7 -0
- package/src/kernel.ts +558 -0
- package/src/loader.ts +245 -0
- package/src/manifest-adapters/http-adapter.ts +35 -0
- package/src/manifest-adapters/local-file-adapter.ts +69 -0
- package/src/manifest-adapters/manifest-adapter.ts +33 -0
- package/src/manifest-adapters/registry-adapter.ts +56 -0
- package/src/manifest-schemas.ts +49 -0
- package/src/registry.ts +137 -0
- package/src/resource-context.ts +266 -0
- package/src/resource-uri.ts +200 -0
- package/src/schema-valiator.ts +57 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -109
- package/dist/cli.js.map +0 -1
- package/dist/expressions.d.ts +0 -20
- package/dist/expressions.d.ts.map +0 -1
- package/dist/expressions.js +0 -253
- package/dist/expressions.js.map +0 -1
- package/dist/template-definition.d.ts +0 -38
- package/dist/template-definition.d.ts.map +0 -1
- package/dist/template-definition.js +0 -26
- package/dist/template-definition.js.map +0 -1
- package/dist/template-expander.d.ts +0 -19
- package/dist/template-expander.d.ts.map +0 -1
- package/dist/template-expander.js +0 -425
- package/dist/template-expander.js.map +0 -1
- /package/{dist/src → src}/controllers/module/module.yaml +0 -0
package/src/loader.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { ResourceManifest, RuntimeResource } from "@telorun/sdk";
|
|
2
|
+
import { compile } from "@telorun/yaml-cel-templating";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { HttpAdapter } from "./manifest-adapters/http-adapter.js";
|
|
5
|
+
import { LocalFileAdapter } from "./manifest-adapters/local-file-adapter.js";
|
|
6
|
+
import type { ManifestAdapter, ManifestSourceData } from "./manifest-adapters/manifest-adapter.js";
|
|
7
|
+
import { RegistryAdapter } from "./manifest-adapters/registry-adapter.js";
|
|
8
|
+
import { formatAjvErrors, validateRuntimeResource } from "./manifest-schemas.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Loader: Ingests resolved YAML manifests from disk or remote URLs into memory
|
|
12
|
+
*/
|
|
13
|
+
export class Loader {
|
|
14
|
+
private static projectRoot: string | null = null;
|
|
15
|
+
|
|
16
|
+
private readonly adapters: ManifestAdapter[] = [
|
|
17
|
+
new HttpAdapter(),
|
|
18
|
+
new RegistryAdapter(),
|
|
19
|
+
new LocalFileAdapter(),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
private getAdapter(pathOrUrl: string): ManifestAdapter {
|
|
23
|
+
const adapter = this.adapters.find((a) => a.supports(pathOrUrl));
|
|
24
|
+
if (!adapter) {
|
|
25
|
+
throw new Error(`No manifest adapter found for: ${pathOrUrl}`);
|
|
26
|
+
}
|
|
27
|
+
return adapter;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private static ensureProjectRoot(baseDir: string): void {
|
|
31
|
+
if (!Loader.projectRoot) {
|
|
32
|
+
Loader.projectRoot = path.resolve(baseDir);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resolvePath(base: string, relative: string): string {
|
|
37
|
+
return this.getAdapter(base).resolveRelative(base, relative);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async loadDirectory(pathOrUrl: string): Promise<ResourceManifest[]> {
|
|
41
|
+
const files = await this.getAdapter(pathOrUrl).readAll(pathOrUrl);
|
|
42
|
+
Loader.ensureProjectRoot(files[0]?.baseDir ?? process.cwd());
|
|
43
|
+
const resources: RuntimeResource[] = [];
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
await this.processFile(file, resources, { env: process.env });
|
|
46
|
+
}
|
|
47
|
+
return this.orderResourcesByKindDependencies(resources);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async loadManifest(
|
|
51
|
+
pathOrUrl: string,
|
|
52
|
+
baseUrl: string,
|
|
53
|
+
compileContext: Record<string, unknown> = {},
|
|
54
|
+
): Promise<ResourceManifest[]> {
|
|
55
|
+
if (!baseUrl) {
|
|
56
|
+
throw new Error("Base URL is required to load target manifest");
|
|
57
|
+
}
|
|
58
|
+
const url = new URL(pathOrUrl, baseUrl).toString();
|
|
59
|
+
const file = await this.getAdapter(url).read(url);
|
|
60
|
+
if (!Loader.projectRoot) {
|
|
61
|
+
Loader.ensureProjectRoot(file.baseDir);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const resolved: ResourceManifest[] = [];
|
|
65
|
+
for (const rawDoc of file.documents) {
|
|
66
|
+
let compiled: any;
|
|
67
|
+
try {
|
|
68
|
+
compiled = compile(rawDoc, { context: compileContext });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Failed to compile manifest in ${file.source}: ${error instanceof Error ? error.message : String(error)}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const compiledDocs = Array.isArray(compiled) ? compiled : [compiled];
|
|
75
|
+
for (const manifest of compiledDocs) {
|
|
76
|
+
if (manifest === null) {
|
|
77
|
+
// Ignore empty documents
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const resource: ResourceManifest = {
|
|
81
|
+
...manifest,
|
|
82
|
+
metadata: {
|
|
83
|
+
...manifest.metadata,
|
|
84
|
+
source: file.source,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
resolved.push(resource);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Auto-assign metadata.module from the file's Kernel.Module declaration to
|
|
92
|
+
// any resource that doesn't explicitly declare one. This matches the implicit
|
|
93
|
+
// convention that resources in the same file belong to the declared module.
|
|
94
|
+
const moduleName = resolved.find((m) => m.kind === "Kernel.Module")?.metadata?.name;
|
|
95
|
+
if (moduleName) {
|
|
96
|
+
for (const manifest of resolved) {
|
|
97
|
+
if (manifest.kind !== "Kernel.Module" && !manifest.metadata?.module) {
|
|
98
|
+
manifest.metadata = { ...manifest.metadata, module: moduleName };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return resolved;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async processFile(
|
|
107
|
+
file: ManifestSourceData,
|
|
108
|
+
resources: RuntimeResource[],
|
|
109
|
+
compileContext: Record<string, unknown> = {},
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const documents = file.documents;
|
|
112
|
+
|
|
113
|
+
for (const rawDoc of documents) {
|
|
114
|
+
let compiled: any;
|
|
115
|
+
try {
|
|
116
|
+
compiled = compile(rawDoc, { context: compileContext });
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Failed to compile manifest in ${file.source}: ${error instanceof Error ? error.message : String(error)}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const compiledDocs = Array.isArray(compiled) ? compiled : [compiled];
|
|
123
|
+
for (const doc of compiledDocs) {
|
|
124
|
+
const resource = this.normalizeResource(doc);
|
|
125
|
+
if (!resource) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!validateRuntimeResource(resource)) {
|
|
129
|
+
const kind = (resource as any).kind;
|
|
130
|
+
const name = (resource as any).metadata?.name;
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Resource validation failed for ${kind}.${name}: ${formatAjvErrors(validateRuntimeResource.errors)}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { kind, name } = resource.metadata;
|
|
137
|
+
resource.metadata.source = file.source;
|
|
138
|
+
resource.metadata.uri = `${file.uriBase}#${kind}.${name}`;
|
|
139
|
+
resource.metadata.generationDepth = 0;
|
|
140
|
+
|
|
141
|
+
resources.push(resource);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private normalizeResource(doc: any): RuntimeResource | null {
|
|
147
|
+
if (!doc || typeof doc !== "object" || typeof doc.kind !== "string") {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Already in correct format
|
|
152
|
+
if (doc.metadata && typeof doc.metadata === "object" && typeof doc.metadata.name === "string") {
|
|
153
|
+
return doc as RuntimeResource;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Validation handled by TypeBox + Ajv schemas.
|
|
160
|
+
|
|
161
|
+
private orderResourcesByKindDependencies(resources: RuntimeResource[]): RuntimeResource[] {
|
|
162
|
+
if (resources.length <= 1) {
|
|
163
|
+
return resources;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const indicesByName = new Map<string, number[]>();
|
|
167
|
+
for (let i = 0; i < resources.length; i += 1) {
|
|
168
|
+
const name = resources[i]?.metadata?.name;
|
|
169
|
+
if (!name) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const list = indicesByName.get(name);
|
|
173
|
+
if (list) {
|
|
174
|
+
list.push(i);
|
|
175
|
+
} else {
|
|
176
|
+
indicesByName.set(name, [i]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const edges = new Map<number, Set<number>>();
|
|
181
|
+
const indegree = new Map<number, number>();
|
|
182
|
+
for (let i = 0; i < resources.length; i += 1) {
|
|
183
|
+
indegree.set(i, 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (let i = 0; i < resources.length; i += 1) {
|
|
187
|
+
const kind = resources[i]?.kind;
|
|
188
|
+
if (!kind) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const definers = indicesByName.get(kind);
|
|
192
|
+
if (!definers) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
for (const definerIndex of definers) {
|
|
196
|
+
if (definerIndex === i) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
let set = edges.get(definerIndex);
|
|
200
|
+
if (!set) {
|
|
201
|
+
set = new Set();
|
|
202
|
+
edges.set(definerIndex, set);
|
|
203
|
+
}
|
|
204
|
+
if (!set.has(i)) {
|
|
205
|
+
set.add(i);
|
|
206
|
+
indegree.set(i, (indegree.get(i) || 0) + 1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const ready: number[] = [];
|
|
212
|
+
for (let i = 0; i < resources.length; i += 1) {
|
|
213
|
+
if ((indegree.get(i) || 0) === 0) {
|
|
214
|
+
ready.push(i);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
ready.sort((a, b) => a - b);
|
|
218
|
+
|
|
219
|
+
const ordered: RuntimeResource[] = [];
|
|
220
|
+
while (ready.length > 0) {
|
|
221
|
+
const index = ready.shift() as number;
|
|
222
|
+
ordered.push(resources[index]);
|
|
223
|
+
const next = edges.get(index);
|
|
224
|
+
if (!next) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
for (const dependent of next) {
|
|
228
|
+
const count = (indegree.get(dependent) || 0) - 1;
|
|
229
|
+
indegree.set(dependent, count);
|
|
230
|
+
if (count === 0) {
|
|
231
|
+
ready.push(dependent);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (ready.length > 1) {
|
|
235
|
+
ready.sort((a, b) => a - b);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (ordered.length !== resources.length) {
|
|
240
|
+
throw new Error("Resource dependency cycle detected");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return ordered;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as yaml from "js-yaml";
|
|
2
|
+
import type { ManifestAdapter, ManifestSourceData } from "./manifest-adapter.js";
|
|
3
|
+
|
|
4
|
+
export class HttpAdapter implements ManifestAdapter {
|
|
5
|
+
supports(pathOrUrl: string): boolean {
|
|
6
|
+
return pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async read(url: string): Promise<ManifestSourceData> {
|
|
10
|
+
return (await this.readAll(url))[0];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async readAll(url: string): Promise<ManifestSourceData[]> {
|
|
14
|
+
const fetchUrl = url.includes(".yaml") ? url : url + "/module.yaml";
|
|
15
|
+
const response = await fetch(fetchUrl);
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Failed to fetch manifest from ${fetchUrl}: ${response.status} ${response.statusText}`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
documents: yaml.loadAll(await response.text()),
|
|
24
|
+
source: fetchUrl,
|
|
25
|
+
baseDir: process.cwd(),
|
|
26
|
+
uriBase: url,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
resolveRelative(base: string, relative: string): string {
|
|
32
|
+
const baseWithSlash = base.endsWith("/") ? base : `${base}/`;
|
|
33
|
+
return new URL(relative, baseWithSlash).href;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as yaml from "js-yaml";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import type { ManifestAdapter, ManifestSourceData } from "./manifest-adapter.js";
|
|
5
|
+
|
|
6
|
+
export class LocalFileAdapter implements ManifestAdapter {
|
|
7
|
+
supports(pathOrUrl: string): boolean {
|
|
8
|
+
return (
|
|
9
|
+
pathOrUrl.startsWith("file://") ||
|
|
10
|
+
pathOrUrl.startsWith("/") ||
|
|
11
|
+
pathOrUrl.startsWith("./") ||
|
|
12
|
+
pathOrUrl.startsWith("../")
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async read(pathOrUrl: string): Promise<ManifestSourceData> {
|
|
17
|
+
const normalizedPath = pathOrUrl.startsWith("file://")
|
|
18
|
+
? pathOrUrl.replace("file://", "")
|
|
19
|
+
: pathOrUrl;
|
|
20
|
+
const stat = await fs.stat(normalizedPath);
|
|
21
|
+
const filePath = stat.isDirectory() ? path.join(normalizedPath, "module.yaml") : normalizedPath;
|
|
22
|
+
return this.readFile(filePath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async readAll(pathOrUrl: string): Promise<ManifestSourceData[]> {
|
|
26
|
+
const normalizedPath = pathOrUrl.startsWith("file://")
|
|
27
|
+
? pathOrUrl.replace("file://", "")
|
|
28
|
+
: pathOrUrl;
|
|
29
|
+
const stat = await fs.stat(normalizedPath);
|
|
30
|
+
if (stat.isDirectory()) {
|
|
31
|
+
const results: ManifestSourceData[] = [];
|
|
32
|
+
await this.collectYamlFiles(normalizedPath, results);
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
return [await this.readFile(normalizedPath)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
resolveRelative(base: string, relative: string): string {
|
|
39
|
+
const basePath = base.startsWith("file://") ? base.slice("file://".length) : base;
|
|
40
|
+
return `file://${path.resolve(basePath, relative)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async readFile(filePath: string): Promise<ManifestSourceData> {
|
|
44
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
documents: yaml.loadAll(content),
|
|
48
|
+
source: `file://${filePath}`,
|
|
49
|
+
baseDir: path.dirname(filePath),
|
|
50
|
+
uriBase: `file://localhost${filePath.replace(/\\/g, "/")}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async collectYamlFiles(dirPath: string, results: ManifestSourceData[]): Promise<void> {
|
|
55
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
await this.collectYamlFiles(fullPath, results);
|
|
60
|
+
} else if (entry.isFile() && this.isYamlFile(entry.name)) {
|
|
61
|
+
results.push(await this.readFile(fullPath));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private isYamlFile(filename: string): boolean {
|
|
67
|
+
return filename.endsWith(".yaml") || filename.endsWith(".yml");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ManifestSourceData {
|
|
2
|
+
/** Parsed YAML documents (result of yaml.loadAll) */
|
|
3
|
+
documents: any[];
|
|
4
|
+
/** Stored as metadata.source (file path or URL) */
|
|
5
|
+
source: string;
|
|
6
|
+
/** Base directory for resolving relative controller entrypoints */
|
|
7
|
+
baseDir: string;
|
|
8
|
+
/** URI prefix — full resource URI is `${uriBase}#${kind}.${name}` */
|
|
9
|
+
uriBase: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ManifestAdapter {
|
|
13
|
+
/** Returns true if this adapter can handle the given path/URL */
|
|
14
|
+
supports(pathOrUrl: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Read a single manifest entry point.
|
|
17
|
+
* - File path or URL → read that file/URL.
|
|
18
|
+
* - Directory path → find and read `module.yaml` within it.
|
|
19
|
+
*/
|
|
20
|
+
read(pathOrUrl: string): Promise<ManifestSourceData>;
|
|
21
|
+
/**
|
|
22
|
+
* Read all manifest files reachable from the given path/URL.
|
|
23
|
+
* Used for module imports.
|
|
24
|
+
* - Directory path → recursive walk, one entry per .yaml/.yml file found.
|
|
25
|
+
* - File path or URL → single-item array.
|
|
26
|
+
*/
|
|
27
|
+
readAll(pathOrUrl: string): Promise<ManifestSourceData[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a potentially relative path/URL against a base directory/URL.
|
|
30
|
+
* For absolute inputs the base is ignored.
|
|
31
|
+
*/
|
|
32
|
+
resolveRelative(base: string, relative: string): string;
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as yaml from "js-yaml";
|
|
2
|
+
import type { ManifestAdapter, ManifestSourceData } from "./manifest-adapter.js";
|
|
3
|
+
|
|
4
|
+
const REGISTRY_BASE = "https://registry.telo.run";
|
|
5
|
+
|
|
6
|
+
export class RegistryAdapter implements ManifestAdapter {
|
|
7
|
+
supports(pathOrUrl: string): boolean {
|
|
8
|
+
// Matches "owner/module@version" — has @ and /, but not an http/https URL or local path
|
|
9
|
+
return (
|
|
10
|
+
!pathOrUrl.startsWith("http://") &&
|
|
11
|
+
!pathOrUrl.startsWith("https://") &&
|
|
12
|
+
!pathOrUrl.startsWith("/") &&
|
|
13
|
+
!pathOrUrl.startsWith(".") &&
|
|
14
|
+
pathOrUrl.includes("@") &&
|
|
15
|
+
pathOrUrl.includes("/")
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async read(moduleRef: string): Promise<ManifestSourceData> {
|
|
20
|
+
return (await this.readAll(moduleRef))[0];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async readAll(moduleRef: string): Promise<ManifestSourceData[]> {
|
|
24
|
+
const url = this.toRegistryUrl(moduleRef);
|
|
25
|
+
const response = await fetch(url);
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Failed to fetch manifest ${moduleRef}: ${response.status} ${response.statusText}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
documents: yaml.loadAll(await response.text()),
|
|
34
|
+
source: url,
|
|
35
|
+
baseDir: process.cwd(),
|
|
36
|
+
uriBase: url.replace("/module.yaml", ""),
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
resolveRelative(base: string, relative: string): string {
|
|
42
|
+
const baseUrl = this.supports(base)
|
|
43
|
+
? this.toRegistryUrl(base).replace("/module.yaml", "")
|
|
44
|
+
: base;
|
|
45
|
+
const baseWithSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
46
|
+
return new URL(relative, baseWithSlash).href;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private toRegistryUrl(moduleRef: string): string {
|
|
50
|
+
const atIdx = moduleRef.lastIndexOf("@");
|
|
51
|
+
const path = moduleRef.slice(0, atIdx); // "example/module"
|
|
52
|
+
const version = moduleRef.slice(atIdx + 1); // "1.2.3" or "v1.2.3"
|
|
53
|
+
const versionSegment = version.startsWith("v") ? version.substring(1) : version;
|
|
54
|
+
return `${REGISTRY_BASE}/${path}/${versionSegment}/module.yaml`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import AjvModule, { ErrorObject } from "ajv";
|
|
3
|
+
import addFormats from "ajv-formats";
|
|
4
|
+
const Ajv = AjvModule.default ?? AjvModule;
|
|
5
|
+
|
|
6
|
+
export const RuntimeResourceSchema = Type.Object(
|
|
7
|
+
{
|
|
8
|
+
kind: Type.String(),
|
|
9
|
+
metadata: Type.Object({ name: Type.String() }, { additionalProperties: true }),
|
|
10
|
+
},
|
|
11
|
+
{ additionalProperties: true },
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const ResourceDefinitionSchema = Type.Object(
|
|
15
|
+
{
|
|
16
|
+
kind: Type.Literal("Kernel.Definition"),
|
|
17
|
+
metadata: Type.Object(
|
|
18
|
+
{
|
|
19
|
+
name: Type.String(),
|
|
20
|
+
module: Type.Optional(Type.String()),
|
|
21
|
+
},
|
|
22
|
+
{ additionalProperties: true },
|
|
23
|
+
),
|
|
24
|
+
schema: Type.Object({}, { additionalProperties: true }),
|
|
25
|
+
capabilities: Type.Array(Type.String(), { minItems: 1 }),
|
|
26
|
+
events: Type.Optional(Type.Array(Type.String())),
|
|
27
|
+
controllers: Type.Optional(Type.Array(Type.String())),
|
|
28
|
+
},
|
|
29
|
+
{ additionalProperties: true },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
33
|
+
addFormats.default(ajv);
|
|
34
|
+
|
|
35
|
+
export const validateRuntimeResource = ajv.compile(RuntimeResourceSchema);
|
|
36
|
+
export const validateResourceDefinition = ajv.compile(ResourceDefinitionSchema);
|
|
37
|
+
|
|
38
|
+
export function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {
|
|
39
|
+
if (!errors || errors.length === 0) {
|
|
40
|
+
return "Unknown schema error";
|
|
41
|
+
}
|
|
42
|
+
return errors
|
|
43
|
+
.map((err) => {
|
|
44
|
+
const path = err.instancePath && err.instancePath.length > 0 ? err.instancePath : "/";
|
|
45
|
+
const message = err.message || "is invalid";
|
|
46
|
+
return `${path} ${message}`;
|
|
47
|
+
})
|
|
48
|
+
.join("; ");
|
|
49
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ResourceManifest, RuntimeError, RuntimeResource } from "@telorun/sdk";
|
|
2
|
+
import { ResourceURI } from "./resource-uri.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Registry: Indexes resources by composite key of Kind and Name
|
|
6
|
+
* Maintains URI-based lookup for tracking resource origins and lineage
|
|
7
|
+
*/
|
|
8
|
+
export class ManifestRegistry {
|
|
9
|
+
private resources: Map<string, Map<string, ResourceManifest>> = new Map();
|
|
10
|
+
private kindInheritance: Map<string, string> = new Map(); // derivedKind -> parentKind
|
|
11
|
+
private uriIndex: Map<string, ResourceManifest> = new Map(); // URI -> Resource
|
|
12
|
+
private sourceIndex: Map<string, ResourceManifest[]> = new Map(); // source path -> Resources
|
|
13
|
+
private depthIndex: Map<number, ResourceManifest[]> = new Map(); // generation depth -> Resources
|
|
14
|
+
|
|
15
|
+
register(resource: RuntimeResource): void {
|
|
16
|
+
const { kind, metadata } = resource;
|
|
17
|
+
const { name } = metadata;
|
|
18
|
+
console.log("Registering resource:", kind, name);
|
|
19
|
+
if (!this.resources.has(kind)) {
|
|
20
|
+
this.resources.set(kind, new Map());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const kindMap = this.resources.get(kind)!;
|
|
24
|
+
|
|
25
|
+
if (kindMap.has(name)) {
|
|
26
|
+
throw new RuntimeError("ERR_DUPLICATE_RESOURCE", `Duplicate resource: ${kind}.${name}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
kindMap.set(name, resource);
|
|
30
|
+
|
|
31
|
+
// Index by URI if available
|
|
32
|
+
if (metadata.uri) {
|
|
33
|
+
this.uriIndex.set(metadata.uri, resource);
|
|
34
|
+
|
|
35
|
+
// Index by source file/path
|
|
36
|
+
try {
|
|
37
|
+
const uri = ResourceURI.parse(metadata.uri);
|
|
38
|
+
if (uri.isFileSource()) {
|
|
39
|
+
const sourcePath = uri.path;
|
|
40
|
+
if (!this.sourceIndex.has(sourcePath)) {
|
|
41
|
+
this.sourceIndex.set(sourcePath, []);
|
|
42
|
+
}
|
|
43
|
+
this.sourceIndex.get(sourcePath)!.push(resource);
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// URI parsing failed, skip indexing
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Index by generation depth
|
|
51
|
+
const depth = metadata.generationDepth ?? 0;
|
|
52
|
+
if (!this.depthIndex.has(depth)) {
|
|
53
|
+
this.depthIndex.set(depth, []);
|
|
54
|
+
}
|
|
55
|
+
this.depthIndex.get(depth)!.push(resource);
|
|
56
|
+
|
|
57
|
+
// Check if this is a Kernel.KindDefinition that creates a new kind
|
|
58
|
+
// if (kind === 'Kernel.KindDefinition') {
|
|
59
|
+
// const newKind = name;
|
|
60
|
+
// const parentKind = resource?.extends;
|
|
61
|
+
// if (parentKind) {
|
|
62
|
+
// this.kindInheritance.set(newKind, parentKind);
|
|
63
|
+
// }
|
|
64
|
+
// }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getParentKind(kind: string): string | undefined {
|
|
68
|
+
return this.kindInheritance.get(kind);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resolveKindChain(kind: string): string[] {
|
|
72
|
+
const chain: string[] = [kind];
|
|
73
|
+
let current = kind;
|
|
74
|
+
while (this.kindInheritance.has(current)) {
|
|
75
|
+
current = this.kindInheritance.get(current)!;
|
|
76
|
+
chain.push(current);
|
|
77
|
+
}
|
|
78
|
+
return chain;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get(kind: string, name: string): ResourceManifest | undefined {
|
|
82
|
+
return this.resources.get(kind)?.get(name);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getByKind(kind: string): ResourceManifest[] {
|
|
86
|
+
const kindMap = this.resources.get(kind);
|
|
87
|
+
return kindMap ? Array.from(kindMap.values()) : [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get resource by its URI
|
|
92
|
+
*/
|
|
93
|
+
getByUri(uri: string): ResourceManifest | undefined {
|
|
94
|
+
return this.uriIndex.get(uri);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get all resources from a specific source file
|
|
99
|
+
*/
|
|
100
|
+
getBySourceFile(sourceFilePath: string): ResourceManifest[] {
|
|
101
|
+
return this.sourceIndex.get(sourceFilePath) ?? [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get all resources at a specific generation depth
|
|
106
|
+
* 0 = directly from files, 1+ = template-generated
|
|
107
|
+
*/
|
|
108
|
+
getByGenerationDepth(depth: number): ResourceManifest[] {
|
|
109
|
+
return this.depthIndex.get(depth) ?? [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get all template-generated resources (depth > 0)
|
|
114
|
+
*/
|
|
115
|
+
getTemplateGenerated(): ResourceManifest[] {
|
|
116
|
+
const results: ResourceManifest[] = [];
|
|
117
|
+
for (const [depth, resources] of this.depthIndex) {
|
|
118
|
+
if (depth > 0) {
|
|
119
|
+
results.push(...resources);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get all directly-loaded resources (depth = 0)
|
|
127
|
+
*/
|
|
128
|
+
getDirectlyLoaded(): ResourceManifest[] {
|
|
129
|
+
return this.depthIndex.get(0) ?? [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getAll(): ResourceManifest[] {
|
|
133
|
+
return Array.from(this.resources.values())
|
|
134
|
+
.map((kindMap) => Array.from(kindMap.values()))
|
|
135
|
+
.flat();
|
|
136
|
+
}
|
|
137
|
+
}
|