@telorun/kernel 0.11.1 → 0.13.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/LICENSE +2 -2
- package/dist/application-env.d.ts +24 -0
- package/dist/application-env.d.ts.map +1 -0
- package/dist/application-env.js +156 -0
- package/dist/application-env.js.map +1 -0
- package/dist/base-definition.d.ts +14 -0
- package/dist/base-definition.d.ts.map +1 -0
- package/dist/base-definition.js +17 -0
- package/dist/base-definition.js.map +1 -0
- package/dist/capabilities/capabilities/component.yaml +4 -0
- package/dist/capabilities/capabilities/executable.yaml +8 -0
- package/dist/capabilities/capabilities/handler.yaml +4 -0
- package/dist/capabilities/capabilities/listener.yaml +4 -0
- package/dist/capabilities/capabilities/provider.yaml +4 -0
- package/dist/capabilities/capabilities/template.yaml +4 -0
- package/dist/capabilities/capabilities/type.yaml +4 -0
- package/dist/capabilities/component.d.ts +3 -0
- package/dist/capabilities/component.d.ts.map +1 -0
- package/dist/capabilities/component.js +4 -0
- package/dist/capabilities/component.js.map +1 -0
- package/dist/capabilities/component.yaml +3 -0
- package/dist/capabilities/executable.d.ts +3 -0
- package/dist/capabilities/executable.d.ts.map +1 -0
- package/dist/capabilities/executable.js +5 -0
- package/dist/capabilities/executable.js.map +1 -0
- package/dist/capabilities/executable.yaml +7 -0
- package/dist/capabilities/handler.d.ts +3 -0
- package/dist/capabilities/handler.d.ts.map +1 -0
- package/dist/capabilities/handler.js +4 -0
- package/dist/capabilities/handler.js.map +1 -0
- package/dist/capabilities/handler.yaml +3 -0
- package/dist/capabilities/invokable.d.ts +3 -0
- package/dist/capabilities/invokable.d.ts.map +1 -0
- package/dist/capabilities/invokable.js +5 -0
- package/dist/capabilities/invokable.js.map +1 -0
- package/dist/capabilities/listener.d.ts +3 -0
- package/dist/capabilities/listener.d.ts.map +1 -0
- package/dist/capabilities/listener.js +5 -0
- package/dist/capabilities/listener.js.map +1 -0
- package/dist/capabilities/listener.yaml +3 -0
- package/dist/capabilities/mount.d.ts +3 -0
- package/dist/capabilities/mount.d.ts.map +1 -0
- package/dist/capabilities/mount.js +5 -0
- package/dist/capabilities/mount.js.map +1 -0
- package/dist/capabilities/provider.d.ts +3 -0
- package/dist/capabilities/provider.d.ts.map +1 -0
- package/dist/capabilities/provider.js +8 -0
- package/dist/capabilities/provider.js.map +1 -0
- package/dist/capabilities/provider.yaml +3 -0
- package/dist/capabilities/runnable.d.ts +3 -0
- package/dist/capabilities/runnable.d.ts.map +1 -0
- package/dist/capabilities/runnable.js +5 -0
- package/dist/capabilities/runnable.js.map +1 -0
- package/dist/capabilities/service.d.ts +3 -0
- package/dist/capabilities/service.d.ts.map +1 -0
- package/dist/capabilities/service.js +5 -0
- package/dist/capabilities/service.js.map +1 -0
- package/dist/capabilities/template.d.ts +3 -0
- package/dist/capabilities/template.d.ts.map +1 -0
- package/dist/capabilities/template.js +5 -0
- package/dist/capabilities/template.js.map +1 -0
- package/dist/capabilities/template.yaml +3 -0
- package/dist/capabilities/type.d.ts +3 -0
- package/dist/capabilities/type.d.ts.map +1 -0
- package/dist/capabilities/type.js +5 -0
- package/dist/capabilities/type.js.map +1 -0
- package/dist/capabilities/type.yaml +3 -0
- package/dist/controller-loaders/npm-loader.d.ts +32 -8
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +120 -118
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controllers/capability/capability-controller.d.ts +32 -0
- package/dist/controllers/capability/capability-controller.d.ts.map +1 -0
- package/dist/controllers/capability/capability-controller.js +26 -0
- package/dist/controllers/capability/capability-controller.js.map +1 -0
- package/dist/controllers/module/import-controller.d.ts +3 -2
- package/dist/controllers/module/import-controller.d.ts.map +1 -1
- package/dist/controllers/module/import-controller.js +23 -25
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/controllers/module/module.json +48 -0
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts +1 -0
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +3 -0
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts +6 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.js +79 -13
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
- package/dist/internal-context.d.ts +25 -0
- package/dist/internal-context.d.ts.map +1 -0
- package/dist/internal-context.js +2 -0
- package/dist/internal-context.js.map +1 -0
- package/dist/kernel.d.ts +21 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +109 -5
- package/dist/kernel.js.map +1 -1
- package/dist/loader.d.ts +18 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +127 -0
- package/dist/loader.js.map +1 -0
- package/dist/manifest-adapters/http-adapter.d.ts +8 -0
- package/dist/manifest-adapters/http-adapter.d.ts.map +1 -0
- package/dist/manifest-adapters/http-adapter.js +31 -0
- package/dist/manifest-adapters/http-adapter.js.map +1 -0
- package/dist/manifest-adapters/local-file-adapter.d.ts +15 -0
- package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -0
- package/dist/manifest-adapters/local-file-adapter.js +95 -0
- package/dist/manifest-adapters/local-file-adapter.js.map +1 -0
- package/dist/manifest-adapters/manifest-adapter.d.ts +35 -0
- package/dist/manifest-adapters/manifest-adapter.d.ts.map +1 -0
- package/dist/manifest-adapters/manifest-adapter.js +2 -0
- package/dist/manifest-adapters/manifest-adapter.js.map +1 -0
- package/dist/manifest-adapters/registry-adapter.d.ts +9 -0
- package/dist/manifest-adapters/registry-adapter.d.ts.map +1 -0
- package/dist/manifest-adapters/registry-adapter.js +48 -0
- package/dist/manifest-adapters/registry-adapter.js.map +1 -0
- package/dist/manifest-schemas.d.ts +7 -23
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +18 -8
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/manifest-sources/analysis-stamp.d.ts +25 -0
- package/dist/manifest-sources/analysis-stamp.d.ts.map +1 -0
- package/dist/manifest-sources/analysis-stamp.js +151 -0
- package/dist/manifest-sources/analysis-stamp.js.map +1 -0
- 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/resource-context.d.ts +2 -0
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +28 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-valiator.d.ts +15 -0
- package/dist/schema-valiator.d.ts.map +1 -0
- package/dist/schema-valiator.js +127 -0
- package/dist/schema-valiator.js.map +1 -0
- package/dist/schema-validator.d.ts +28 -0
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +161 -1
- package/dist/schema-validator.js.map +1 -1
- package/dist/snapshot-serializer.d.ts +62 -0
- package/dist/snapshot-serializer.d.ts.map +1 -0
- package/dist/snapshot-serializer.js +164 -0
- package/dist/snapshot-serializer.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +9 -6
- package/src/application-env.ts +216 -0
- package/src/controller-loaders/npm-loader.ts +133 -118
- package/src/controllers/module/import-controller.ts +33 -36
- package/src/controllers/resource-definition/resource-definition-controller.ts +6 -0
- package/src/controllers/resource-definition/resource-template-controller.ts +110 -16
- package/src/internal-context.ts +25 -0
- package/src/kernel.ts +130 -5
- package/src/manifest-schemas.ts +31 -11
- package/src/manifest-sources/analysis-stamp.ts +169 -0
- package/src/resource-context.ts +34 -0
- package/src/schema-validator.ts +178 -2
- package/dist/generated/runtime-deps.json +0 -6
|
@@ -81,10 +81,13 @@ export interface NpmControllerLoaderOptions {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
* The npm-loader maintains a single install root per kernel process
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
* The npm-loader maintains a single install root per kernel process. For a
|
|
85
|
+
* local entry manifest (`file://` URL or bare path) the root lives at
|
|
86
|
+
* `<entry-manifest-dir>/.telo/npm/`; for an HTTP(S) entry URL it lives in a
|
|
87
|
+
* user-level cache keyed by `sha256(entryUrl)` (see `computeInstallRoot`).
|
|
88
|
+
* Every controller — registry tag or `local_path` — is installed via
|
|
89
|
+
* `npm install <spec>` into this root, then imported from
|
|
90
|
+
* `<root>/node_modules/<pkg>`. This collapses two parallel
|
|
88
91
|
* module realms (kernel-side @telorun/sdk vs. controller-side @telorun/sdk)
|
|
89
92
|
* into one: the kernel's own SDK is wired in as a `file:` dep, npm/pnpm
|
|
90
93
|
* symlink it, and Node's ESM resolver follows the symlink to the same
|
|
@@ -150,7 +153,13 @@ export class NpmControllerLoader {
|
|
|
150
153
|
|
|
151
154
|
const installRoot = await this.ensureInstallRoot();
|
|
152
155
|
const resolved = await resolveInstallSpec(parsed, packageName, baseUri);
|
|
153
|
-
const source = await this.installPackage(
|
|
156
|
+
const source = await this.installPackage(
|
|
157
|
+
installRoot,
|
|
158
|
+
packageName,
|
|
159
|
+
resolved.spec,
|
|
160
|
+
resolved.kind,
|
|
161
|
+
parsed.version ?? null,
|
|
162
|
+
);
|
|
154
163
|
const instance = await loadFromInstall(installRoot, packageName, parsed.subpath ?? null, purl);
|
|
155
164
|
return { instance, source };
|
|
156
165
|
}
|
|
@@ -184,36 +193,24 @@ export class NpmControllerLoader {
|
|
|
184
193
|
);
|
|
185
194
|
}
|
|
186
195
|
const entryUrlStr = this.entryUrl;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
const entryDir = path.dirname(path.resolve(entryPath));
|
|
196
|
-
const installRoot = path.join(entryDir, ".telo", "npm");
|
|
197
|
-
|
|
198
|
-
// Build the install-root package.json: kernel-runtime deps as `file:` refs,
|
|
199
|
-
// `overrides` + `pnpm.overrides` pinning every name to `$<name>`. This is
|
|
200
|
-
// the realm-collapse mechanism — npm's `file:` symlinks the kernel's own
|
|
201
|
-
// package locations, the resolver follows symlinks to the realpath, and
|
|
202
|
-
// every controller transitively resolving these names lands on the
|
|
203
|
-
// identical module instance the kernel itself is using.
|
|
204
|
-
const runtimeDeps = await loadRuntimeDeps();
|
|
196
|
+
const installRoot = computeInstallRoot(entryUrlStr);
|
|
197
|
+
|
|
198
|
+
// Build the install-root package.json: kernel-runtime deps as `file:` refs
|
|
199
|
+
// pointing at the kernel-side realpath. Modules declare these names as
|
|
200
|
+
// `peerDependencies`, so npm/pnpm resolve each controller's `import` to the
|
|
201
|
+
// single copy provided here — the realm-collapse mechanism that gives
|
|
202
|
+
// class-identity-sensitive types (today: `Stream`) one constructor across
|
|
203
|
+
// the kernel/controller boundary.
|
|
205
204
|
const dependencies: Record<string, string> = {};
|
|
206
|
-
const
|
|
207
|
-
for (const name of runtimeDeps) {
|
|
205
|
+
for (const name of REALM_COLLAPSE_NAMES) {
|
|
208
206
|
const resolvedPkgRoot = await resolveKernelPackageRoot(name);
|
|
209
207
|
if (!resolvedPkgRoot) {
|
|
210
208
|
// A kernel runtime dep that can't be resolved at boot is unusual but
|
|
211
|
-
// not fatal — the realm-collapse story degrades to "rely on
|
|
212
|
-
//
|
|
209
|
+
// not fatal — the realm-collapse story degrades to "rely on whatever
|
|
210
|
+
// the package manager picks" for that name. Don't crash the loader.
|
|
213
211
|
continue;
|
|
214
212
|
}
|
|
215
213
|
dependencies[name] = `file:${resolvedPkgRoot}`;
|
|
216
|
-
overrides[name] = `$${name}`;
|
|
217
214
|
}
|
|
218
215
|
|
|
219
216
|
const packageJson = {
|
|
@@ -221,8 +218,6 @@ export class NpmControllerLoader {
|
|
|
221
218
|
private: true,
|
|
222
219
|
version: "0.0.0",
|
|
223
220
|
dependencies,
|
|
224
|
-
overrides,
|
|
225
|
-
pnpm: { overrides },
|
|
226
221
|
};
|
|
227
222
|
const packageJsonPath = path.join(installRoot, "package.json");
|
|
228
223
|
const stateFile = path.join(installRoot, ".telo-state.json");
|
|
@@ -299,6 +294,7 @@ export class NpmControllerLoader {
|
|
|
299
294
|
packageName: string,
|
|
300
295
|
spec: string,
|
|
301
296
|
kind: SpecKind,
|
|
297
|
+
requestedVersion: string | null,
|
|
302
298
|
): Promise<NpmResolveSource> {
|
|
303
299
|
const cacheKey = `${packageName}@${spec}`;
|
|
304
300
|
if (this.installedSpecs.has(cacheKey)) return "cache";
|
|
@@ -309,22 +305,37 @@ export class NpmControllerLoader {
|
|
|
309
305
|
return "cache";
|
|
310
306
|
}
|
|
311
307
|
|
|
312
|
-
// Lock-free fast path: consult the in-process snapshot of the install
|
|
313
|
-
// root's `dependencies` map (seeded by `materializeInstallRoot`). If the
|
|
314
|
-
// spec matches what's already wired in and the package directory exists,
|
|
315
|
-
// there's nothing to do — no lock, no fork, no per-load file read. This
|
|
316
|
-
// is the dominant case for warm test suites: every Kernel touches the
|
|
317
|
-
// same controllers and a fresh-on-disk read per (Kernel × controller)
|
|
318
|
-
// dominates wall time even when the actual installs are already done.
|
|
319
|
-
//
|
|
320
|
-
// Normalize specs before comparing because npm rewrites absolute `file:`
|
|
321
|
-
// deps to relative paths inside the install root's `package.json`. The
|
|
322
|
-
// loader passes absolute paths (resolved against the declaring library's
|
|
323
|
-
// baseUri); the on-disk record is `file:../../foo`. Without normalization
|
|
324
|
-
// every fast-path read is a string-mismatch and the loader would fall
|
|
325
|
-
// through to a real `npm install` per controller per Kernel — turning a
|
|
326
|
-
// 1ms hit into a 150–200ms one.
|
|
327
308
|
const targetPath = path.join(installRoot, "node_modules", ...packageName.split("/"));
|
|
309
|
+
|
|
310
|
+
// Registry fast path: compare against the installed package's own
|
|
311
|
+
// `package.json` version field. `rootDeps[packageName]` can't be used
|
|
312
|
+
// here because npm rewrites registry specs on `--save` — we pass
|
|
313
|
+
// `@scope/pkg@0.3.4`, npm writes `^0.3.4` — so a string comparison
|
|
314
|
+
// never matches and every fresh `NpmControllerLoader` (one per
|
|
315
|
+
// `Telo.Definition.init`) would fall through to a no-op but ~200ms
|
|
316
|
+
// `npm install`. Reading the installed package.json sidesteps that
|
|
317
|
+
// entirely: if the requested PURL version equals what's on disk, we
|
|
318
|
+
// already have the right thing.
|
|
319
|
+
//
|
|
320
|
+
// Ranges (`^1.0.0`, `~2.3.0`) fall through to a real `npm install` —
|
|
321
|
+
// they're rare in PURLs (which typically pin) and a proper range check
|
|
322
|
+
// would need a semver dep here.
|
|
323
|
+
if (kind === "registry") {
|
|
324
|
+
const installedVersion = await readInstalledVersion(targetPath);
|
|
325
|
+
if (
|
|
326
|
+
installedVersion !== null &&
|
|
327
|
+
(requestedVersion === null || requestedVersion === installedVersion)
|
|
328
|
+
) {
|
|
329
|
+
this.installedSpecs.add(cacheKey);
|
|
330
|
+
return "cache";
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Local (`file:`) spec fast path: consult the in-process snapshot of the
|
|
335
|
+
// install root's `dependencies` map (seeded by `materializeInstallRoot`).
|
|
336
|
+
// Normalize because npm rewrites absolute `file:` deps to relative paths
|
|
337
|
+
// inside the install root's `package.json` — the loader passes the
|
|
338
|
+
// absolute path, the on-disk record is `file:../../foo`.
|
|
328
339
|
const cachedSpec = this.rootDeps[packageName];
|
|
329
340
|
if (
|
|
330
341
|
cachedSpec !== undefined &&
|
|
@@ -383,54 +394,18 @@ export class NpmControllerLoader {
|
|
|
383
394
|
}
|
|
384
395
|
|
|
385
396
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
397
|
+
* Names of packages whose realpath must be shared between the kernel and every
|
|
398
|
+
* loaded controller. Each name here becomes a `file:` dep in the install-root
|
|
399
|
+
* `package.json`, pinned at the kernel's own resolution; controllers declare
|
|
400
|
+
* these names as `peerDependencies` so npm/pnpm resolves them to that single
|
|
401
|
+
* copy instead of nesting their own.
|
|
402
|
+
*
|
|
403
|
+
* Add a name here if you ship another shared runtime symbol whose `instanceof`
|
|
404
|
+
* or constructor identity matters across module boundaries. Today the only
|
|
405
|
+
* such name is `@telorun/sdk` (carries the `Stream` class registered with
|
|
406
|
+
* `@marcbachmann/cel-js`).
|
|
393
407
|
*/
|
|
394
|
-
|
|
395
|
-
const here = fileURLToPath(import.meta.url);
|
|
396
|
-
// Walk up from this file's location to the kernel-package root (the dir
|
|
397
|
-
// that contains package.json). In dev: `kernel/nodejs/`. In published:
|
|
398
|
-
// the installed package root inside `node_modules/@telorun/kernel/`.
|
|
399
|
-
// Walk-until-root rather than a fixed depth — the directory tree depth
|
|
400
|
-
// depends on whether we're in a workspace or installed tree.
|
|
401
|
-
const pkgDir = await walkUpToPackageRoot(path.dirname(here));
|
|
402
|
-
if (!pkgDir) return [];
|
|
403
|
-
|
|
404
|
-
const generated = path.join(pkgDir, "dist", "generated", "runtime-deps.json");
|
|
405
|
-
if (!(await pathExists(generated))) return [];
|
|
406
|
-
// The file is generated by `scripts/generate-runtime-deps.mjs`; if it
|
|
407
|
-
// exists but is malformed, that's a kernel-build bug. Don't swallow —
|
|
408
|
-
// surface so the cause is debuggable. Realm collapse is the whole point
|
|
409
|
-
// of this code path; quietly degrading to "no realm collapse" would
|
|
410
|
-
// silently re-introduce the very bug the file fixes.
|
|
411
|
-
const data = JSON.parse(await fs.readFile(generated, "utf8"));
|
|
412
|
-
if (!Array.isArray(data?.names)) {
|
|
413
|
-
throw new Error(
|
|
414
|
-
`[telo] ${generated} is malformed: expected { names: string[] } at top level. ` +
|
|
415
|
-
`Re-run \`node scripts/generate-runtime-deps.mjs <pkg-dir>\` to regenerate.`,
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
return data.names as string[];
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Walk up from `from` until the directory contains a `package.json`.
|
|
423
|
-
* Returns null at filesystem root if none found.
|
|
424
|
-
*/
|
|
425
|
-
async function walkUpToPackageRoot(from: string): Promise<string | null> {
|
|
426
|
-
let dir = from;
|
|
427
|
-
while (true) {
|
|
428
|
-
if (await pathExists(path.join(dir, "package.json"))) return dir;
|
|
429
|
-
const parent = path.dirname(dir);
|
|
430
|
-
if (parent === dir) return null;
|
|
431
|
-
dir = parent;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
408
|
+
const REALM_COLLAPSE_NAMES: ReadonlyArray<string> = ["@telorun/sdk"];
|
|
434
409
|
|
|
435
410
|
/**
|
|
436
411
|
* Resolve a kernel-runtime dep name to the realpath of its package directory.
|
|
@@ -618,6 +593,24 @@ async function readJsonField(filePath: string, field: string): Promise<unknown>
|
|
|
618
593
|
}
|
|
619
594
|
}
|
|
620
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Read the `version` field from `<packageRoot>/package.json`, or null if the
|
|
598
|
+
* file is missing/unreadable/malformed or carries a non-string version. Used
|
|
599
|
+
* by the registry fast path to decide whether an existing install already
|
|
600
|
+
* satisfies the requested PURL version — the install-root's own
|
|
601
|
+
* `dependencies` map can't answer that because npm rewrites registry specs
|
|
602
|
+
* on `--save`.
|
|
603
|
+
*/
|
|
604
|
+
async function readInstalledVersion(packageRoot: string): Promise<string | null> {
|
|
605
|
+
try {
|
|
606
|
+
const text = await fs.readFile(path.join(packageRoot, "package.json"), "utf8");
|
|
607
|
+
const pkg = JSON.parse(text);
|
|
608
|
+
return typeof pkg?.version === "string" ? pkg.version : null;
|
|
609
|
+
} catch {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
621
614
|
/**
|
|
622
615
|
* Read the install root's `dependencies[<packageName>]` value, or undefined
|
|
623
616
|
* if package.json is missing/unreadable or the dep isn't listed. Used by the
|
|
@@ -635,35 +628,56 @@ async function readDepSpec(installRoot: string, packageName: string): Promise<st
|
|
|
635
628
|
}
|
|
636
629
|
|
|
637
630
|
/**
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
* `
|
|
631
|
+
* Decide where the per-kernel install root lives for a given entry URL.
|
|
632
|
+
*
|
|
633
|
+
* - `file://` URL or bare filesystem path: anchored next to the manifest at
|
|
634
|
+
* `<entry-dir>/.telo/npm/`. Same as before — keeps the "install lives with
|
|
635
|
+
* the project" story for local development and Docker builds where the
|
|
636
|
+
* tree is `COPY`-d into the image.
|
|
637
|
+
* - `http(s)://` URL: there is no on-disk anchor next to the manifest, so
|
|
638
|
+
* the install root lives in a user-level cache keyed by the SHA-256 of
|
|
639
|
+
* the entry URL: `<cacheDir>/<hash>/npm/`. Repeat runs of the same URL
|
|
640
|
+
* hit the same cache; distinct URLs get isolated trees so two unrelated
|
|
641
|
+
* remote apps don't share `node_modules` (different controller versions,
|
|
642
|
+
* different realm-collapse pins).
|
|
644
643
|
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
644
|
+
* Cache location, in priority order:
|
|
645
|
+
* 1. `$TELO_NPM_CACHE_DIR` (explicit override — tests use this to avoid
|
|
646
|
+
* polluting the developer's `~/.cache`).
|
|
647
|
+
* 2. `$XDG_CACHE_HOME/telo/remote` (standard XDG path on Linux).
|
|
648
|
+
* 3. `<os.homedir()>/.cache/telo/remote` (POSIX fallback / macOS).
|
|
649
|
+
*
|
|
650
|
+
* Unrecognised schemes (anything that isn't `file://`, `http://`, or
|
|
651
|
+
* `https://`) still throw `ControllerEnvMissingError` so the dispatcher can
|
|
652
|
+
* advance to a non-npm candidate.
|
|
648
653
|
*/
|
|
649
|
-
function
|
|
650
|
-
if (entryUrl.startsWith("file://"))
|
|
651
|
-
|
|
652
|
-
|
|
654
|
+
function computeInstallRoot(entryUrl: string): string {
|
|
655
|
+
if (entryUrl.startsWith("file://")) {
|
|
656
|
+
const entryPath = fileURLToPath(entryUrl);
|
|
657
|
+
return path.join(path.dirname(path.resolve(entryPath)), ".telo", "npm");
|
|
658
|
+
}
|
|
653
659
|
const schemeMatch = entryUrl.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
|
|
654
|
-
if (schemeMatch) {
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
660
|
+
if (!schemeMatch) {
|
|
661
|
+
// Bare filesystem path: same anchor as `file://`.
|
|
662
|
+
return path.join(path.dirname(path.resolve(entryUrl)), ".telo", "npm");
|
|
663
|
+
}
|
|
664
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
665
|
+
if (scheme === "http" || scheme === "https") {
|
|
666
|
+
const cacheBase =
|
|
667
|
+
process.env.TELO_NPM_CACHE_DIR ||
|
|
668
|
+
(process.env.XDG_CACHE_HOME
|
|
669
|
+
? path.join(process.env.XDG_CACHE_HOME, "telo", "remote")
|
|
670
|
+
: path.join(os.homedir(), ".cache", "telo", "remote"));
|
|
671
|
+
return path.join(cacheBase, sha256(entryUrl), "npm");
|
|
665
672
|
}
|
|
666
|
-
|
|
673
|
+
// Env-missing rather than a hard error: the dispatcher should advance to
|
|
674
|
+
// the next candidate when a non-npm scheme is paired with a `pkg:cargo`
|
|
675
|
+
// (or other) fallback.
|
|
676
|
+
throw new ControllerEnvMissingError(
|
|
677
|
+
`[telo] entry URL scheme '${schemeMatch[1]}' is not supported by the npm controller ` +
|
|
678
|
+
`loader. Supported schemes: file://, http://, https://, or a bare filesystem path. ` +
|
|
679
|
+
`(entryUrl: ${entryUrl})`,
|
|
680
|
+
);
|
|
667
681
|
}
|
|
668
682
|
|
|
669
683
|
/**
|
|
@@ -914,7 +928,8 @@ export const __testing__ = {
|
|
|
914
928
|
resolvePackageExportTarget,
|
|
915
929
|
resolveExportTargetValue,
|
|
916
930
|
tryResolveFile,
|
|
917
|
-
|
|
931
|
+
computeInstallRoot,
|
|
918
932
|
EXPORTS_MAX_DEPTH,
|
|
919
933
|
DEFAULT_RESOLVER_CONDITIONS,
|
|
934
|
+
REALM_COLLAPSE_NAMES,
|
|
920
935
|
};
|
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
import { DiagnosticSeverity, StaticAnalyzer } from "@telorun/analyzer";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ResourceInstance } from "@telorun/sdk";
|
|
3
3
|
import { RuntimeError } from "@telorun/sdk";
|
|
4
|
+
import type { BuiltinControllerContext } from "../../internal-context.js";
|
|
4
5
|
import { ModuleContext } from "../../module-context.js";
|
|
5
6
|
import { isDefaultPolicy, normalizeRuntime } from "../../runtime-registry.js";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
>
|
|
11
|
-
|
|
12
|
-
// Only resolve relative/absolute-path sources against the importer's URL. Registry refs
|
|
13
|
-
// (std/foo@1.2.3) and absolute URLs (https://, file://) must pass through unchanged so the
|
|
14
|
-
// loader's source chain can dispatch them — otherwise `new URL("std/foo@1", "file:///srv/telo.yaml")`
|
|
15
|
-
// turns a registry ref into a bogus file path and LocalFileSource ENOENTs on it.
|
|
16
|
-
function resolveImportSource(source: string, baseSource: string): string {
|
|
17
|
-
if (source.startsWith(".") || source.startsWith("/")) {
|
|
18
|
-
return new URL(source, baseSource).toString();
|
|
19
|
-
}
|
|
20
|
-
return source;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function create(resource: any, ctx: ResourceContext): Promise<ResourceInstance> {
|
|
8
|
+
export async function create(
|
|
9
|
+
resource: any,
|
|
10
|
+
ctx: BuiltinControllerContext,
|
|
11
|
+
): Promise<ResourceInstance> {
|
|
24
12
|
const alias = resource.metadata.name as string;
|
|
25
13
|
|
|
26
14
|
const moduleSource: string = resource.module ?? resource.source;
|
|
@@ -36,27 +24,36 @@ export async function create(resource: any, ctx: ResourceContext): Promise<Resou
|
|
|
36
24
|
// Validate the imported module and all its transitive imports before loading for runtime.
|
|
37
25
|
// loadManifests() follows Telo.Import chains so definitions from sub-imports are present,
|
|
38
26
|
// preventing false UNDEFINED_KIND errors for kinds that come from the module's own imports.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
27
|
+
//
|
|
28
|
+
// Route URL resolution through the kernel/loader's own helper rather than
|
|
29
|
+
// a hand-rolled `new URL(...).toString()`. For LocalFileSource the
|
|
30
|
+
// outputs match; for any custom `ManifestSource` with a non-trivial
|
|
31
|
+
// `resolveRelative`, only this path produces the canonical URL the
|
|
32
|
+
// loader keyed its caches under — without which fast paths like
|
|
33
|
+
// `isImportValidatedAtLoad` silently miss.
|
|
34
|
+
const resolvedUrl = ctx.resolveImportUrl(base, moduleSource);
|
|
35
|
+
|
|
36
|
+
// Fast path: when the kernel's load-time `analyzeErrors` already covered
|
|
37
|
+
// this import's subtree (the common case — every Telo.Import declared in
|
|
38
|
+
// the entry graph is walked by `loadGraph` and validated by
|
|
39
|
+
// `kernel.load`), skip the redundant per-import StaticAnalyzer pass.
|
|
40
|
+
// Falls through to the full analysis for URLs that arrived
|
|
41
|
+
// programmatically after `load()` (e.g. dynamically constructed imports
|
|
42
|
+
// in tests). Two Telo.Imports with the same source but distinct
|
|
43
|
+
// metadata.name would each re-run analysis here — a memoisation hook
|
|
44
|
+
// can be reintroduced if that turns into a measurable cost.
|
|
45
|
+
if (!ctx.isImportValidatedAtLoad(resolvedUrl)) {
|
|
46
|
+
const analysisManifests = await ctx.loadManifests(resolvedUrl);
|
|
48
47
|
const diagnostics = new StaticAnalyzer().analyze(analysisManifests);
|
|
49
|
-
errors = diagnostics
|
|
48
|
+
const errors = diagnostics
|
|
50
49
|
.filter((d) => d.severity === DiagnosticSeverity.Error)
|
|
51
50
|
.map((d) => d.message);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
errors.join("\n"),
|
|
59
|
-
);
|
|
51
|
+
if (errors.length > 0) {
|
|
52
|
+
throw new RuntimeError(
|
|
53
|
+
"ERR_MANIFEST_VALIDATION_FAILED",
|
|
54
|
+
errors.join("\n"),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
// Load target module manifests for runtime. Inject variables/secrets as compile context so
|
|
@@ -18,6 +18,7 @@ type ResourceDefinitionResource = RuntimeResource & {
|
|
|
18
18
|
schema: Record<string, any>;
|
|
19
19
|
capability?: string;
|
|
20
20
|
controllers?: Array<string>;
|
|
21
|
+
provide?: unknown;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -31,6 +32,11 @@ class ResourceDefinition implements ResourceInstance {
|
|
|
31
32
|
|
|
32
33
|
async init(ctx: ResourceContext) {
|
|
33
34
|
if (!this.resource.controllers?.length) {
|
|
35
|
+
if (this.resource.capability === "Telo.Provider" && this.resource.provide == null) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Telo.Definition '${this.resource.metadata.name}': 'capability: Telo.Provider' requires either 'controllers:' (TS-backed) or 'provide:' (template-backed).`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
34
40
|
const controllerInstance = createTemplateController(this.resource as any);
|
|
35
41
|
ctx.registerDefinition(this.resource);
|
|
36
42
|
await ctx.registerController(
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import type { ControllerInstance, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
2
|
import { isCompiledValue } from "@telorun/sdk";
|
|
3
3
|
|
|
4
|
+
/** Reports the resources: entries available to dispatch against, by expanded
|
|
5
|
+
* name and kind. Used in error messages to guide the developer back to the
|
|
6
|
+
* template's `resources:` array when a dispatch target doesn't match. */
|
|
7
|
+
function describeAvailableTargets(
|
|
8
|
+
ctx: ResourceContext,
|
|
9
|
+
resources: any[] | undefined,
|
|
10
|
+
self: Record<string, unknown>,
|
|
11
|
+
): string {
|
|
12
|
+
if (!resources || resources.length === 0) return "<none>";
|
|
13
|
+
return resources
|
|
14
|
+
.map((r) => {
|
|
15
|
+
const expanded = ctx.moduleContext.expandWith(r?.metadata?.name ?? "", { self }) as string;
|
|
16
|
+
const kind = typeof r?.kind === "string" ? r.kind : "<unknown-kind>";
|
|
17
|
+
return `'${expanded || "<unnamed>"}' (${kind})`;
|
|
18
|
+
})
|
|
19
|
+
.join(", ");
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
export function createTemplateController(definition: {
|
|
5
23
|
schema: Record<string, any>;
|
|
6
24
|
resources?: any[];
|
|
7
|
-
invoke?: string | { kind?: string; name: string
|
|
25
|
+
invoke?: string | { kind?: string; name: string };
|
|
26
|
+
inputs?: Record<string, any>;
|
|
8
27
|
run?: string;
|
|
28
|
+
provide?: { kind: string; name: string };
|
|
29
|
+
result?: Record<string, any>;
|
|
9
30
|
}): ControllerInstance {
|
|
10
31
|
return {
|
|
11
32
|
schema: definition.schema ?? { type: "object", additionalProperties: true },
|
|
@@ -13,13 +34,16 @@ export function createTemplateController(definition: {
|
|
|
13
34
|
create: async (resource: any, ctx: ResourceContext): Promise<ResourceInstance> => {
|
|
14
35
|
const self = { ...resource, name: resource.metadata.name };
|
|
15
36
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
37
|
+
// `invoke` describes the dispatch target: a string name template (legacy
|
|
38
|
+
// shorthand) or an object `{ kind?, name }` for explicit kind-typed
|
|
39
|
+
// dispatch. `inputs:` lives as a sibling on the definition (same shape
|
|
40
|
+
// as Run.Sequence steps) — the values passed to the dispatch target's
|
|
41
|
+
// invoke() after CEL expansion.
|
|
18
42
|
const objectInvoke =
|
|
19
43
|
definition.invoke !== null &&
|
|
20
44
|
typeof definition.invoke === "object" &&
|
|
21
45
|
!isCompiledValue(definition.invoke)
|
|
22
|
-
? (definition.invoke as { kind?: string; name: string
|
|
46
|
+
? (definition.invoke as { kind?: string; name: string })
|
|
23
47
|
: null;
|
|
24
48
|
const invokeNameTemplate = objectInvoke ? objectInvoke.name : (definition.invoke ?? null);
|
|
25
49
|
const invokeTarget = invokeNameTemplate
|
|
@@ -28,6 +52,9 @@ export function createTemplateController(definition: {
|
|
|
28
52
|
const runTarget = definition.run
|
|
29
53
|
? (ctx.moduleContext.expandWith(definition.run, { self }) as string)
|
|
30
54
|
: null;
|
|
55
|
+
const provideTarget = definition.provide?.name
|
|
56
|
+
? (ctx.moduleContext.expandWith(definition.provide.name, { self }) as string)
|
|
57
|
+
: null;
|
|
31
58
|
|
|
32
59
|
const persistentManifests: any[] = [];
|
|
33
60
|
let ephemeralTemplate: any = null;
|
|
@@ -36,7 +63,10 @@ export function createTemplateController(definition: {
|
|
|
36
63
|
const expandedName = ctx.moduleContext.expandWith(template.metadata?.name ?? "", {
|
|
37
64
|
self,
|
|
38
65
|
}) as string;
|
|
39
|
-
const isTarget =
|
|
66
|
+
const isTarget =
|
|
67
|
+
expandedName === invokeTarget ||
|
|
68
|
+
expandedName === runTarget ||
|
|
69
|
+
expandedName === provideTarget;
|
|
40
70
|
if (isTarget) {
|
|
41
71
|
ephemeralTemplate = template;
|
|
42
72
|
} else {
|
|
@@ -80,7 +110,8 @@ export function createTemplateController(definition: {
|
|
|
80
110
|
invoke: async (inputs: any) => {
|
|
81
111
|
if (!ephemeralTemplate) {
|
|
82
112
|
throw new Error(
|
|
83
|
-
`Template '${resource.metadata.name}':
|
|
113
|
+
`Template '${resource.metadata.name}': 'invoke:' targets '${invokeTarget}' ` +
|
|
114
|
+
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
84
115
|
);
|
|
85
116
|
}
|
|
86
117
|
const extraContext = { self, inputs };
|
|
@@ -91,18 +122,32 @@ export function createTemplateController(definition: {
|
|
|
91
122
|
return withEphemeral(expanded, async (name) => {
|
|
92
123
|
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
93
124
|
if (!entry?.instance?.invoke) {
|
|
94
|
-
|
|
125
|
+
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
126
|
+
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
127
|
+
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Template '${resource.metadata.name}': 'invoke:' target '${targetKind}/${invokeTarget}' ` +
|
|
130
|
+
`has capability '${actualCap}', not Telo.Invocable. Update 'invoke:' to a Telo.Invocable kind, or change the target's kind in 'resources:'.`,
|
|
131
|
+
);
|
|
95
132
|
}
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
133
|
+
// Top-level `inputs:` (sibling of `invoke:`) carries the values passed
|
|
134
|
+
// to the dispatch target's invoke(). When absent, fall back to the
|
|
135
|
+
// expanded resource entry's own `inputs` field (legacy string-form
|
|
136
|
+
// shape where the inputs live on the resource declaration), then
|
|
137
|
+
// finally to the caller's `inputs` arg.
|
|
138
|
+
const invokeInputs = definition.inputs != null
|
|
100
139
|
? ctx.moduleContext.expandWith(
|
|
101
|
-
ctx.moduleContext.expandWith(
|
|
140
|
+
ctx.moduleContext.expandWith(definition.inputs, extraContext),
|
|
102
141
|
extraContext,
|
|
103
142
|
)
|
|
104
143
|
: expanded.inputs ?? inputs;
|
|
105
|
-
|
|
144
|
+
const raw = await entry.instance.invoke(invokeInputs);
|
|
145
|
+
if (definition.result == null) return raw;
|
|
146
|
+
const resultContext = { self, result: raw };
|
|
147
|
+
return ctx.moduleContext.expandWith(
|
|
148
|
+
ctx.moduleContext.expandWith(definition.result, resultContext),
|
|
149
|
+
resultContext,
|
|
150
|
+
);
|
|
106
151
|
});
|
|
107
152
|
},
|
|
108
153
|
}),
|
|
@@ -111,24 +156,73 @@ export function createTemplateController(definition: {
|
|
|
111
156
|
run: async () => {
|
|
112
157
|
if (!ephemeralTemplate) {
|
|
113
158
|
throw new Error(
|
|
114
|
-
`Template '${resource.metadata.name}':
|
|
159
|
+
`Template '${resource.metadata.name}': 'run:' targets '${runTarget}' ` +
|
|
160
|
+
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
115
161
|
);
|
|
116
162
|
}
|
|
117
163
|
const extraContext = { self };
|
|
118
164
|
const expanded = ctx.moduleContext.expandWith(
|
|
119
165
|
ctx.moduleContext.expandWith(ephemeralTemplate, extraContext),
|
|
120
166
|
extraContext,
|
|
121
|
-
);
|
|
167
|
+
) as any;
|
|
122
168
|
return withEphemeral(expanded, async (name) => {
|
|
123
169
|
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
124
170
|
if (!entry?.instance?.run) {
|
|
125
|
-
|
|
171
|
+
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
172
|
+
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
173
|
+
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Template '${resource.metadata.name}': 'run:' target '${targetKind}/${runTarget}' ` +
|
|
176
|
+
`has capability '${actualCap}', not Telo.Runnable. Update 'run:' to a Telo.Runnable kind, or change the target's kind in 'resources:'.`,
|
|
177
|
+
);
|
|
126
178
|
}
|
|
127
179
|
return entry.instance.run();
|
|
128
180
|
});
|
|
129
181
|
},
|
|
130
182
|
}),
|
|
131
183
|
|
|
184
|
+
...(provideTarget && {
|
|
185
|
+
provide: async () => {
|
|
186
|
+
if (!ephemeralTemplate) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Template '${resource.metadata.name}': 'provide:' targets '${provideTarget}' ` +
|
|
189
|
+
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const extraContext = { self };
|
|
193
|
+
const expanded = ctx.moduleContext.expandWith(
|
|
194
|
+
ctx.moduleContext.expandWith(ephemeralTemplate, extraContext),
|
|
195
|
+
extraContext,
|
|
196
|
+
) as any;
|
|
197
|
+
return withEphemeral(expanded, async (name) => {
|
|
198
|
+
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
199
|
+
if (!entry?.instance?.invoke) {
|
|
200
|
+
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
201
|
+
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
202
|
+
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Template '${resource.metadata.name}': 'provide:' target '${targetKind}/${provideTarget}' ` +
|
|
205
|
+
`has capability '${actualCap}', not Telo.Invocable. Update 'provide:' to a Telo.Invocable kind, or change the target's kind in 'resources:'.`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const provideInputs: any =
|
|
209
|
+
definition.inputs != null
|
|
210
|
+
? ctx.moduleContext.expandWith(
|
|
211
|
+
ctx.moduleContext.expandWith(definition.inputs, extraContext),
|
|
212
|
+
extraContext,
|
|
213
|
+
)
|
|
214
|
+
: {};
|
|
215
|
+
const raw = await entry.instance.invoke(provideInputs);
|
|
216
|
+
if (definition.result == null) return raw;
|
|
217
|
+
const resultContext = { self, result: raw };
|
|
218
|
+
return ctx.moduleContext.expandWith(
|
|
219
|
+
ctx.moduleContext.expandWith(definition.result, resultContext),
|
|
220
|
+
resultContext,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
|
|
132
226
|
teardown: async () => {
|
|
133
227
|
await childContext.teardownResources();
|
|
134
228
|
},
|