@volter/world-runtime 3.0.39 → 3.0.41
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/src/catalog-index.d.ts +4 -3
- package/dist/src/catalog-index.js +21 -12
- package/dist/src/catalog.js +15 -32
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/init.js +3 -8
- package/package.json +4 -4
- package/src/catalog-index.ts +20 -14
- package/src/catalog.ts +11 -28
- package/src/index.ts +1 -0
- package/src/init.ts +3 -3
|
@@ -16,6 +16,8 @@ export type Version = {
|
|
|
16
16
|
by: string;
|
|
17
17
|
reason?: string;
|
|
18
18
|
commit?: string;
|
|
19
|
+
integrity?: string;
|
|
20
|
+
submission?: string;
|
|
19
21
|
};
|
|
20
22
|
export type Entry = {
|
|
21
23
|
vendor: string;
|
|
@@ -29,9 +31,8 @@ export declare const readSources: (index: string) => Source[];
|
|
|
29
31
|
export declare function readVendors(index: string): Entry[];
|
|
30
32
|
/** A package's vendor: `@volter/twin-<vendor>`, or the part after `twin-` in any scope. */
|
|
31
33
|
export declare const vendorOf: (name: string) => string;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
export declare function defaults(index: string): Record<string, {
|
|
34
|
+
/** An explicit recommendation, or a vendor's sole live implementation. Publisher versions are independent. */
|
|
35
|
+
export declare function defaults(index: string, vendors?: Iterable<string>): Record<string, {
|
|
35
36
|
name: string;
|
|
36
37
|
version: string;
|
|
37
38
|
source: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The catalog index as the runtime reads it (docs/contributing/architecture.md, "The catalog: where twins come from"):
|
|
2
2
|
// its sources, its vendors' packages and versions, and each vendor's default twin. The index is a directory of data (the
|
|
3
|
-
// `volter-ai/twin-catalog` repository, published as the npm package `@volter/twin-catalog`)
|
|
4
|
-
//
|
|
3
|
+
// `volter-ai/twin-catalog` repository, published as the npm package `@volter/twin-catalog`). The catalog owns
|
|
4
|
+
// validation and publication; `init` reads defaults from the installed artifact.
|
|
5
5
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
/** The index as npm carries it. */
|
|
@@ -17,21 +17,30 @@ const semver = (v) => v.split(/[.-]/).slice(0, 3).map((n) => Number(n) || 0);
|
|
|
17
17
|
const newer = (a, b) => { const x = semver(a); const y = semver(b); for (let i = 0; i < 3; i++)
|
|
18
18
|
if (x[i] !== y[i])
|
|
19
19
|
return x[i] - y[i]; return 0; };
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
export function defaults(index) {
|
|
20
|
+
/** An explicit recommendation, or a vendor's sole live implementation. Publisher versions are independent. */
|
|
21
|
+
export function defaults(index, vendors) {
|
|
23
22
|
const sources = new Map(readSources(index).map((s) => [s.name, s]));
|
|
24
|
-
const
|
|
23
|
+
const recommendationFile = join(index, 'recommendations.json');
|
|
24
|
+
const recommendations = existsSync(recommendationFile) ? JSON.parse(readFileSync(recommendationFile, 'utf8')) : {};
|
|
25
|
+
const revocationsFile = join(index, 'revocations.json');
|
|
26
|
+
const revocations = existsSync(revocationsFile) ? JSON.parse(readFileSync(revocationsFile, 'utf8')) : [];
|
|
25
27
|
const out = {};
|
|
28
|
+
const requested = vendors === undefined ? undefined : new Set(vendors);
|
|
26
29
|
for (const e of readVendors(index)) {
|
|
30
|
+
if (requested && !requested.has(e.vendor))
|
|
31
|
+
continue;
|
|
27
32
|
const candidates = e.packages
|
|
28
|
-
.map((p) => ({ p, s: sources.get(p.source), live: p.versions.filter((v) => v.status === 'live').sort((a, b) => newer(b.version, a.version))[0] }))
|
|
29
|
-
.filter((c) => c.live && c.s)
|
|
30
|
-
|
|
31
|
-
const best = candidates[0];
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
.map((p) => ({ p, s: sources.get(p.source), live: p.versions.filter((v) => v.status === 'live' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(v.version) && !revocations.some((r) => r.package === p.name && r.version === v.version)).sort((a, b) => newer(b.version, a.version))[0] }))
|
|
34
|
+
.filter((c) => c.live && c.s);
|
|
35
|
+
const recommendation = recommendations[e.vendor];
|
|
36
|
+
const best = recommendation ? candidates.find((c) => c.p.name === recommendation) : candidates.length === 1 ? candidates[0] : undefined;
|
|
37
|
+
if (!best && (recommendation || candidates.length > 1))
|
|
38
|
+
throw new Error(`${e.vendor}: catalog needs an explicit live package recommendation; publishers' versions cannot choose it`);
|
|
39
|
+
if (best) {
|
|
40
|
+
if (!/^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(best.p.name) || !best.p.name.startsWith(`${best.s.scope}/`))
|
|
41
|
+
throw new Error(`${e.vendor}: invalid catalog package identity`);
|
|
34
42
|
out[e.vendor] = { name: best.p.name, version: best.live.version, source: best.p.source };
|
|
43
|
+
}
|
|
35
44
|
}
|
|
36
45
|
return out;
|
|
37
46
|
}
|
package/dist/src/catalog.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// The catalog — where the twins a world can run come from (docs/concepts/worlds.md
|
|
2
2
|
// #the-config-and-the-running-world). Two sources answer one interface: a pack-repository CHECKOUT and an
|
|
3
|
-
// INSTALLED catalog (
|
|
3
|
+
// INSTALLED catalog (independently named pack packages under
|
|
4
4
|
// `node_modules`, walked up from the world root — hoisting-proof, and independent of the
|
|
5
5
|
// packages' `exports` maps, which do not expose `package.json`). A world born in an app repo
|
|
6
6
|
// names its twins as packages and the runtime resolves them at boot; a world born here names the
|
|
7
7
|
// selected checkout paths.
|
|
8
8
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
-
import {
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
11
|
import { overlayPackFacts } from "./pack-facts.js";
|
|
12
|
-
import { installedPackageDir, isDerivedPack, packRepository } from '@volter/world-core/runtime';
|
|
12
|
+
import { installedPackageDir, installedVendorPacks, isDerivedPack, packRepository } from '@volter/world-core/runtime';
|
|
13
13
|
export const TWIN_PACKAGE_PREFIX = '@volter/twin-';
|
|
14
14
|
export function twinPackageName(vendor) { return `${TWIN_PACKAGE_PREFIX}${vendor}`; }
|
|
15
15
|
/** A derived (Protocol 3) pack carries the surface derive-pack generated from its spec. */
|
|
@@ -56,34 +56,6 @@ export function packCli(dir) {
|
|
|
56
56
|
}
|
|
57
57
|
/** The nearest `node_modules/<name>` above `from` that holds a package.json — node's own walk. */
|
|
58
58
|
export const findInstalledPackage = installedPackageDir;
|
|
59
|
-
/** Every installed `@volter/twin-<vendor>` visible from `from`, nearest wins. */
|
|
60
|
-
function installedTwins(from) {
|
|
61
|
-
const found = new Map();
|
|
62
|
-
let dir = resolve(from);
|
|
63
|
-
for (;;) {
|
|
64
|
-
const scope = join(dir, 'node_modules', '@volter');
|
|
65
|
-
if (existsSync(scope)) {
|
|
66
|
-
for (const entry of readdirSync(scope, { withFileTypes: true })) {
|
|
67
|
-
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
68
|
-
continue;
|
|
69
|
-
const name = `@volter/${entry.name}`;
|
|
70
|
-
if (name === '@volter/twin-standard')
|
|
71
|
-
continue; // the platform's standard is tooling, not a vendor pack
|
|
72
|
-
if (!name.startsWith(TWIN_PACKAGE_PREFIX))
|
|
73
|
-
continue; // the platform's packages are `@volter/world-*`: outside the prefix by name
|
|
74
|
-
const vendor = name.slice(TWIN_PACKAGE_PREFIX.length);
|
|
75
|
-
const packDir = join(scope, entry.name);
|
|
76
|
-
if (!found.has(vendor) && isPackDir(packDir))
|
|
77
|
-
found.set(vendor, packDir);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
const up = dirname(dir);
|
|
81
|
-
if (up === dir)
|
|
82
|
-
break;
|
|
83
|
-
dir = up;
|
|
84
|
-
}
|
|
85
|
-
return found;
|
|
86
|
-
}
|
|
87
59
|
function fnv1a(text) {
|
|
88
60
|
let h = 0x811c9dc5;
|
|
89
61
|
for (let i = 0; i < text.length; i += 1) {
|
|
@@ -113,7 +85,7 @@ export function checkoutCatalog(root) {
|
|
|
113
85
|
};
|
|
114
86
|
}
|
|
115
87
|
export function installedCatalog(root) {
|
|
116
|
-
const twins =
|
|
88
|
+
const twins = new Map([...installedVendorPacks(root)].filter(([, dir]) => packCli(dir) !== undefined));
|
|
117
89
|
return {
|
|
118
90
|
kind: 'installed',
|
|
119
91
|
root: resolve(root),
|
|
@@ -137,6 +109,17 @@ export function mergedCatalog(sources) {
|
|
|
137
109
|
chosen.set(vendor, source);
|
|
138
110
|
continue;
|
|
139
111
|
}
|
|
112
|
+
const heldName = manifest(held.packDir(vendor))?.name;
|
|
113
|
+
const nextName = manifest(source.packDir(vendor))?.name;
|
|
114
|
+
if (heldName !== nextName) {
|
|
115
|
+
if (source.kindOf(vendor) === 'installed' && held.kindOf(vendor) === 'checkout') {
|
|
116
|
+
chosen.set(vendor, source);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (source.kindOf(vendor) === 'checkout' && held.kindOf(vendor) === 'installed')
|
|
120
|
+
continue;
|
|
121
|
+
throw new Error(`${vendor}: multiple package implementations (${heldName}, ${nextName}); choose one explicitly`);
|
|
122
|
+
}
|
|
140
123
|
if (major(source.version(vendor)) > major(held.version(vendor)))
|
|
141
124
|
chosen.set(vendor, source);
|
|
142
125
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -55,3 +55,4 @@ export { assertWorldStateRemovable } from './ancestry.js';
|
|
|
55
55
|
export { withoutVaultSignIn } from './runtime.js';
|
|
56
56
|
export { errorMessage } from './error-message.js';
|
|
57
57
|
export { materializeBrowserFiles, browsersDir, worldBrowser, worldBrowsers, putWorldBrowser, deleteWorldBrowser, inheritWorldBrowsers, type WorldBrowser } from './browsers.js';
|
|
58
|
+
export { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from './catalog-index.js';
|
package/dist/src/index.js
CHANGED
|
@@ -39,3 +39,4 @@ export { assertWorldStateRemovable } from "./ancestry.js";
|
|
|
39
39
|
export { withoutVaultSignIn } from "./runtime.js";
|
|
40
40
|
export { errorMessage } from "./error-message.js";
|
|
41
41
|
export { materializeBrowserFiles, browsersDir, worldBrowser, worldBrowsers, putWorldBrowser, deleteWorldBrowser, inheritWorldBrowsers } from "./browsers.js";
|
|
42
|
+
export { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from "./catalog-index.js";
|
package/dist/src/init.js
CHANGED
|
@@ -38,7 +38,7 @@ import { PROTOCOL_MAJOR, stateDirName } from '@volter/world-core/runtime';
|
|
|
38
38
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
39
39
|
import { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, injectorEnvNameForKey, injectorVendorKeysFor, isCredentialShapedEnvName, registryAcknowledgedReason, } from "./covers.js";
|
|
40
40
|
import { fakeEnvValue, isAppKeyName, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from "./fixture-env.js";
|
|
41
|
-
import { findInstalledPackage, packCli, packEntry, resolveCatalog
|
|
41
|
+
import { findInstalledPackage, packCli, packEntry, resolveCatalog } from "./catalog.js";
|
|
42
42
|
import { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from "./catalog-index.js";
|
|
43
43
|
import { projectEnvReads, projectManifestDirs } from "./project-inspect.js";
|
|
44
44
|
import { overlayEndpointEnv, packFacts } from "./pack-facts.js";
|
|
@@ -523,12 +523,6 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
523
523
|
// the catalog index's defaults (the installed `@volter/twin-catalog`), which name the twin to install for a vendor
|
|
524
524
|
// this catalog lacks
|
|
525
525
|
const indexDir = findInstalledPackage(root, CATALOG_INDEX_PACKAGE);
|
|
526
|
-
const indexDefaults = (() => { try {
|
|
527
|
-
return indexDir === undefined ? {} : catalogDefaults(indexDir);
|
|
528
|
-
}
|
|
529
|
-
catch {
|
|
530
|
-
return {};
|
|
531
|
-
} })();
|
|
532
526
|
// A pack's starters, copied VISIBLY into the world dir: the author's to edit or delete (copy = ownership; never
|
|
533
527
|
// baked into the pack). Its handlers starter is the answering turns its own life scripts (journeys/scenario.json, the
|
|
534
528
|
// World's handler format, its faults left out); its default data is src/semantics/seed.ts.
|
|
@@ -570,6 +564,7 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
570
564
|
continue;
|
|
571
565
|
}
|
|
572
566
|
if (!catalog.has(vendor)) {
|
|
567
|
+
const indexDefaults = indexDir === undefined ? {} : catalogDefaults(indexDir, [vendor]);
|
|
573
568
|
vendors.push({
|
|
574
569
|
vendor,
|
|
575
570
|
pack: null,
|
|
@@ -631,7 +626,7 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
631
626
|
// inside this checkout it is the checkout-relative path, as the cookbook and the gates expect.
|
|
632
627
|
const boot = catalogSource.kindOf(vendor) === 'installed'
|
|
633
628
|
? {
|
|
634
|
-
package:
|
|
629
|
+
package: JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8')).name,
|
|
635
630
|
...(handlersArg !== null ? { args: ['--scenario', handlersArg] } : {}),
|
|
636
631
|
...(serveExport !== undefined ? { colocate: { export: serveExport, ...(handlersArg !== null ? { scenarioPath: handlersArg } : {}) } } : {}),
|
|
637
632
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.41",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -69,14 +69,14 @@
|
|
|
69
69
|
"node": ">=22.3"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@volter/world-core": "3.0.
|
|
73
|
-
"@volter/world-console": "3.0.
|
|
72
|
+
"@volter/world-core": "3.0.41",
|
|
73
|
+
"@volter/world-console": "3.0.36"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@electric-sql/pglite": "0.5.8",
|
|
77
77
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
78
78
|
"@volter/world-access": "3.0.1",
|
|
79
|
-
"@volter/world-core": "3.0.
|
|
79
|
+
"@volter/world-core": "3.0.41",
|
|
80
80
|
"mongodb": "~6.20.0",
|
|
81
81
|
"pg-gateway": "0.3.0-beta.4",
|
|
82
82
|
"smol-toml": "^1.8.0",
|
package/src/catalog-index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The catalog index as the runtime reads it (docs/contributing/architecture.md, "The catalog: where twins come from"):
|
|
2
2
|
// its sources, its vendors' packages and versions, and each vendor's default twin. The index is a directory of data (the
|
|
3
|
-
// `volter-ai/twin-catalog` repository, published as the npm package `@volter/twin-catalog`)
|
|
4
|
-
//
|
|
3
|
+
// `volter-ai/twin-catalog` repository, published as the npm package `@volter/twin-catalog`). The catalog owns
|
|
4
|
+
// validation and publication; `init` reads defaults from the installed artifact.
|
|
5
5
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
|
|
@@ -10,7 +10,7 @@ export const CATALOG_INDEX_PACKAGE = '@volter/twin-catalog';
|
|
|
10
10
|
|
|
11
11
|
/** A source; `workflow` is the file under `.github/workflows/` its packages are released by (release.yml unless named). */
|
|
12
12
|
export type Source = { name: string; repository: string; scope: string; official: boolean; protocol: '3'; workflow?: string };
|
|
13
|
-
export type Version = { version: string; status: 'live' | 'pending' | 'rejected'; at: string; by: string; reason?: string; commit?: string };
|
|
13
|
+
export type Version = { version: string; status: 'live' | 'pending' | 'rejected'; at: string; by: string; reason?: string; commit?: string; integrity?: string; submission?: string };
|
|
14
14
|
export type Entry = { vendor: string; packages: Array<{ name: string; source: string; versions: Version[] }> };
|
|
15
15
|
|
|
16
16
|
export const readSources = (index: string): Source[] => JSON.parse(readFileSync(join(index, 'sources.json'), 'utf8')) as Source[];
|
|
@@ -24,21 +24,27 @@ export const vendorOf = (name: string): string => name.replace(/^@[^/]+\//, '').
|
|
|
24
24
|
const semver = (v: string): number[] => v.split(/[.-]/).slice(0, 3).map((n) => Number(n) || 0);
|
|
25
25
|
const newer = (a: string, b: string): number => { const x = semver(a); const y = semver(b); for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i]! - y[i]!; return 0; };
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
export function defaults(index: string): Record<string, { name: string; version: string; source: string }> {
|
|
27
|
+
/** An explicit recommendation, or a vendor's sole live implementation. Publisher versions are independent. */
|
|
28
|
+
export function defaults(index: string, vendors?: Iterable<string>): Record<string, { name: string; version: string; source: string }> {
|
|
30
29
|
const sources = new Map(readSources(index).map((s) => [s.name, s]));
|
|
31
|
-
const
|
|
30
|
+
const recommendationFile = join(index, 'recommendations.json');
|
|
31
|
+
const recommendations = existsSync(recommendationFile) ? JSON.parse(readFileSync(recommendationFile, 'utf8')) as Record<string, string> : {};
|
|
32
|
+
const revocationsFile = join(index, 'revocations.json');
|
|
33
|
+
const revocations = existsSync(revocationsFile) ? JSON.parse(readFileSync(revocationsFile, 'utf8')) as Array<{ package: string; version: string }> : [];
|
|
32
34
|
const out: Record<string, { name: string; version: string; source: string }> = {};
|
|
35
|
+
const requested = vendors === undefined ? undefined : new Set(vendors);
|
|
33
36
|
for (const e of readVendors(index)) {
|
|
37
|
+
if (requested && !requested.has(e.vendor)) continue;
|
|
34
38
|
const candidates = e.packages
|
|
35
|
-
.map((p) => ({ p, s: sources.get(p.source), live: p.versions.filter((v) => v.status === 'live').sort((a, b) => newer(b.version, a.version))[0] }))
|
|
36
|
-
.filter((c) => c.live && c.s)
|
|
37
|
-
|
|
38
|
-
const best = candidates[0];
|
|
39
|
-
|
|
40
|
-
if (best
|
|
39
|
+
.map((p) => ({ p, s: sources.get(p.source), live: p.versions.filter((v) => v.status === 'live' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(v.version) && !revocations.some((r) => r.package === p.name && r.version === v.version)).sort((a, b) => newer(b.version, a.version))[0] }))
|
|
40
|
+
.filter((c) => c.live && c.s);
|
|
41
|
+
const recommendation = recommendations[e.vendor];
|
|
42
|
+
const best = recommendation ? candidates.find((c) => c.p.name === recommendation) : candidates.length === 1 ? candidates[0] : undefined;
|
|
43
|
+
if (!best && (recommendation || candidates.length > 1)) throw new Error(`${e.vendor}: catalog needs an explicit live package recommendation; publishers' versions cannot choose it`);
|
|
44
|
+
if (best) {
|
|
45
|
+
if (!/^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(best.p.name) || !best.p.name.startsWith(`${best.s!.scope}/`)) throw new Error(`${e.vendor}: invalid catalog package identity`);
|
|
46
|
+
out[e.vendor] = { name: best.p.name, version: best.live!.version, source: best.p.source };
|
|
47
|
+
}
|
|
41
48
|
}
|
|
42
49
|
return out;
|
|
43
50
|
}
|
|
44
|
-
|
package/src/catalog.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// The catalog — where the twins a world can run come from (docs/concepts/worlds.md
|
|
2
2
|
// #the-config-and-the-running-world). Two sources answer one interface: a pack-repository CHECKOUT and an
|
|
3
|
-
// INSTALLED catalog (
|
|
3
|
+
// INSTALLED catalog (independently named pack packages under
|
|
4
4
|
// `node_modules`, walked up from the world root — hoisting-proof, and independent of the
|
|
5
5
|
// packages' `exports` maps, which do not expose `package.json`). A world born in an app repo
|
|
6
6
|
// names its twins as packages and the runtime resolves them at boot; a world born here names the
|
|
7
7
|
// selected checkout paths.
|
|
8
8
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
-
import {
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
11
|
import { overlayPackFacts, type PackFacts } from './pack-facts.ts';
|
|
12
|
-
import { installedPackageDir, isDerivedPack, packRepository } from '@volter/world-core/runtime';
|
|
12
|
+
import { installedPackageDir, installedVendorPacks, isDerivedPack, packRepository } from '@volter/world-core/runtime';
|
|
13
13
|
|
|
14
14
|
export const TWIN_PACKAGE_PREFIX = '@volter/twin-';
|
|
15
15
|
|
|
@@ -71,30 +71,6 @@ export function packCli(dir: string): string | undefined {
|
|
|
71
71
|
/** The nearest `node_modules/<name>` above `from` that holds a package.json — node's own walk. */
|
|
72
72
|
export const findInstalledPackage = installedPackageDir;
|
|
73
73
|
|
|
74
|
-
/** Every installed `@volter/twin-<vendor>` visible from `from`, nearest wins. */
|
|
75
|
-
function installedTwins(from: string): Map<string, string> {
|
|
76
|
-
const found = new Map<string, string>();
|
|
77
|
-
let dir = resolve(from);
|
|
78
|
-
for (;;) {
|
|
79
|
-
const scope = join(dir, 'node_modules', '@volter');
|
|
80
|
-
if (existsSync(scope)) {
|
|
81
|
-
for (const entry of readdirSync(scope, { withFileTypes: true })) {
|
|
82
|
-
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
83
|
-
const name = `@volter/${entry.name}`;
|
|
84
|
-
if (name === '@volter/twin-standard') continue; // the platform's standard is tooling, not a vendor pack
|
|
85
|
-
if (!name.startsWith(TWIN_PACKAGE_PREFIX)) continue; // the platform's packages are `@volter/world-*`: outside the prefix by name
|
|
86
|
-
const vendor = name.slice(TWIN_PACKAGE_PREFIX.length);
|
|
87
|
-
const packDir = join(scope, entry.name);
|
|
88
|
-
if (!found.has(vendor) && isPackDir(packDir)) found.set(vendor, packDir);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const up = dirname(dir);
|
|
92
|
-
if (up === dir) break;
|
|
93
|
-
dir = up;
|
|
94
|
-
}
|
|
95
|
-
return found;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
74
|
function fnv1a(text: string): string {
|
|
99
75
|
let h = 0x811c9dc5;
|
|
100
76
|
for (let i = 0; i < text.length; i += 1) { h ^= text.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
|
|
@@ -123,7 +99,7 @@ export function checkoutCatalog(root: string): Catalog {
|
|
|
123
99
|
}
|
|
124
100
|
|
|
125
101
|
export function installedCatalog(root: string): Catalog {
|
|
126
|
-
const twins =
|
|
102
|
+
const twins = new Map([...installedVendorPacks(root)].filter(([, dir]) => packCli(dir) !== undefined));
|
|
127
103
|
return {
|
|
128
104
|
kind: 'installed',
|
|
129
105
|
root: resolve(root),
|
|
@@ -145,6 +121,13 @@ export function mergedCatalog(sources: Catalog[]): Catalog {
|
|
|
145
121
|
for (const vendor of source.vendors()) {
|
|
146
122
|
const held = chosen.get(vendor);
|
|
147
123
|
if (held === undefined) { chosen.set(vendor, source); continue; }
|
|
124
|
+
const heldName = manifest(held.packDir(vendor)!)?.name;
|
|
125
|
+
const nextName = manifest(source.packDir(vendor)!)?.name;
|
|
126
|
+
if (heldName !== nextName) {
|
|
127
|
+
if (source.kindOf(vendor) === 'installed' && held.kindOf(vendor) === 'checkout') { chosen.set(vendor, source); continue; }
|
|
128
|
+
if (source.kindOf(vendor) === 'checkout' && held.kindOf(vendor) === 'installed') continue;
|
|
129
|
+
throw new Error(`${vendor}: multiple package implementations (${heldName}, ${nextName}); choose one explicitly`);
|
|
130
|
+
}
|
|
148
131
|
if (major(source.version(vendor)) > major(held.version(vendor))) chosen.set(vendor, source);
|
|
149
132
|
}
|
|
150
133
|
}
|
package/src/index.ts
CHANGED
|
@@ -180,3 +180,4 @@ export { withoutVaultSignIn } from './runtime.ts';
|
|
|
180
180
|
export { errorMessage } from './error-message.ts';
|
|
181
181
|
|
|
182
182
|
export { materializeBrowserFiles, browsersDir, worldBrowser, worldBrowsers, putWorldBrowser, deleteWorldBrowser, inheritWorldBrowsers, type WorldBrowser } from './browsers.ts';
|
|
183
|
+
export { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from './catalog-index.ts';
|
package/src/init.ts
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
type CoverageReport,
|
|
49
49
|
} from './covers.ts';
|
|
50
50
|
import { fakeEnvValue, isAppKeyName, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from './fixture-env.ts';
|
|
51
|
-
import { findInstalledPackage, packCli, packEntry, resolveCatalog,
|
|
51
|
+
import { findInstalledPackage, packCli, packEntry, resolveCatalog, type Catalog } from './catalog.ts';
|
|
52
52
|
import { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from './catalog-index.ts';
|
|
53
53
|
import { projectEnvReads, projectManifestDirs } from './project-inspect.ts';
|
|
54
54
|
import { overlayEndpointEnv, packFacts } from './pack-facts.ts';
|
|
@@ -726,7 +726,6 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
726
726
|
// the catalog index's defaults (the installed `@volter/twin-catalog`), which name the twin to install for a vendor
|
|
727
727
|
// this catalog lacks
|
|
728
728
|
const indexDir = findInstalledPackage(root, CATALOG_INDEX_PACKAGE);
|
|
729
|
-
const indexDefaults = (() => { try { return indexDir === undefined ? {} : catalogDefaults(indexDir); } catch { return {}; } })();
|
|
730
729
|
// A pack's starters, copied VISIBLY into the world dir: the author's to edit or delete (copy = ownership; never
|
|
731
730
|
// baked into the pack). Its handlers starter is the answering turns its own life scripts (journeys/scenario.json, the
|
|
732
731
|
// World's handler format, its faults left out); its default data is src/semantics/seed.ts.
|
|
@@ -765,6 +764,7 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
765
764
|
continue;
|
|
766
765
|
}
|
|
767
766
|
if (!catalog.has(vendor)) {
|
|
767
|
+
const indexDefaults = indexDir === undefined ? {} : catalogDefaults(indexDir, [vendor]);
|
|
768
768
|
vendors.push({
|
|
769
769
|
vendor,
|
|
770
770
|
pack: null,
|
|
@@ -821,7 +821,7 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
821
821
|
// inside this checkout it is the checkout-relative path, as the cookbook and the gates expect.
|
|
822
822
|
const boot: Pick<WorldServiceConfig, 'package' | 'command' | 'args' | 'colocate'> = catalogSource.kindOf(vendor) === 'installed'
|
|
823
823
|
? {
|
|
824
|
-
package:
|
|
824
|
+
package: (JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8')) as { name: string }).name,
|
|
825
825
|
...(handlersArg !== null ? { args: ['--scenario', handlersArg] } : {}),
|
|
826
826
|
...(serveExport !== undefined ? { colocate: { export: serveExport, ...(handlersArg !== null ? { scenarioPath: handlersArg } : {}) } } : {}),
|
|
827
827
|
}
|