@volter/twin-world 0.1.0 → 0.1.2
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/known-external-services.json +1192 -0
- package/package.json +12 -5
- package/src/app-url.ts +175 -0
- package/src/attach.ts +113 -0
- package/src/browser-proxy-cli.ts +2 -1
- package/src/changeset.ts +416 -0
- package/src/cli.ts +589 -10
- package/src/covers.ts +724 -0
- package/src/fixture-env.ts +207 -0
- package/src/host-cli.ts +2 -1
- package/src/host-worker.ts +4 -3
- package/src/host.ts +26 -4
- package/src/index.ts +97 -1
- package/src/init.ts +1142 -0
- package/src/inject-map.ts +70 -0
- package/src/managed-infra-cli.ts +208 -0
- package/src/pack-facts.ts +136 -0
- package/src/pglite-backing.ts +125 -0
- package/src/pglite-host.mjs +147 -0
- package/src/prerequisites.ts +15 -58
- package/src/project-inspect.ts +683 -0
- package/src/redirect-proxy.ts +109 -85
- package/src/reflect.ts +443 -0
- package/src/resource-holder.ts +11 -0
- package/src/resources.ts +160 -0
- package/src/runtime-test-support.ts +208 -0
- package/src/runtime.ts +667 -106
- package/src/schema.ts +67 -0
- package/src/serve.ts +102 -0
- package/src/tail.ts +203 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One managed-infrastructure postgres service, served without a container:
|
|
3
|
+
// PGlite (real Postgres compiled to WASM) behind a pg-gateway wire-protocol
|
|
4
|
+
// front. Spawned by `volter-world-managed-infra` when the pglite backing is
|
|
5
|
+
// selected; never operated directly (the same contract as the compose file).
|
|
6
|
+
//
|
|
7
|
+
// Plain .mjs under node, deliberately: PGlite's WASM init does not complete
|
|
8
|
+
// under bun, and under node its create() promise only progresses while
|
|
9
|
+
// something holds the event loop — an interval carries the init, and the TCP
|
|
10
|
+
// server starts listening only once the backend is ready, so a port that
|
|
11
|
+
// answers is a port that serves.
|
|
12
|
+
//
|
|
13
|
+
// PGlite is a SINGLE-SESSION backend, so this host serializes connections
|
|
14
|
+
// with a transaction-affinity lock: a connection acquires the global lock at
|
|
15
|
+
// its first protocol message and keeps it for as long as the backend reports
|
|
16
|
+
// an open transaction (ReadyForQuery status != 'I'). Without the hold, two
|
|
17
|
+
// pooled clients interleave into one session and a ROLLBACK on one swallows
|
|
18
|
+
// the other's writes. A connection that dies mid-transaction is rolled back
|
|
19
|
+
// before the lock is released.
|
|
20
|
+
//
|
|
21
|
+
// Identity is aliased, not faked: trust auth accepts the world's declared
|
|
22
|
+
// user/password/database, but the backend is PGlite's single `postgres`
|
|
23
|
+
// database — apps that introspect current_database() will see that. Worlds
|
|
24
|
+
// accept this; an app needing true multi-database Postgres needs the
|
|
25
|
+
// container backing.
|
|
26
|
+
import net from 'node:net';
|
|
27
|
+
import { mkdirSync } from 'node:fs';
|
|
28
|
+
import { PGlite } from '@electric-sql/pglite';
|
|
29
|
+
import { fromNodeSocket } from 'pg-gateway/node';
|
|
30
|
+
|
|
31
|
+
function arg(name) {
|
|
32
|
+
const index = process.argv.indexOf(name);
|
|
33
|
+
const value = index >= 0 ? process.argv[index + 1] : undefined;
|
|
34
|
+
if (!value) {
|
|
35
|
+
process.stderr.write(`pglite-host: ${name} is required\n`);
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const port = Number(arg('--port'));
|
|
42
|
+
const dataDir = arg('--data');
|
|
43
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
44
|
+
process.stderr.write('pglite-host: --port must be 1-65535\n');
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
mkdirSync(dataDir, { recursive: true });
|
|
49
|
+
// stdio may be a log fd whose reader is gone; never die on a status line
|
|
50
|
+
process.stdout.on('error', () => {});
|
|
51
|
+
process.stderr.on('error', () => {});
|
|
52
|
+
// PGlite's create() only progresses while something holds the event loop;
|
|
53
|
+
// listening starts AFTER readiness so "port answers" means "serves queries"
|
|
54
|
+
const initKeepAlive = setInterval(() => {}, 500);
|
|
55
|
+
const dbReady = PGlite.create({ dataDir });
|
|
56
|
+
dbReady.catch((error) => {
|
|
57
|
+
process.stderr.write(`pglite-host: backend failed to start: ${error?.message ?? error}\n`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ---- transaction-affinity lock ---------------------------------------------
|
|
62
|
+
let holder = null;
|
|
63
|
+
const waiters = [];
|
|
64
|
+
async function acquire(token) {
|
|
65
|
+
while (holder !== null && holder !== token) {
|
|
66
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
67
|
+
}
|
|
68
|
+
holder = token;
|
|
69
|
+
}
|
|
70
|
+
function release(token) {
|
|
71
|
+
if (holder !== token) return;
|
|
72
|
+
holder = null;
|
|
73
|
+
const next = waiters.shift();
|
|
74
|
+
if (next) next();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The transaction status of the LAST ReadyForQuery ('Z') message in a raw
|
|
78
|
+
* protocol response, or null when the response carries none (mid-pipeline:
|
|
79
|
+
* the sender keeps the lock). 'I' = idle, 'T' = in transaction, 'E' = failed
|
|
80
|
+
* transaction. */
|
|
81
|
+
function lastReadyStatus(response) {
|
|
82
|
+
let status = null;
|
|
83
|
+
let offset = 0;
|
|
84
|
+
while (offset + 5 <= response.length) {
|
|
85
|
+
const type = response[offset];
|
|
86
|
+
const length = (response[offset + 1] << 24) | (response[offset + 2] << 16)
|
|
87
|
+
| (response[offset + 3] << 8) | response[offset + 4];
|
|
88
|
+
if (length < 4) break; // malformed; stop scanning rather than loop
|
|
89
|
+
if (type === 0x5a /* 'Z' */ && offset + 5 < response.length) {
|
|
90
|
+
status = String.fromCharCode(response[offset + 5]);
|
|
91
|
+
}
|
|
92
|
+
offset += 1 + length;
|
|
93
|
+
}
|
|
94
|
+
return status;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const server = net.createServer(async (socket) => {
|
|
98
|
+
const token = {};
|
|
99
|
+
let holding = false;
|
|
100
|
+
socket.on('close', () => {
|
|
101
|
+
if (!holding) return;
|
|
102
|
+
// the client vanished inside its transaction: roll it back, then let the
|
|
103
|
+
// next connection in — never leak a half-open transaction into a stranger
|
|
104
|
+
void dbReady
|
|
105
|
+
.then((db) => db.query('ROLLBACK'))
|
|
106
|
+
.catch(() => {})
|
|
107
|
+
.then(() => {
|
|
108
|
+
holding = false;
|
|
109
|
+
release(token);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
socket.on('error', () => { /* close handler owns cleanup */ });
|
|
113
|
+
await fromNodeSocket(socket, {
|
|
114
|
+
serverVersion: '18.3 (PGlite)',
|
|
115
|
+
auth: { method: 'trust' },
|
|
116
|
+
async onMessage(data, { isAuthenticated }) {
|
|
117
|
+
if (!isAuthenticated) return undefined; // gateway owns startup/auth traffic
|
|
118
|
+
const db = await dbReady;
|
|
119
|
+
await acquire(token);
|
|
120
|
+
holding = true;
|
|
121
|
+
const response = await db.execProtocolRaw(data);
|
|
122
|
+
const status = lastReadyStatus(response);
|
|
123
|
+
if (status === 'I') {
|
|
124
|
+
holding = false;
|
|
125
|
+
release(token);
|
|
126
|
+
}
|
|
127
|
+
return response;
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
void dbReady.then(() => {
|
|
133
|
+
clearInterval(initKeepAlive);
|
|
134
|
+
server.listen(port, '127.0.0.1', () => {
|
|
135
|
+
process.stdout.write(`pglite-host serving postgres wire protocol on 127.0.0.1:${port}\n`);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
for (const signal of ['SIGTERM', 'SIGINT']) {
|
|
140
|
+
process.on(signal, () => {
|
|
141
|
+
server.close(() => {
|
|
142
|
+
void dbReady.then((db) => db.close()).finally(() => process.exit(0));
|
|
143
|
+
});
|
|
144
|
+
// a hung close must not make `down` hang the world
|
|
145
|
+
setTimeout(() => process.exit(0), 3000).unref();
|
|
146
|
+
});
|
|
147
|
+
}
|
package/src/prerequisites.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
1
|
import { spawnSync } from 'node:child_process';
|
|
5
2
|
|
|
6
|
-
export type PrerequisiteId = '
|
|
3
|
+
export type PrerequisiteId = 'local-execution';
|
|
7
4
|
|
|
8
5
|
export type PrerequisiteCheck = {
|
|
9
6
|
id: PrerequisiteId;
|
|
@@ -26,75 +23,35 @@ const realRunner: Runner = (cmd, args) => {
|
|
|
26
23
|
};
|
|
27
24
|
};
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
function dockerContextHost(runner: Runner): { context?: string; host?: string } {
|
|
34
|
-
const contextResult = runner('docker', ['context', 'show']);
|
|
35
|
-
const context = contextResult.status === 0 ? trim(contextResult.stdout) : undefined;
|
|
36
|
-
if (!context) return {};
|
|
37
|
-
|
|
38
|
-
const inspect = runner('docker', ['context', 'inspect', context]);
|
|
39
|
-
if (inspect.status !== 0 || !inspect.stdout.trim()) return { context };
|
|
40
|
-
try {
|
|
41
|
-
const parsed = JSON.parse(inspect.stdout) as Array<{ Endpoints?: { docker?: { Host?: string } } }>;
|
|
42
|
-
return { context, host: parsed[0]?.Endpoints?.docker?.Host };
|
|
43
|
-
} catch {
|
|
44
|
-
return { context };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function commonDockerGuidance(details: Record<string, string>): string[] {
|
|
49
|
-
const guidance = [
|
|
50
|
-
'Start the computer-native Docker-compatible runtime before running this world.',
|
|
51
|
-
'If you are an AI agent running this command, it is your job to start that local runtime now; do not hand this back to the user unless the runtime cannot be started from the machine.',
|
|
52
|
-
'Agents should boot the runtime installed on this machine: `colima start` for Colima, open/start Docker Desktop for Docker Desktop, or start the native Docker service on Linux.',
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
if (details.context || details.host) {
|
|
56
|
-
guidance.push(`Active Docker context${details.context ? ` '${details.context}'` : ''}${details.host ? ` points at ${details.host}` : ''}.`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const colimaSocket = join(homedir(), '.colima', 'default', 'docker.sock');
|
|
60
|
-
if (existsSync(colimaSocket)) {
|
|
61
|
-
guidance.push(`A Colima socket exists at ${colimaSocket}; if the Docker context points elsewhere, update the context or export DOCKER_HOST=unix://${colimaSocket}.`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return guidance;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function checkDockerRuntime(runner: Runner = realRunner): PrerequisiteCheck {
|
|
26
|
+
/** Private implementation probe behind the public World capability name. Its implementation and
|
|
27
|
+
* diagnostics never become instructions for application agents. */
|
|
28
|
+
export function checkLocalExecution(runner: Runner = realRunner): PrerequisiteCheck {
|
|
68
29
|
const info = runner('docker', ['info']);
|
|
69
30
|
if (info.status === 0) {
|
|
70
31
|
return {
|
|
71
|
-
id: '
|
|
32
|
+
id: 'local-execution',
|
|
72
33
|
ok: true,
|
|
73
|
-
label: '
|
|
74
|
-
message: '
|
|
34
|
+
label: 'Local execution',
|
|
35
|
+
message: 'Local execution capacity is available.',
|
|
75
36
|
guidance: [],
|
|
76
37
|
};
|
|
77
38
|
}
|
|
78
39
|
|
|
79
|
-
const context = dockerContextHost(runner);
|
|
80
|
-
const details: Record<string, string> = {};
|
|
81
|
-
if (context.context) details.context = context.context;
|
|
82
|
-
if (context.host) details.host = context.host;
|
|
83
|
-
if (info.stderr.trim()) details.error = info.stderr.trim();
|
|
84
|
-
|
|
85
40
|
return {
|
|
86
|
-
id: '
|
|
41
|
+
id: 'local-execution',
|
|
87
42
|
ok: false,
|
|
88
|
-
label: '
|
|
89
|
-
message: '
|
|
90
|
-
guidance:
|
|
91
|
-
|
|
43
|
+
label: 'Local execution',
|
|
44
|
+
message: 'Local execution capacity is unavailable.',
|
|
45
|
+
guidance: [
|
|
46
|
+
'Run the World through `volter-world up`, then use `volter-world doctor <world>` and its declared service log if startup still fails.',
|
|
47
|
+
'Do not start, inspect, or clean up a backing runtime separately; the declared World service owns it.',
|
|
48
|
+
],
|
|
92
49
|
};
|
|
93
50
|
}
|
|
94
51
|
|
|
95
52
|
export function checkPrerequisites(ids: PrerequisiteId[], runner: Runner = realRunner): PrerequisiteCheck[] {
|
|
96
53
|
return ids.map((id) => {
|
|
97
|
-
if (id === '
|
|
54
|
+
if (id === 'local-execution') return checkLocalExecution(runner);
|
|
98
55
|
throw new Error(`Unknown prerequisite: ${id}`);
|
|
99
56
|
});
|
|
100
57
|
}
|