@quolu/lattice 0.52.4 → 0.53.1
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 +147 -147
- package/README.ja.md +355 -355
- package/README.md +258 -258
- package/bin/lattice-bridge-supervisor.mjs +58 -0
- package/bin/lattice-bridge.mjs +25 -0
- package/bin/lattice-hub.mjs +67 -0
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice-scripted-adapter.mjs +0 -0
- package/bin/lattice-scripted-worker.mjs +0 -0
- package/bin/lattice-work-order-adapter.mjs +0 -0
- package/bin/lattice.mjs +0 -0
- package/docs/bridge-setup.md +132 -132
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
- package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
- package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
- package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
- package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
- package/package.json +103 -103
- package/sensor/LICENSE +21 -21
- package/sensor/NOTICE +19 -19
- package/sensor/dist/bin/lattice-sensor.js +9 -9
- package/sensor/dist/db/index.js +24 -24
- package/sensor/dist/db/migrations.js +41 -41
- package/sensor/dist/db/queries.js +164 -164
- package/sensor/dist/db/schema.sql +205 -205
- package/sensor/dist/directory.js +5 -5
- package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/sensor/dist/mcp/liveness-watchdog.js +53 -53
- package/sensor/dist/mcp/server-instructions.js +95 -95
- package/sensor/package.json +56 -56
- package/src/bridge-cli.mjs +32 -7
- package/src/bridge-config.mjs +49 -6
- package/src/bridge-daemon.mjs +4 -1
- package/src/bridge-hub-heartbeat.mjs +170 -0
- package/src/bridge-hub-server.mjs +555 -0
- package/src/bridge-registrar.mjs +18 -0
- package/src/bridge-server.mjs +4 -1
- package/src/bridge-startup-folder.mjs +348 -0
- package/src/cli-help.mjs +4 -4
- package/src/todo-store.mjs +1 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows bridge persistence via the per-user Startup folder — the Windows
|
|
3
|
+
* counterpart to `bridge-launch-agent.mjs`'s macOS LaunchAgent, with the same
|
|
4
|
+
* public contract (`snapshot`/`install`/`disable`/`restore`) so `bridge-cli.mjs`
|
|
5
|
+
* can select between them by `process.platform` without changing its own flow.
|
|
6
|
+
*
|
|
7
|
+
* Windows has no per-user analogue of launchd's KeepAlive supervision, and
|
|
8
|
+
* Task Scheduler's `ONLOGON` trigger requires elevation this product's "no
|
|
9
|
+
* ritual beyond one-time setup" bar cannot spend (verified empirically: both
|
|
10
|
+
* `schtasks /Create /SC ONLOGON` and `Register-ScheduledTask -Trigger
|
|
11
|
+
* (New-ScheduledTaskTrigger -AtLogOn)` return access-denied under a normal,
|
|
12
|
+
* non-elevated user token). The Startup folder needs no elevation — writing
|
|
13
|
+
* into `%APPDATA%\...\Startup` is an ordinary per-user file operation — but it
|
|
14
|
+
* only *starts* something at logon; nothing supervises it afterward.
|
|
15
|
+
*
|
|
16
|
+
* `lattice-bridge-supervisor.mjs` supplies that supervision (spawn, wait for
|
|
17
|
+
* exit, restart) in plain JS rather than a batch GOTO loop: a loop's own
|
|
18
|
+
* process is awkward to track and kill reliably on Windows, while a Node
|
|
19
|
+
* process's pid is not. The Startup-folder `.vbs` launcher runs the
|
|
20
|
+
* supervisor hidden (`WindowStyle 0`) so no console window appears at logon
|
|
21
|
+
* or at any crash-restart — the same class of bug the Windows-console-
|
|
22
|
+
* avalanche P0 hotfix (`windowsHide`, 0.52.4) exists to avoid, here via a
|
|
23
|
+
* different mechanism since this process tree is spawned by Windows logon
|
|
24
|
+
* rather than by this codebase's own `child_process.spawn`. Stopping the
|
|
25
|
+
* whole tree (supervisor + whatever bridge child it currently owns) uses
|
|
26
|
+
* `taskkill /T /F /PID <supervisor pid>` — Windows's own recursive-kill,
|
|
27
|
+
* since a forcibly-terminated supervisor gets no chance to clean up its own
|
|
28
|
+
* child.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { execFile } from 'node:child_process';
|
|
32
|
+
import { randomBytes } from 'node:crypto';
|
|
33
|
+
import { constants as fsConstants } from 'node:fs';
|
|
34
|
+
import {
|
|
35
|
+
lstat, mkdir, open, readFile, realpath, rename, rm, writeFile,
|
|
36
|
+
} from 'node:fs/promises';
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
import { promisify } from 'node:util';
|
|
39
|
+
|
|
40
|
+
import { BridgeConfigError, readBridgeConfig } from './bridge-config.mjs';
|
|
41
|
+
import { readBridgeDaemonDescriptor } from './bridge-daemon.mjs';
|
|
42
|
+
import { bridgeRegistrarSettings } from './bridge-registrar.mjs';
|
|
43
|
+
|
|
44
|
+
export const BRIDGE_STARTUP_LABEL = 'LatticeBridge';
|
|
45
|
+
const DESCRIPTOR_SCHEMA = 'lattice.bridge_supervisor_descriptor.v1';
|
|
46
|
+
const START_TIMEOUT_MS = 5_000;
|
|
47
|
+
const STOP_TIMEOUT_MS = 3_000;
|
|
48
|
+
const execFileAsync = promisify(execFile);
|
|
49
|
+
|
|
50
|
+
function fail(code, message, cause = undefined) {
|
|
51
|
+
return new BridgeConfigError(code, message, undefined, cause);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function bridgeStartupFolderPaths(env = process.env) {
|
|
55
|
+
const appData = env.APPDATA;
|
|
56
|
+
const localAppData = env.LOCALAPPDATA;
|
|
57
|
+
if (typeof appData !== 'string' || !path.isAbsolute(appData)) {
|
|
58
|
+
throw fail('BRIDGE_STARTUP_FOLDER_APPDATA_INVALID', 'APPDATA must be an absolute path');
|
|
59
|
+
}
|
|
60
|
+
if (typeof localAppData !== 'string' || !path.isAbsolute(localAppData)) {
|
|
61
|
+
throw fail('BRIDGE_STARTUP_FOLDER_APPDATA_INVALID', 'LOCALAPPDATA must be an absolute path');
|
|
62
|
+
}
|
|
63
|
+
// The launcher must live in the Startup folder — Windows only runs what it
|
|
64
|
+
// finds there. Everything it launches lives in our own runtime directory
|
|
65
|
+
// instead: Startup-folder contents are conventionally opaque shortcuts, and
|
|
66
|
+
// keeping the real state (descriptor, pidfile) in a folder this module
|
|
67
|
+
// fully owns keeps the safety checks below meaningful.
|
|
68
|
+
const startupDirectory = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
69
|
+
const runtimeDirectory = path.join(localAppData, 'Lattice', 'bridge-startup');
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
startupDirectory, runtimeDirectory,
|
|
72
|
+
launcher: path.join(startupDirectory, `${BRIDGE_STARTUP_LABEL}.vbs`),
|
|
73
|
+
descriptor: path.join(runtimeDirectory, 'descriptor.json'),
|
|
74
|
+
pidfile: path.join(runtimeDirectory, 'supervisor.pid'),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function prepareDirectory(directory) {
|
|
79
|
+
await mkdir(directory, { recursive: true });
|
|
80
|
+
const stats = await lstat(directory);
|
|
81
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
82
|
+
throw fail('BRIDGE_STARTUP_FOLDER_DIR_UNSAFE', 'startup folder path is unsafe');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function strictFile(ref, maxBytes = 65_536) {
|
|
87
|
+
let before;
|
|
88
|
+
let handle;
|
|
89
|
+
try {
|
|
90
|
+
before = await lstat(ref);
|
|
91
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > maxBytes) {
|
|
92
|
+
throw new Error('unsafe startup file');
|
|
93
|
+
}
|
|
94
|
+
handle = await open(ref, fsConstants.O_RDONLY);
|
|
95
|
+
const opened = await handle.stat();
|
|
96
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino
|
|
97
|
+
|| opened.size !== before.size) {
|
|
98
|
+
throw new Error('startup file changed during validation');
|
|
99
|
+
}
|
|
100
|
+
const content = await handle.readFile('utf8');
|
|
101
|
+
const after = await lstat(ref);
|
|
102
|
+
if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size) {
|
|
103
|
+
throw new Error('startup file changed during read');
|
|
104
|
+
}
|
|
105
|
+
return content;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error?.code === 'ENOENT' && before === undefined) return null;
|
|
108
|
+
throw fail('BRIDGE_STARTUP_FOLDER_FILE_UNSAFE', 'bridge startup file is unsafe', error);
|
|
109
|
+
} finally {
|
|
110
|
+
await handle?.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function atomicFile(ref, content) {
|
|
115
|
+
const temporary = `${ref}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
116
|
+
try {
|
|
117
|
+
await writeFile(temporary, content, { encoding: 'utf8', flag: 'wx' });
|
|
118
|
+
await rename(temporary, ref);
|
|
119
|
+
} finally {
|
|
120
|
+
await rm(temporary, { force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function executablePath(ref, label) {
|
|
125
|
+
if (typeof ref !== 'string' || !path.isAbsolute(ref)) {
|
|
126
|
+
throw fail('BRIDGE_STARTUP_FOLDER_EXECUTABLE_INVALID', `${label} path must be absolute`);
|
|
127
|
+
}
|
|
128
|
+
let resolved;
|
|
129
|
+
let stats;
|
|
130
|
+
try {
|
|
131
|
+
resolved = await realpath(ref);
|
|
132
|
+
stats = await lstat(resolved);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw fail('BRIDGE_STARTUP_FOLDER_EXECUTABLE_INVALID', `${label} is unavailable`, error);
|
|
135
|
+
}
|
|
136
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
137
|
+
throw fail('BRIDGE_STARTUP_FOLDER_EXECUTABLE_INVALID', `${label} is unsafe`);
|
|
138
|
+
}
|
|
139
|
+
return resolved;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** VBS escapes an embedded `"` by doubling it. Paths we generate never
|
|
143
|
+
* contain one (`executablePath`/our own runtime dir), so this only guards
|
|
144
|
+
* against that invariant silently breaking rather than mis-escaping. */
|
|
145
|
+
function vbsQuoted(label, value) {
|
|
146
|
+
if (typeof value !== 'string' || value.length === 0 || value.includes('"')) {
|
|
147
|
+
throw fail('BRIDGE_STARTUP_FOLDER_VALUE_UNSAFE', `${label} is unsafe to embed in the startup launcher`);
|
|
148
|
+
}
|
|
149
|
+
return `"""${value}"""`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function launcherScript({ nodePath, supervisorPath, descriptorPath }) {
|
|
153
|
+
// WScript.Shell.Run(command, windowStyle, waitOnReturn). windowStyle 0 =
|
|
154
|
+
// hidden, waitOnReturn False = fire-and-forget (the supervisor outlives
|
|
155
|
+
// wscript.exe, which exits right after this call).
|
|
156
|
+
const command = [vbsQuoted('node executable', nodePath), vbsQuoted('supervisor script', supervisorPath),
|
|
157
|
+
vbsQuoted('descriptor path', descriptorPath)].join(' & " " & ');
|
|
158
|
+
return `Set shell = CreateObject("WScript.Shell")\r\nshell.Run ${command}, 0, False\r\n`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function supervisorDescriptor({ bridgePath, pidfile, instanceToken, env }) {
|
|
162
|
+
const forwarded = { LATTICE_BRIDGE_INSTANCE_TOKEN: instanceToken };
|
|
163
|
+
if (env.LATTICE_CONFIG_DIR !== undefined) {
|
|
164
|
+
if (typeof env.LATTICE_CONFIG_DIR !== 'string' || !path.isAbsolute(env.LATTICE_CONFIG_DIR)) {
|
|
165
|
+
throw fail('BRIDGE_CONFIG_DIR_INVALID', 'LATTICE_CONFIG_DIR must be absolute');
|
|
166
|
+
}
|
|
167
|
+
forwarded.LATTICE_CONFIG_DIR = env.LATTICE_CONFIG_DIR;
|
|
168
|
+
}
|
|
169
|
+
// Same rationale as the LaunchAgent plist: nothing here inherits the
|
|
170
|
+
// installer's shell environment at restart time, so registrar settings must
|
|
171
|
+
// be baked in or self-registration silently never fires after a crash restart.
|
|
172
|
+
const registrar = bridgeRegistrarSettings(env);
|
|
173
|
+
if (registrar !== null) {
|
|
174
|
+
forwarded.LATTICE_BRIDGE_REGISTRAR_SSH_HOST = registrar.host;
|
|
175
|
+
forwarded.LATTICE_BRIDGE_REGISTRAR_SCRIPT = registrar.script;
|
|
176
|
+
}
|
|
177
|
+
return JSON.stringify({ schema: DESCRIPTOR_SCHEMA, bridgePath, pidPath: pidfile, env: forwarded });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function defaultStartupRunner(args) {
|
|
181
|
+
try {
|
|
182
|
+
const result = await execFileAsync(args[0], args.slice(1), { encoding: 'utf8', windowsHide: true });
|
|
183
|
+
return { code: 0, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (Number.isInteger(error?.code)) {
|
|
186
|
+
return { code: error.code, stdout: error.stdout ?? '', stderr: error.stderr ?? '' };
|
|
187
|
+
}
|
|
188
|
+
throw fail('BRIDGE_STARTUP_LAUNCHER_UNAVAILABLE', 'the startup launcher could not be executed', error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function healthHost(address) {
|
|
193
|
+
if (address === '0.0.0.0') return '127.0.0.1';
|
|
194
|
+
if (address === '::') return '[::1]';
|
|
195
|
+
return address.includes(':') ? `[${address}]` : address;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function defaultWaitReady({ config, instanceToken, env, timeoutMs = START_TIMEOUT_MS }) {
|
|
199
|
+
const deadline = Date.now() + timeoutMs;
|
|
200
|
+
while (Date.now() < deadline) {
|
|
201
|
+
const descriptor = await readBridgeDaemonDescriptor({ env });
|
|
202
|
+
if (descriptor?.address === config.listen.address && descriptor?.port === config.listen.port
|
|
203
|
+
&& descriptor?.config_updated_at === config.updated_at) {
|
|
204
|
+
try {
|
|
205
|
+
const response = await fetch(
|
|
206
|
+
`http://${healthHost(descriptor.address)}:${descriptor.port}/__lattice/bridge-health`, {
|
|
207
|
+
headers: { 'x-lattice-bridge-instance-token': instanceToken },
|
|
208
|
+
signal: AbortSignal.timeout(400),
|
|
209
|
+
});
|
|
210
|
+
const body = response.status === 200 ? await response.json() : null;
|
|
211
|
+
if (body?.schema === 'lattice.bridge_health.v1' && body.pid === descriptor.pid
|
|
212
|
+
&& body.updated_at === config.updated_at) return descriptor;
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
216
|
+
}
|
|
217
|
+
throw fail('BRIDGE_STARTUP_FOLDER_START_FAILED', 'bridge startup process did not become healthy');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function defaultWaitStopped({ listen, timeoutMs = STOP_TIMEOUT_MS }) {
|
|
221
|
+
if (listen === null) return;
|
|
222
|
+
const deadline = Date.now() + timeoutMs;
|
|
223
|
+
while (Date.now() < deadline) {
|
|
224
|
+
try {
|
|
225
|
+
await fetch(`http://${healthHost(listen.address)}:${listen.port}/__lattice/bridge-health`,
|
|
226
|
+
{ signal: AbortSignal.timeout(300) });
|
|
227
|
+
} catch { return; }
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
229
|
+
}
|
|
230
|
+
throw fail('BRIDGE_STARTUP_FOLDER_STOP_FAILED', 'bridge startup process socket did not stop');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function snapshotBridgeStartupFolder({ env = process.env } = {}) {
|
|
234
|
+
const refs = bridgeStartupFolderPaths(env);
|
|
235
|
+
await prepareDirectory(refs.startupDirectory);
|
|
236
|
+
await prepareDirectory(refs.runtimeDirectory);
|
|
237
|
+
const launcherContent = await strictFile(refs.launcher);
|
|
238
|
+
const descriptorContent = await strictFile(refs.descriptor);
|
|
239
|
+
if ((launcherContent === null) !== (descriptorContent === null)) {
|
|
240
|
+
throw fail('BRIDGE_STARTUP_FOLDER_STATE_INVALID', 'startup launcher and descriptor disagree on installed state');
|
|
241
|
+
}
|
|
242
|
+
return Object.freeze({ installed: launcherContent !== null, launcherContent, descriptorContent });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Read the supervisor's own recorded pid and kill its whole process tree
|
|
246
|
+
* (`taskkill /T /F`) — a forcibly-terminated supervisor cannot clean up its
|
|
247
|
+
* child itself, so the tree kill is what actually stops the bridge, not the
|
|
248
|
+
* SIGTERM handler `lattice-bridge.mjs` relies on when Node manages it directly. */
|
|
249
|
+
async function stopRunning({ env, listen, runner, waitStopped }) {
|
|
250
|
+
const refs = bridgeStartupFolderPaths(env);
|
|
251
|
+
let pidText;
|
|
252
|
+
try { pidText = await readFile(refs.pidfile, 'utf8'); } catch (error) {
|
|
253
|
+
if (error?.code === 'ENOENT') return;
|
|
254
|
+
throw fail('BRIDGE_STARTUP_FOLDER_STOP_FAILED', 'could not read supervisor pidfile', error);
|
|
255
|
+
}
|
|
256
|
+
const pid = Number(pidText.trim());
|
|
257
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return;
|
|
258
|
+
const result = await runner(['taskkill.exe', '/T', '/F', '/PID', String(pid)]);
|
|
259
|
+
// taskkill exits non-zero (128) when the target is already gone — not a failure to report.
|
|
260
|
+
if (result.code !== 0 && !/not found|not running/iu.test(result.stderr ?? '')) {
|
|
261
|
+
throw fail('BRIDGE_STARTUP_FOLDER_STOP_FAILED', 'could not stop bridge supervisor process tree');
|
|
262
|
+
}
|
|
263
|
+
await waitStopped({ listen, env });
|
|
264
|
+
await rm(refs.pidfile, { force: true });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function installBridgeStartupFolder({ config, env = process.env,
|
|
268
|
+
runner = defaultStartupRunner, nodePath = process.execPath,
|
|
269
|
+
bridgePath = path.resolve(import.meta.dirname, '../bin/lattice-bridge.mjs'),
|
|
270
|
+
supervisorPath = path.resolve(import.meta.dirname, '../bin/lattice-bridge-supervisor.mjs'),
|
|
271
|
+
waitReady = defaultWaitReady, waitStopped = defaultWaitStopped,
|
|
272
|
+
previousListen = null } = {}) {
|
|
273
|
+
if (config?.enabled !== true) throw fail('BRIDGE_DISABLED', 'bridge is disabled');
|
|
274
|
+
const refs = bridgeStartupFolderPaths(env);
|
|
275
|
+
await prepareDirectory(refs.startupDirectory);
|
|
276
|
+
await prepareDirectory(refs.runtimeDirectory);
|
|
277
|
+
await strictFile(refs.launcher);
|
|
278
|
+
await strictFile(refs.descriptor);
|
|
279
|
+
const resolvedNode = await executablePath(nodePath, 'node executable');
|
|
280
|
+
const resolvedBridge = await executablePath(bridgePath, 'bridge executable');
|
|
281
|
+
const resolvedSupervisor = await executablePath(supervisorPath, 'supervisor executable');
|
|
282
|
+
const instanceToken = randomBytes(32).toString('hex');
|
|
283
|
+
const descriptorContent = supervisorDescriptor({
|
|
284
|
+
bridgePath: resolvedBridge, pidfile: refs.pidfile, instanceToken, env,
|
|
285
|
+
});
|
|
286
|
+
await stopRunning({ env, listen: previousListen, runner, waitStopped });
|
|
287
|
+
await atomicFile(refs.descriptor, descriptorContent);
|
|
288
|
+
await atomicFile(refs.launcher,
|
|
289
|
+
launcherScript({ nodePath: resolvedNode, supervisorPath: resolvedSupervisor, descriptorPath: refs.descriptor }));
|
|
290
|
+
await launch(runner, ['wscript.exe', refs.launcher], 'BRIDGE_STARTUP_LAUNCHER_FAILED',
|
|
291
|
+
'could not start the bridge startup process');
|
|
292
|
+
return waitReady({ config, instanceToken, env });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function launch(runner, args, code, message) {
|
|
296
|
+
let result;
|
|
297
|
+
try { result = await runner(args); } catch (error) {
|
|
298
|
+
if (error instanceof BridgeConfigError) throw error;
|
|
299
|
+
throw fail(code, message, error);
|
|
300
|
+
}
|
|
301
|
+
if (!result || result.code !== 0) throw fail(code, message);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function disableBridgeStartupFolder({ snapshot, listen, env = process.env,
|
|
306
|
+
runner = defaultStartupRunner, waitStopped = defaultWaitStopped } = {}) {
|
|
307
|
+
if (!snapshot || typeof snapshot.installed !== 'boolean') {
|
|
308
|
+
throw new TypeError('bridge startup folder snapshot required');
|
|
309
|
+
}
|
|
310
|
+
const refs = bridgeStartupFolderPaths(env);
|
|
311
|
+
const stopped = snapshot.installed;
|
|
312
|
+
if (stopped) await stopRunning({ env, listen, runner, waitStopped });
|
|
313
|
+
await rm(refs.launcher, { force: true });
|
|
314
|
+
await rm(refs.descriptor, { force: true });
|
|
315
|
+
return { removed: snapshot.installed, stopped };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function restoreBridgeStartupFolder({ snapshot, listen = null, env = process.env,
|
|
319
|
+
runner = defaultStartupRunner, waitStopped = defaultWaitStopped,
|
|
320
|
+
config = undefined, waitReady = defaultWaitReady } = {}) {
|
|
321
|
+
if (!snapshot || typeof snapshot.installed !== 'boolean'
|
|
322
|
+
|| (snapshot.installed
|
|
323
|
+
&& (typeof snapshot.launcherContent !== 'string' || typeof snapshot.descriptorContent !== 'string'))) {
|
|
324
|
+
throw new TypeError('bridge startup folder snapshot required');
|
|
325
|
+
}
|
|
326
|
+
const refs = bridgeStartupFolderPaths(env);
|
|
327
|
+
await prepareDirectory(refs.startupDirectory);
|
|
328
|
+
await prepareDirectory(refs.runtimeDirectory);
|
|
329
|
+
await stopRunning({ env, listen, runner, waitStopped });
|
|
330
|
+
if (snapshot.installed) {
|
|
331
|
+
await atomicFile(refs.descriptor, snapshot.descriptorContent);
|
|
332
|
+
await atomicFile(refs.launcher, snapshot.launcherContent);
|
|
333
|
+
await launch(runner, ['wscript.exe', refs.launcher], 'BRIDGE_STARTUP_ROLLBACK_FAILED',
|
|
334
|
+
'could not restore the bridge startup process');
|
|
335
|
+
const restoredConfig = config ?? await readBridgeConfig({ env });
|
|
336
|
+
let tokenMatch = null;
|
|
337
|
+
try { tokenMatch = JSON.parse(snapshot.descriptorContent).env?.LATTICE_BRIDGE_INSTANCE_TOKEN ?? null; }
|
|
338
|
+
catch { tokenMatch = null; }
|
|
339
|
+
if (restoredConfig?.enabled !== true || typeof tokenMatch !== 'string' || !/^[0-9a-f]{64}$/u.test(tokenMatch)) {
|
|
340
|
+
throw fail('BRIDGE_STARTUP_ROLLBACK_FAILED', 'restored bridge startup process is not attestable');
|
|
341
|
+
}
|
|
342
|
+
await waitReady({ config: restoredConfig, instanceToken: tokenMatch, env });
|
|
343
|
+
} else {
|
|
344
|
+
await rm(refs.launcher, { force: true });
|
|
345
|
+
await rm(refs.descriptor, { force: true });
|
|
346
|
+
}
|
|
347
|
+
return snapshot;
|
|
348
|
+
}
|
package/src/cli-help.mjs
CHANGED
|
@@ -169,8 +169,8 @@ Commands:
|
|
|
169
169
|
bridge: `Usage: lattice bridge <command> [options] --json
|
|
170
170
|
|
|
171
171
|
Commands:
|
|
172
|
-
setup --listen <IP> [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--allow-host <host>...]
|
|
173
|
-
reconfigure [--listen <IP>] [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--allow-host <host>...]
|
|
172
|
+
setup --listen <IP> [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--hub <URL>|none] [--allow-host <host>...]
|
|
173
|
+
reconfigure [--listen <IP>] [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--hub <URL>|none] [--allow-host <host>...]
|
|
174
174
|
status
|
|
175
175
|
disable
|
|
176
176
|
register # 現在のlisten portをreverse proxy hostへ自己登録する
|
|
@@ -266,8 +266,8 @@ const SUBCOMMAND_USAGE = Object.freeze({
|
|
|
266
266
|
'runtime-errors resolve': 'runtime-errors resolve <fingerprint> --json',
|
|
267
267
|
'runtime-errors reopen': 'runtime-errors reopen <fingerprint> --json',
|
|
268
268
|
'runtime-errors compact': 'runtime-errors compact --json',
|
|
269
|
-
'bridge setup': 'bridge setup --listen <IP> [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--allow-host <host>...] --json',
|
|
270
|
-
'bridge reconfigure': 'bridge reconfigure [--listen <IP>] [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--allow-host <host>...] --json',
|
|
269
|
+
'bridge setup': 'bridge setup --listen <IP> [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--hub <URL>|none] [--allow-host <host>...] --json',
|
|
270
|
+
'bridge reconfigure': 'bridge reconfigure [--listen <IP>] [--port <49152..65535|auto>] [--dashboard|--upstream <URL>] [--hub <URL>|none] [--allow-host <host>...] --json',
|
|
271
271
|
'bridge status': 'bridge status --json',
|
|
272
272
|
'bridge disable': 'bridge disable --json',
|
|
273
273
|
'bridge register': 'bridge register --json',
|
package/src/todo-store.mjs
CHANGED
|
@@ -1063,7 +1063,7 @@ const EVIDENCE_BLOB_CACHE_LIMIT = 512;
|
|
|
1063
1063
|
const evidenceBlobCache = new Map();
|
|
1064
1064
|
|
|
1065
1065
|
function readEvidenceBlob(absoluteRepo, oid) {
|
|
1066
|
-
const key = `${absoluteRepo}
|
|
1066
|
+
const key = `${absoluteRepo}\0${oid}`;
|
|
1067
1067
|
const cached = evidenceBlobCache.get(key);
|
|
1068
1068
|
if (cached !== undefined) return cached;
|
|
1069
1069
|
const [entry] = gitCatFileBatch([oid], {
|