@rebon/cli-win32-x64 1.2.0 → 1.4.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 +202 -0
- package/NOTICE +10 -0
- package/README.md +203 -144
- package/THIRD_PARTY_NOTICES.txt +1232 -128
- package/bin/rebon.js +22 -22
- package/lib/windows-managed.js +390 -390
- package/package.json +9 -2
- package/payload/compose-runtime/package.json +17 -17
- package/payload/compose-runtime/payload/compose/shims/dsh-anonymous-user-id.js +10 -10
- package/payload/compose-runtime/payload/compose/shims/dsh-credentials.js +13 -13
- package/payload/compose-runtime/payload/compose/shims/dsh-launch-environment.js +21 -21
- package/payload/compose-runtime/payload/compose/shims/dsh-llm.js +313 -313
- package/payload/compose-runtime/payload/compose/shims/dsh-settings.js +43 -43
- package/payload/compose-runtime/payload/compose/shims/dsh-tools.js +20 -20
- package/payload/compose-runtime/payload/compose/shims/dsh-web.js +10 -10
- package/payload/compose-runtime/payload/compose/shims/llm-adapter.js +32 -32
- package/payload/compose-runtime/payload/compose/shims/zod-lite.js +49 -49
- package/payload/compose-runtime/payload/vendor/cordis/LICENSE +21 -0
- package/payload/compose-runtime/payload/vendor/cordis/index.js +1530 -1530
- package/payload/compose-runtime/payload/vendor/cosmokit/LICENSE +21 -21
- package/payload/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -357
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent-loop +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-core +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-deepseek +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-logger-console +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-scope +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-session +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-system-prompt +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timeout +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timer +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-todo +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-web +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tools-schema +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown +21 -0
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown-plugin-gfm +21 -0
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-web-search-exa +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/agent-loop.js +1193 -1193
- package/payload/compose-runtime/payload/vendor/dsh/agent.js +714 -714
- package/payload/compose-runtime/payload/vendor/dsh/llm-core.js +269 -269
- package/payload/compose-runtime/payload/vendor/dsh/llm-deepseek.js +672 -672
- package/payload/compose-runtime/payload/vendor/dsh/logger-console.js +83 -83
- package/payload/compose-runtime/payload/vendor/dsh/scope.js +287 -287
- package/payload/compose-runtime/payload/vendor/dsh/session.js +1668 -1668
- package/payload/compose-runtime/payload/vendor/dsh/system-prompt.js +309 -309
- package/payload/compose-runtime/payload/vendor/dsh/timeout.js +100 -100
- package/payload/compose-runtime/payload/vendor/dsh/timer.js +128 -128
- package/payload/compose-runtime/payload/vendor/dsh/tool-todo.js +143 -143
- package/payload/compose-runtime/payload/vendor/dsh/tool-web.js +1527 -1527
- package/payload/compose-runtime/payload/vendor/dsh/tools-schema.js +849 -849
- package/payload/compose-runtime/payload/vendor/dsh/web-search-exa.js +122 -122
- package/payload/compose-runtime/payload/vendor/eventsource-parser/LICENSE +21 -21
- package/payload/compose-runtime/payload/vendor/eventsource-parser/index.js +177 -177
- package/payload/compose-runtime/payload/vendor/eventsource-parser/stream.js +48 -48
- package/payload/compose-runtime/payload/vendor/schemastery/LICENSE +21 -0
- package/payload/compose-runtime/payload/vendor/schemastery/index.mjs +656 -656
- package/payload/compose-runtime/payload-manifests.json +48 -48
- package/payload/compose-runtime/src/bridge.mjs +193 -195
- package/payload/compose-runtime/src/credentials-runtime.mjs +50 -50
- package/payload/compose-runtime/src/eventsource-stream.mjs +48 -48
- package/payload/compose-runtime/src/index.mjs +7 -7
- package/payload/compose-runtime/src/llm-runtime.mjs +326 -326
- package/payload/compose-runtime/src/loader.mjs +78 -78
- package/payload/compose-runtime/src/loop-assembly.mjs +237 -237
- package/payload/compose-runtime/src/payload.mjs +49 -49
- package/payload/compose-runtime/src/plugin.mjs +59 -59
- package/payload/compose-runtime/src/realm.mjs +223 -223
- package/payload/compose-runtime/src/registry.mjs +191 -191
- package/payload/compose-runtime/src/resolve.mjs +115 -115
- package/payload/compose-runtime/src/systemprompt-runtime.mjs +67 -67
- package/payload/compose-runtime/src/tools-runtime.mjs +239 -239
- package/payload/compose-runtime/src/web-runtime.mjs +209 -209
- package/payload/plugin-host/package.json +11 -11
- package/payload/plugin-host/src/bridge.mjs +64 -64
- package/payload/plugin-host/src/cli.mjs +147 -147
- package/payload/plugin-host/src/framing.mjs +113 -113
- package/payload/plugin-host/src/host.mjs +691 -691
- package/payload/plugin-host/src/json.mjs +208 -208
- package/payload/plugin-host/src/lifecycle.mjs +114 -114
- package/payload/plugin-host/src/loader.mjs +183 -183
- package/payload/plugin-host/src/methods.mjs +377 -377
- package/payload/plugin-host/src/ownership.mjs +57 -57
- package/payload/plugin-host/src/protocol.mjs +129 -129
- package/payload/plugin-host/src/sdk.mjs +7 -7
- package/payload/rebon-boa-helper.exe +0 -0
- package/payload/rebon-browser-mcp.exe +0 -0
- package/payload/rebon-computer-use.exe +0 -0
- package/payload/rebon-lsp-mcp.exe +0 -0
- package/payload/rebon.exe +2 -2
- package/payload/sandbox-win.exe +0 -0
- package/scripts/postinstall.js +112 -112
- package/payload/compose-runtime/payload/compose/credentials-runtime.js +0 -38
- package/payload/compose-runtime/payload/compose/llm-runtime.js +0 -330
- package/payload/compose-runtime/payload/compose/loop-assembly.js +0 -149
- package/payload/compose-runtime/payload/compose/serve-dispatch.js +0 -82
- package/payload/compose-runtime/payload/compose/systemprompt-runtime.js +0 -74
- package/payload/compose-runtime/payload/compose/tools-runtime.js +0 -228
- package/payload/compose-runtime/payload/compose/web-runtime.js +0 -204
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { pathToFileURL } from 'node:url';
|
|
3
|
-
import { NdjsonDecoder, SerializedWriter } from './framing.mjs';
|
|
4
|
-
import { PluginHost } from './host.mjs';
|
|
5
|
-
import { loadPlugin } from './loader.mjs';
|
|
6
|
-
import { currentOwner } from './ownership.mjs';
|
|
7
|
-
import { ProtocolError } from './protocol.mjs';
|
|
8
|
-
|
|
9
|
-
export function diagnostic(stream, code, message) {
|
|
10
|
-
const bounded = String(message).replace(/[\r\n]+/g, ' ').slice(0, 512);
|
|
11
|
-
stream.write(`${JSON.stringify({ level: 'error', code, message: bounded })}\n`);
|
|
12
|
-
}
|
|
13
|
-
export function installHostConsole(stream) {
|
|
14
|
-
const write = (level, values) => stream.write(`${JSON.stringify({ level, message: values.map(String).join(' ').slice(0, 512) })}\n`);
|
|
15
|
-
return Object.freeze({ log: (...v) => write('info', v), info: (...v) => write('info', v), warn: (...v) => write('warn', v), error: (...v) => write('error', v) });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Frames are not handled one-at-a-time.
|
|
19
|
-
//
|
|
20
|
-
// A request handler may itself call rebon — that is the whole Node → rebon
|
|
21
|
-
// direction — and the answer to that call arrives as a frame on this very
|
|
22
|
-
// stream. Awaiting each dispatch before reading the next would mean the handler
|
|
23
|
-
// waits for a frame the loop cannot read until the handler returns: a deadlock
|
|
24
|
-
// that no amount of care inside a plugin can avoid. So dispatches are tracked
|
|
25
|
-
// rather than awaited, and everything that needs them finished waits here.
|
|
26
|
-
/// Loads the module that decides what a plugin package *is*.
|
|
27
|
-
///
|
|
28
|
-
/// The host itself knows one shape: a module exporting `activate`. Anything
|
|
29
|
-
/// else — a Cordis plugin mounted into a shared composition realm, say — is a
|
|
30
|
-
/// different answer to the same question, and the host has no business knowing
|
|
31
|
-
/// which answers exist. So rebon names the module that does, and this loads it.
|
|
32
|
-
///
|
|
33
|
-
/// The module exports `createLoader({ next })` and returns `{ load, unload }`;
|
|
34
|
-
/// `next` is the built-in loader, so an adapter that does not recognise a
|
|
35
|
-
/// package hands it back rather than reimplementing it.
|
|
36
|
-
export async function resolveLoader(specifier, error) {
|
|
37
|
-
if (!specifier) return {};
|
|
38
|
-
const url = specifier.startsWith('file:') ? specifier : pathToFileURL(specifier).href;
|
|
39
|
-
const module = await import(url);
|
|
40
|
-
if (typeof module.createLoader !== 'function') {
|
|
41
|
-
throw new ProtocolError('loader_invalid', `${specifier} exports no createLoader`);
|
|
42
|
-
}
|
|
43
|
-
const seams = await module.createLoader({ next: loadPlugin });
|
|
44
|
-
if (typeof seams?.load !== 'function') {
|
|
45
|
-
throw new ProtocolError('loader_invalid', `${specifier} produced no load function`);
|
|
46
|
-
}
|
|
47
|
-
return seams;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function runHost({ input, output, error, loader }) {
|
|
51
|
-
const decoder = new NdjsonDecoder();
|
|
52
|
-
const writer = new SerializedWriter(output);
|
|
53
|
-
const host = new PluginHost(writer, loader ?? {});
|
|
54
|
-
const running = new Set();
|
|
55
|
-
let fatal = null;
|
|
56
|
-
const track = (promise) => {
|
|
57
|
-
const tracked = promise.catch((cause) => { fatal ??= cause; }).finally(() => running.delete(tracked));
|
|
58
|
-
running.add(tracked);
|
|
59
|
-
};
|
|
60
|
-
// Draining rather than a single `Promise.all`: a dispatch that is still
|
|
61
|
-
// running can schedule another one, and the process must not exit under it.
|
|
62
|
-
const quiesce = async () => { while (running.size > 0) await Promise.all([...running]); };
|
|
63
|
-
try {
|
|
64
|
-
for await (const chunk of input) {
|
|
65
|
-
for (const frame of decoder.push(chunk)) {
|
|
66
|
-
if (fatal) throw fatal;
|
|
67
|
-
if (host.shuttingDown) throw new ProtocolError('frame_after_shutdown', 'frame received after shutdown');
|
|
68
|
-
track(host.accept(frame));
|
|
69
|
-
}
|
|
70
|
-
if (fatal) throw fatal;
|
|
71
|
-
if (host.shuttingDown) { await quiesce(); if (fatal) throw fatal; return 0; }
|
|
72
|
-
}
|
|
73
|
-
decoder.finish();
|
|
74
|
-
await quiesce();
|
|
75
|
-
if (fatal) throw fatal;
|
|
76
|
-
diagnostic(error, 'connection_lost', 'stdin reached EOF before successful shutdown');
|
|
77
|
-
return 3;
|
|
78
|
-
} catch (cause) {
|
|
79
|
-
// Terminals already owed still have to reach stdout: a caller waiting on
|
|
80
|
-
// one is not made whole by the host exiting with a diagnosis.
|
|
81
|
-
await quiesce();
|
|
82
|
-
const code = cause?.code ?? 'host_failure';
|
|
83
|
-
diagnostic(error, code, cause?.message ?? 'host failed');
|
|
84
|
-
return code === 'write_error' ? 4 : 2;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/// A rejection nobody handled, said out loud instead of swallowed, and
|
|
89
|
-
/// attributed where the async chain reaches back to an entry.
|
|
90
|
-
///
|
|
91
|
-
/// Node does not stop this process for one, and
|
|
92
|
-
///
|
|
93
|
-
///
|
|
94
|
-
///
|
|
95
|
-
///
|
|
96
|
-
///
|
|
97
|
-
///
|
|
98
|
-
///
|
|
99
|
-
///
|
|
100
|
-
///
|
|
101
|
-
///
|
|
102
|
-
/// It never exits. A stray rejection in one plugin must not take down a host
|
|
103
|
-
/// other plugins are being served from; a load still in flight is failed by its
|
|
104
|
-
/// own path instead, which is the caller's to run.
|
|
105
|
-
export function installRejectionDiagnostic(stream) {
|
|
106
|
-
process.on('unhandledRejection', (reason) => {
|
|
107
|
-
const first = String(reason?.stack ?? reason ?? '').split('\n')[0];
|
|
108
|
-
const owner = currentOwner();
|
|
109
|
-
if (owner === undefined) {
|
|
110
|
-
diagnostic(stream, 'unhandled_rejection', `unattributed: ${first}`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
// `loading` says which of the two this is. While a load is in flight the
|
|
114
|
-
// entry did not install correctly and nothing depends on it yet, so the
|
|
115
|
-
// load fails and the composition skips it with a reason. Afterwards the
|
|
116
|
-
// plugin stays ready: a load that already succeeded cannot be un-failed,
|
|
117
|
-
// and tearing down a plane other entries serve from would cost far more
|
|
118
|
-
// than the one bad promise.
|
|
119
|
-
if (owner.loading) {
|
|
120
|
-
owner.rejection = first;
|
|
121
|
-
diagnostic(stream, 'unhandled_rejection', `plugin=${owner.entry} during load: ${first}`);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
diagnostic(stream, 'unhandled_rejection', `plugin=${owner.entry}: ${first}`);
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
129
|
-
installRejectionDiagnostic(process.stderr);
|
|
130
|
-
const flag = process.argv.indexOf('--loader');
|
|
131
|
-
const specifier = flag >= 0 ? process.argv[flag + 1] : process.env.REBON_PLUGIN_LOADER;
|
|
132
|
-
let loader;
|
|
133
|
-
try {
|
|
134
|
-
loader = await resolveLoader(specifier, process.stderr);
|
|
135
|
-
} catch (cause) {
|
|
136
|
-
// A host that cannot build the loader rebon asked for must not fall back to
|
|
137
|
-
// the built-in one: it would load a different set of plugins than the one
|
|
138
|
-
// rebon believes it asked for, and say nothing.
|
|
139
|
-
diagnostic(process.stderr, cause?.code ?? 'loader_failure', cause?.message ?? 'loader failed');
|
|
140
|
-
process.exitCode = 2;
|
|
141
|
-
}
|
|
142
|
-
if (loader !== undefined) {
|
|
143
|
-
process.exitCode = await runHost({
|
|
144
|
-
input: process.stdin, output: process.stdout, error: process.stderr, loader,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { NdjsonDecoder, SerializedWriter } from './framing.mjs';
|
|
4
|
+
import { PluginHost } from './host.mjs';
|
|
5
|
+
import { loadPlugin } from './loader.mjs';
|
|
6
|
+
import { currentOwner } from './ownership.mjs';
|
|
7
|
+
import { ProtocolError } from './protocol.mjs';
|
|
8
|
+
|
|
9
|
+
export function diagnostic(stream, code, message) {
|
|
10
|
+
const bounded = String(message).replace(/[\r\n]+/g, ' ').slice(0, 512);
|
|
11
|
+
stream.write(`${JSON.stringify({ level: 'error', code, message: bounded })}\n`);
|
|
12
|
+
}
|
|
13
|
+
export function installHostConsole(stream) {
|
|
14
|
+
const write = (level, values) => stream.write(`${JSON.stringify({ level, message: values.map(String).join(' ').slice(0, 512) })}\n`);
|
|
15
|
+
return Object.freeze({ log: (...v) => write('info', v), info: (...v) => write('info', v), warn: (...v) => write('warn', v), error: (...v) => write('error', v) });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Frames are not handled one-at-a-time.
|
|
19
|
+
//
|
|
20
|
+
// A request handler may itself call rebon — that is the whole Node → rebon
|
|
21
|
+
// direction — and the answer to that call arrives as a frame on this very
|
|
22
|
+
// stream. Awaiting each dispatch before reading the next would mean the handler
|
|
23
|
+
// waits for a frame the loop cannot read until the handler returns: a deadlock
|
|
24
|
+
// that no amount of care inside a plugin can avoid. So dispatches are tracked
|
|
25
|
+
// rather than awaited, and everything that needs them finished waits here.
|
|
26
|
+
/// Loads the module that decides what a plugin package *is*.
|
|
27
|
+
///
|
|
28
|
+
/// The host itself knows one shape: a module exporting `activate`. Anything
|
|
29
|
+
/// else — a Cordis plugin mounted into a shared composition realm, say — is a
|
|
30
|
+
/// different answer to the same question, and the host has no business knowing
|
|
31
|
+
/// which answers exist. So rebon names the module that does, and this loads it.
|
|
32
|
+
///
|
|
33
|
+
/// The module exports `createLoader({ next })` and returns `{ load, unload }`;
|
|
34
|
+
/// `next` is the built-in loader, so an adapter that does not recognise a
|
|
35
|
+
/// package hands it back rather than reimplementing it.
|
|
36
|
+
export async function resolveLoader(specifier, error) {
|
|
37
|
+
if (!specifier) return {};
|
|
38
|
+
const url = specifier.startsWith('file:') ? specifier : pathToFileURL(specifier).href;
|
|
39
|
+
const module = await import(url);
|
|
40
|
+
if (typeof module.createLoader !== 'function') {
|
|
41
|
+
throw new ProtocolError('loader_invalid', `${specifier} exports no createLoader`);
|
|
42
|
+
}
|
|
43
|
+
const seams = await module.createLoader({ next: loadPlugin });
|
|
44
|
+
if (typeof seams?.load !== 'function') {
|
|
45
|
+
throw new ProtocolError('loader_invalid', `${specifier} produced no load function`);
|
|
46
|
+
}
|
|
47
|
+
return seams;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runHost({ input, output, error, loader }) {
|
|
51
|
+
const decoder = new NdjsonDecoder();
|
|
52
|
+
const writer = new SerializedWriter(output);
|
|
53
|
+
const host = new PluginHost(writer, loader ?? {});
|
|
54
|
+
const running = new Set();
|
|
55
|
+
let fatal = null;
|
|
56
|
+
const track = (promise) => {
|
|
57
|
+
const tracked = promise.catch((cause) => { fatal ??= cause; }).finally(() => running.delete(tracked));
|
|
58
|
+
running.add(tracked);
|
|
59
|
+
};
|
|
60
|
+
// Draining rather than a single `Promise.all`: a dispatch that is still
|
|
61
|
+
// running can schedule another one, and the process must not exit under it.
|
|
62
|
+
const quiesce = async () => { while (running.size > 0) await Promise.all([...running]); };
|
|
63
|
+
try {
|
|
64
|
+
for await (const chunk of input) {
|
|
65
|
+
for (const frame of decoder.push(chunk)) {
|
|
66
|
+
if (fatal) throw fatal;
|
|
67
|
+
if (host.shuttingDown) throw new ProtocolError('frame_after_shutdown', 'frame received after shutdown');
|
|
68
|
+
track(host.accept(frame));
|
|
69
|
+
}
|
|
70
|
+
if (fatal) throw fatal;
|
|
71
|
+
if (host.shuttingDown) { await quiesce(); if (fatal) throw fatal; return 0; }
|
|
72
|
+
}
|
|
73
|
+
decoder.finish();
|
|
74
|
+
await quiesce();
|
|
75
|
+
if (fatal) throw fatal;
|
|
76
|
+
diagnostic(error, 'connection_lost', 'stdin reached EOF before successful shutdown');
|
|
77
|
+
return 3;
|
|
78
|
+
} catch (cause) {
|
|
79
|
+
// Terminals already owed still have to reach stdout: a caller waiting on
|
|
80
|
+
// one is not made whole by the host exiting with a diagnosis.
|
|
81
|
+
await quiesce();
|
|
82
|
+
const code = cause?.code ?? 'host_failure';
|
|
83
|
+
diagnostic(error, code, cause?.message ?? 'host failed');
|
|
84
|
+
return code === 'write_error' ? 4 : 2;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// A rejection nobody handled, said out loud instead of swallowed, and
|
|
89
|
+
/// attributed where the async chain reaches back to an entry.
|
|
90
|
+
///
|
|
91
|
+
/// Node does not stop this process for one, and nothing else looks either: a
|
|
92
|
+
/// plugin could start an async task, have it fail, and leave no trace anywhere
|
|
93
|
+
/// rebon reads.
|
|
94
|
+
///
|
|
95
|
+
/// A failed promise does not carry who created it, but `plugin/load` runs
|
|
96
|
+
/// inside a store that survives timers, microtasks and awaits, so this handler
|
|
97
|
+
/// reads the owner straight out of it. What escapes the chain — through an
|
|
98
|
+
/// `EventEmitter`, a native callback or a third-party library — is still
|
|
99
|
+
/// `unattributed`, which is the truthful answer and is kept distinct from a
|
|
100
|
+
/// wrong one.
|
|
101
|
+
///
|
|
102
|
+
/// It never exits. A stray rejection in one plugin must not take down a host
|
|
103
|
+
/// other plugins are being served from; a load still in flight is failed by its
|
|
104
|
+
/// own path instead, which is the caller's to run.
|
|
105
|
+
export function installRejectionDiagnostic(stream) {
|
|
106
|
+
process.on('unhandledRejection', (reason) => {
|
|
107
|
+
const first = String(reason?.stack ?? reason ?? '').split('\n')[0];
|
|
108
|
+
const owner = currentOwner();
|
|
109
|
+
if (owner === undefined) {
|
|
110
|
+
diagnostic(stream, 'unhandled_rejection', `unattributed: ${first}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// `loading` says which of the two this is. While a load is in flight the
|
|
114
|
+
// entry did not install correctly and nothing depends on it yet, so the
|
|
115
|
+
// load fails and the composition skips it with a reason. Afterwards the
|
|
116
|
+
// plugin stays ready: a load that already succeeded cannot be un-failed,
|
|
117
|
+
// and tearing down a plane other entries serve from would cost far more
|
|
118
|
+
// than the one bad promise.
|
|
119
|
+
if (owner.loading) {
|
|
120
|
+
owner.rejection = first;
|
|
121
|
+
diagnostic(stream, 'unhandled_rejection', `plugin=${owner.entry} during load: ${first}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
diagnostic(stream, 'unhandled_rejection', `plugin=${owner.entry}: ${first}`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
129
|
+
installRejectionDiagnostic(process.stderr);
|
|
130
|
+
const flag = process.argv.indexOf('--loader');
|
|
131
|
+
const specifier = flag >= 0 ? process.argv[flag + 1] : process.env.REBON_PLUGIN_LOADER;
|
|
132
|
+
let loader;
|
|
133
|
+
try {
|
|
134
|
+
loader = await resolveLoader(specifier, process.stderr);
|
|
135
|
+
} catch (cause) {
|
|
136
|
+
// A host that cannot build the loader rebon asked for must not fall back to
|
|
137
|
+
// the built-in one: it would load a different set of plugins than the one
|
|
138
|
+
// rebon believes it asked for, and say nothing.
|
|
139
|
+
diagnostic(process.stderr, cause?.code ?? 'loader_failure', cause?.message ?? 'loader failed');
|
|
140
|
+
process.exitCode = 2;
|
|
141
|
+
}
|
|
142
|
+
if (loader !== undefined) {
|
|
143
|
+
process.exitCode = await runHost({
|
|
144
|
+
input: process.stdin, output: process.stdout, error: process.stderr, loader,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
import { MAX_FRAME_BYTES, ProtocolError, validateEnvelope } from './protocol.mjs';
|
|
2
|
-
import { parseEnvelopeJson } from './json.mjs';
|
|
3
|
-
|
|
4
|
-
export class FramingError extends ProtocolError {}
|
|
5
|
-
|
|
6
|
-
export class NdjsonDecoder {
|
|
7
|
-
#segments = []; #byteCount = 0; #failed = false; #copiedBytes = 0;
|
|
8
|
-
constructor(maxFrameBytes = MAX_FRAME_BYTES) { this.maxFrameBytes = maxFrameBytes; }
|
|
9
|
-
get stats() { return Object.freeze({ retained_bytes: this.#byteCount, copied_bytes: this.#copiedBytes }); }
|
|
10
|
-
push(chunk) {
|
|
11
|
-
if (this.#failed) throw new FramingError('poisoned', 'decoder is poisoned');
|
|
12
|
-
if (!Buffer.isBuffer(chunk) && !(chunk instanceof Uint8Array)) return this.#fail('wrong_chunk', 'input chunk must be bytes');
|
|
13
|
-
const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
14
|
-
const frames = [];
|
|
15
|
-
let start = 0;
|
|
16
|
-
for (let index = 0; index < input.length; index++) {
|
|
17
|
-
if (input[index] !== 0x0a) continue;
|
|
18
|
-
this.#append(input.subarray(start, index));
|
|
19
|
-
let bytes = this.#join();
|
|
20
|
-
this.#segments = []; this.#byteCount = 0;
|
|
21
|
-
if (bytes.at(-1) === 0x0d) bytes = bytes.subarray(0, -1);
|
|
22
|
-
if (bytes.length > this.maxFrameBytes) return this.#fail('frame_too_large', `frame exceeds ${this.maxFrameBytes} bytes`);
|
|
23
|
-
if (bytes.length === 0) return this.#fail('empty_frame', 'NDJSON frame is empty');
|
|
24
|
-
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
25
|
-
return this.#fail('utf8_bom', 'UTF-8 BOM is not permitted');
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
29
|
-
frames.push(parseEnvelopeJson(text));
|
|
30
|
-
} catch (error) {
|
|
31
|
-
if (error instanceof ProtocolError) return this.#fail(error.code, error.message);
|
|
32
|
-
return this.#fail('invalid_utf8', 'frame is not valid UTF-8');
|
|
33
|
-
}
|
|
34
|
-
start = index + 1;
|
|
35
|
-
}
|
|
36
|
-
this.#append(input.subarray(start));
|
|
37
|
-
if (this.#byteCount > this.maxFrameBytes
|
|
38
|
-
&& !(this.#byteCount === this.maxFrameBytes + 1 && this.#lastByte() === 0x0d)) {
|
|
39
|
-
return this.#fail('frame_too_large', `frame exceeds ${this.maxFrameBytes} bytes`);
|
|
40
|
-
}
|
|
41
|
-
return frames;
|
|
42
|
-
}
|
|
43
|
-
finish() {
|
|
44
|
-
if (this.#failed) throw new FramingError('poisoned', 'decoder is poisoned');
|
|
45
|
-
if (this.#byteCount) return this.#fail('incomplete_frame', `EOF with unterminated ${this.#byteCount}-byte frame`);
|
|
46
|
-
}
|
|
47
|
-
#append(bytes) {
|
|
48
|
-
if (!bytes.length) return;
|
|
49
|
-
const copy = Buffer.from(bytes);
|
|
50
|
-
this.#segments.push(copy); this.#byteCount += copy.length; this.#copiedBytes += copy.length;
|
|
51
|
-
}
|
|
52
|
-
#join() {
|
|
53
|
-
if (this.#segments.length === 1) return this.#segments[0];
|
|
54
|
-
this.#copiedBytes += this.#byteCount;
|
|
55
|
-
return Buffer.concat(this.#segments, this.#byteCount);
|
|
56
|
-
}
|
|
57
|
-
#lastByte() { return this.#segments.at(-1)?.at(-1); }
|
|
58
|
-
#fail(code, message) { this.#failed = true; this.#segments = []; this.#byteCount = 0; throw new FramingError(code, message); }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function encodeFrame(value, maxFrameBytes = MAX_FRAME_BYTES) {
|
|
62
|
-
const normalized = validateEnvelope(value);
|
|
63
|
-
let text;
|
|
64
|
-
try { text = JSON.stringify(normalized); } catch { throw new FramingError('encode_error', 'envelope cannot be encoded'); }
|
|
65
|
-
const bytes = Buffer.from(text);
|
|
66
|
-
if (bytes.length > maxFrameBytes) throw new FramingError('frame_too_large', `frame exceeds ${maxFrameBytes} bytes`);
|
|
67
|
-
return Buffer.concat([bytes, Buffer.from('\n')]);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export class SerializedWriter {
|
|
71
|
-
#tail = Promise.resolve(); #error;
|
|
72
|
-
constructor(stream, maxFrameBytes = MAX_FRAME_BYTES) { this.stream = stream; this.maxFrameBytes = maxFrameBytes; }
|
|
73
|
-
send(value) {
|
|
74
|
-
if (this.#error) return Promise.reject(this.#error);
|
|
75
|
-
const bytes = encodeFrame(value, this.maxFrameBytes);
|
|
76
|
-
const operation = this.#tail.then(() => {
|
|
77
|
-
if (this.#error) throw this.#error;
|
|
78
|
-
return this.#write(bytes);
|
|
79
|
-
});
|
|
80
|
-
this.#tail = operation.catch((error) => { this.#error ??= error; });
|
|
81
|
-
return operation;
|
|
82
|
-
}
|
|
83
|
-
async flush() { await this.#tail; if (this.#error) throw this.#error; }
|
|
84
|
-
#write(bytes) {
|
|
85
|
-
return new Promise((resolve, reject) => {
|
|
86
|
-
let callbackDone = false; let accepted; let drainSeen = false; let settled = false;
|
|
87
|
-
const cleanup = () => { this.stream.removeListener('error', onError); this.stream.removeListener('drain', onDrain); };
|
|
88
|
-
const fail = (cause) => {
|
|
89
|
-
if (settled) return;
|
|
90
|
-
settled = true; cleanup();
|
|
91
|
-
const error = cause instanceof FramingError ? cause
|
|
92
|
-
: new FramingError('write_error', `protocol output failed: ${cause?.code ?? 'stream error'}`);
|
|
93
|
-
this.#error ??= error; reject(error);
|
|
94
|
-
};
|
|
95
|
-
const complete = () => {
|
|
96
|
-
if (settled || accepted === undefined || !callbackDone || (!accepted && !drainSeen)) return;
|
|
97
|
-
settled = true; cleanup(); resolve();
|
|
98
|
-
};
|
|
99
|
-
const onError = (error) => fail(error);
|
|
100
|
-
const onDrain = () => { drainSeen = true; complete(); };
|
|
101
|
-
this.stream.once('error', onError);
|
|
102
|
-
this.stream.once('drain', onDrain);
|
|
103
|
-
try {
|
|
104
|
-
accepted = this.stream.write(bytes, (error) => {
|
|
105
|
-
if (error) { setImmediate(() => fail(error)); return; }
|
|
106
|
-
callbackDone = true; complete();
|
|
107
|
-
});
|
|
108
|
-
if (accepted) this.stream.removeListener('drain', onDrain);
|
|
109
|
-
complete();
|
|
110
|
-
} catch (error) { fail(error); }
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
}
|
|
1
|
+
import { MAX_FRAME_BYTES, ProtocolError, validateEnvelope } from './protocol.mjs';
|
|
2
|
+
import { parseEnvelopeJson } from './json.mjs';
|
|
3
|
+
|
|
4
|
+
export class FramingError extends ProtocolError {}
|
|
5
|
+
|
|
6
|
+
export class NdjsonDecoder {
|
|
7
|
+
#segments = []; #byteCount = 0; #failed = false; #copiedBytes = 0;
|
|
8
|
+
constructor(maxFrameBytes = MAX_FRAME_BYTES) { this.maxFrameBytes = maxFrameBytes; }
|
|
9
|
+
get stats() { return Object.freeze({ retained_bytes: this.#byteCount, copied_bytes: this.#copiedBytes }); }
|
|
10
|
+
push(chunk) {
|
|
11
|
+
if (this.#failed) throw new FramingError('poisoned', 'decoder is poisoned');
|
|
12
|
+
if (!Buffer.isBuffer(chunk) && !(chunk instanceof Uint8Array)) return this.#fail('wrong_chunk', 'input chunk must be bytes');
|
|
13
|
+
const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
14
|
+
const frames = [];
|
|
15
|
+
let start = 0;
|
|
16
|
+
for (let index = 0; index < input.length; index++) {
|
|
17
|
+
if (input[index] !== 0x0a) continue;
|
|
18
|
+
this.#append(input.subarray(start, index));
|
|
19
|
+
let bytes = this.#join();
|
|
20
|
+
this.#segments = []; this.#byteCount = 0;
|
|
21
|
+
if (bytes.at(-1) === 0x0d) bytes = bytes.subarray(0, -1);
|
|
22
|
+
if (bytes.length > this.maxFrameBytes) return this.#fail('frame_too_large', `frame exceeds ${this.maxFrameBytes} bytes`);
|
|
23
|
+
if (bytes.length === 0) return this.#fail('empty_frame', 'NDJSON frame is empty');
|
|
24
|
+
if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
25
|
+
return this.#fail('utf8_bom', 'UTF-8 BOM is not permitted');
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
29
|
+
frames.push(parseEnvelopeJson(text));
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error instanceof ProtocolError) return this.#fail(error.code, error.message);
|
|
32
|
+
return this.#fail('invalid_utf8', 'frame is not valid UTF-8');
|
|
33
|
+
}
|
|
34
|
+
start = index + 1;
|
|
35
|
+
}
|
|
36
|
+
this.#append(input.subarray(start));
|
|
37
|
+
if (this.#byteCount > this.maxFrameBytes
|
|
38
|
+
&& !(this.#byteCount === this.maxFrameBytes + 1 && this.#lastByte() === 0x0d)) {
|
|
39
|
+
return this.#fail('frame_too_large', `frame exceeds ${this.maxFrameBytes} bytes`);
|
|
40
|
+
}
|
|
41
|
+
return frames;
|
|
42
|
+
}
|
|
43
|
+
finish() {
|
|
44
|
+
if (this.#failed) throw new FramingError('poisoned', 'decoder is poisoned');
|
|
45
|
+
if (this.#byteCount) return this.#fail('incomplete_frame', `EOF with unterminated ${this.#byteCount}-byte frame`);
|
|
46
|
+
}
|
|
47
|
+
#append(bytes) {
|
|
48
|
+
if (!bytes.length) return;
|
|
49
|
+
const copy = Buffer.from(bytes);
|
|
50
|
+
this.#segments.push(copy); this.#byteCount += copy.length; this.#copiedBytes += copy.length;
|
|
51
|
+
}
|
|
52
|
+
#join() {
|
|
53
|
+
if (this.#segments.length === 1) return this.#segments[0];
|
|
54
|
+
this.#copiedBytes += this.#byteCount;
|
|
55
|
+
return Buffer.concat(this.#segments, this.#byteCount);
|
|
56
|
+
}
|
|
57
|
+
#lastByte() { return this.#segments.at(-1)?.at(-1); }
|
|
58
|
+
#fail(code, message) { this.#failed = true; this.#segments = []; this.#byteCount = 0; throw new FramingError(code, message); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function encodeFrame(value, maxFrameBytes = MAX_FRAME_BYTES) {
|
|
62
|
+
const normalized = validateEnvelope(value);
|
|
63
|
+
let text;
|
|
64
|
+
try { text = JSON.stringify(normalized); } catch { throw new FramingError('encode_error', 'envelope cannot be encoded'); }
|
|
65
|
+
const bytes = Buffer.from(text);
|
|
66
|
+
if (bytes.length > maxFrameBytes) throw new FramingError('frame_too_large', `frame exceeds ${maxFrameBytes} bytes`);
|
|
67
|
+
return Buffer.concat([bytes, Buffer.from('\n')]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class SerializedWriter {
|
|
71
|
+
#tail = Promise.resolve(); #error;
|
|
72
|
+
constructor(stream, maxFrameBytes = MAX_FRAME_BYTES) { this.stream = stream; this.maxFrameBytes = maxFrameBytes; }
|
|
73
|
+
send(value) {
|
|
74
|
+
if (this.#error) return Promise.reject(this.#error);
|
|
75
|
+
const bytes = encodeFrame(value, this.maxFrameBytes);
|
|
76
|
+
const operation = this.#tail.then(() => {
|
|
77
|
+
if (this.#error) throw this.#error;
|
|
78
|
+
return this.#write(bytes);
|
|
79
|
+
});
|
|
80
|
+
this.#tail = operation.catch((error) => { this.#error ??= error; });
|
|
81
|
+
return operation;
|
|
82
|
+
}
|
|
83
|
+
async flush() { await this.#tail; if (this.#error) throw this.#error; }
|
|
84
|
+
#write(bytes) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
let callbackDone = false; let accepted; let drainSeen = false; let settled = false;
|
|
87
|
+
const cleanup = () => { this.stream.removeListener('error', onError); this.stream.removeListener('drain', onDrain); };
|
|
88
|
+
const fail = (cause) => {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
settled = true; cleanup();
|
|
91
|
+
const error = cause instanceof FramingError ? cause
|
|
92
|
+
: new FramingError('write_error', `protocol output failed: ${cause?.code ?? 'stream error'}`);
|
|
93
|
+
this.#error ??= error; reject(error);
|
|
94
|
+
};
|
|
95
|
+
const complete = () => {
|
|
96
|
+
if (settled || accepted === undefined || !callbackDone || (!accepted && !drainSeen)) return;
|
|
97
|
+
settled = true; cleanup(); resolve();
|
|
98
|
+
};
|
|
99
|
+
const onError = (error) => fail(error);
|
|
100
|
+
const onDrain = () => { drainSeen = true; complete(); };
|
|
101
|
+
this.stream.once('error', onError);
|
|
102
|
+
this.stream.once('drain', onDrain);
|
|
103
|
+
try {
|
|
104
|
+
accepted = this.stream.write(bytes, (error) => {
|
|
105
|
+
if (error) { setImmediate(() => fail(error)); return; }
|
|
106
|
+
callbackDone = true; complete();
|
|
107
|
+
});
|
|
108
|
+
if (accepted) this.stream.removeListener('drain', onDrain);
|
|
109
|
+
complete();
|
|
110
|
+
} catch (error) { fail(error); }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|