@quolu/lattice 0.52.0 → 0.52.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.
Files changed (182) hide show
  1. package/LICENSE +147 -147
  2. package/README.ja.md +355 -355
  3. package/README.md +258 -258
  4. package/bin/lattice-mcp.mjs +0 -0
  5. package/bin/lattice-scripted-adapter.mjs +0 -0
  6. package/bin/lattice-scripted-worker.mjs +0 -0
  7. package/bin/lattice-work-order-adapter.mjs +0 -0
  8. package/bin/lattice.mjs +0 -0
  9. package/docs/bridge-setup.md +132 -132
  10. package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
  11. package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
  12. package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
  13. package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
  14. package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
  15. package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
  16. package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
  17. package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
  18. package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
  19. package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
  20. package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
  21. package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
  22. package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
  23. package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
  24. package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
  25. package/package.json +103 -103
  26. package/sensor/LICENSE +21 -21
  27. package/sensor/NOTICE +19 -19
  28. package/sensor/dist/bin/lattice-sensor.js +9 -9
  29. package/sensor/dist/db/index.js +24 -24
  30. package/sensor/dist/db/migrations.js +41 -41
  31. package/sensor/dist/db/queries.js +164 -164
  32. package/sensor/dist/db/schema.sql +205 -205
  33. package/sensor/dist/directory.js +5 -5
  34. package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  35. package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  36. package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  37. package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  38. package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  39. package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  40. package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
  41. package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
  42. package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
  43. package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  44. package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  45. package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
  46. package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
  47. package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
  48. package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  49. package/sensor/dist/mcp/liveness-watchdog.js +53 -53
  50. package/sensor/dist/mcp/server-instructions.js +95 -95
  51. package/sensor/package.json +56 -56
  52. package/src/artifact-contracts-v2.mjs +325 -325
  53. package/src/artifact-contracts.mjs +895 -895
  54. package/src/boundary-compiler.mjs +712 -712
  55. package/src/boundary-observation-compiler-v2.mjs +344 -344
  56. package/src/bounded-seam.mjs +230 -230
  57. package/src/bridge-address.mjs +107 -107
  58. package/src/bridge-cli.mjs +297 -297
  59. package/src/bridge-config.mjs +346 -346
  60. package/src/bridge-daemon.mjs +378 -378
  61. package/src/bridge-launch-agent.mjs +323 -323
  62. package/src/bridge-registrar.mjs +102 -102
  63. package/src/bridge-server.mjs +376 -376
  64. package/src/cli-help.mjs +308 -308
  65. package/src/cli-stdio.mjs +40 -40
  66. package/src/control-compiler.mjs +532 -532
  67. package/src/dag-chain.mjs +261 -261
  68. package/src/factory-diagnostics.mjs +188 -188
  69. package/src/hash-chain.mjs +76 -76
  70. package/src/hooks-cli.mjs +1057 -1053
  71. package/src/isolation-runner.mjs +409 -409
  72. package/src/node-version-guard.mjs +44 -44
  73. package/src/project-cli.mjs +697 -697
  74. package/src/project-identity.mjs +130 -130
  75. package/src/rc1-black-box-oracle.mjs +906 -906
  76. package/src/rc1-comparison.mjs +154 -154
  77. package/src/rc1-evidence-bundle.mjs +456 -456
  78. package/src/rc1-v4-campaign.mjs +708 -708
  79. package/src/rc1-v4-transform.mjs +467 -467
  80. package/src/rc1-v5-artifact-set.mjs +855 -855
  81. package/src/rc1-v5-behavior-evidence.mjs +594 -594
  82. package/src/rc1-v5-campaign.mjs +797 -797
  83. package/src/rc1-v5-transform.mjs +421 -421
  84. package/src/rc1-v6-artifact-set.mjs +807 -807
  85. package/src/rc1-v6-behavior-evidence.mjs +273 -273
  86. package/src/rc1-v6-campaign.mjs +623 -623
  87. package/src/rc1-v6-causal-binding.mjs +473 -473
  88. package/src/rc1-v6-measurement.mjs +313 -313
  89. package/src/rc2-artifact-set.mjs +1584 -1584
  90. package/src/rc2-campaign.mjs +1506 -1502
  91. package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
  92. package/src/rc2-delivery-policy-oracle.mjs +134 -134
  93. package/src/rc2-delivery-policy-transform.mjs +1127 -1127
  94. package/src/rc2-rc1-transfer-front-end.mjs +511 -511
  95. package/src/rc3-actual-dogfood.mjs +652 -652
  96. package/src/rc3-dogfood-scaffold.mjs +315 -315
  97. package/src/rc3-scripted-campaign.mjs +1383 -1383
  98. package/src/rc4-stage1-dogfood.mjs +673 -673
  99. package/src/runtime-adapter-registry.mjs +524 -520
  100. package/src/runtime-cli.mjs +4588 -4584
  101. package/src/runtime-contracts.mjs +824 -824
  102. package/src/runtime-control-store.mjs +604 -600
  103. package/src/runtime-controller-protocol.mjs +587 -587
  104. package/src/runtime-decision-verifier.mjs +701 -701
  105. package/src/runtime-diff-observer.mjs +361 -361
  106. package/src/runtime-direct-os-observer.mjs +301 -301
  107. package/src/runtime-driver-state.mjs +166 -162
  108. package/src/runtime-engine.mjs +779 -779
  109. package/src/runtime-errors.mjs +356 -356
  110. package/src/runtime-event-store.mjs +189 -189
  111. package/src/runtime-front-end.mjs +925 -925
  112. package/src/runtime-gate-store.mjs +481 -477
  113. package/src/runtime-hold-recompile.mjs +916 -916
  114. package/src/runtime-io-sentinel.mjs +391 -391
  115. package/src/runtime-lifecycle-lock.mjs +294 -290
  116. package/src/runtime-managed-supervisor.mjs +1490 -1490
  117. package/src/runtime-multi-epoch-store.mjs +838 -834
  118. package/src/runtime-projection.mjs +269 -269
  119. package/src/runtime-pull-intake.mjs +1192 -1188
  120. package/src/runtime-scripted-adapter-controller.mjs +1160 -1156
  121. package/src/runtime-scripted-executor.mjs +163 -163
  122. package/src/runtime-scripted-worktree.mjs +104 -104
  123. package/src/runtime-seam-resolve.mjs +428 -428
  124. package/src/runtime-seam-treatment.mjs +173 -173
  125. package/src/runtime-socket-owner.mjs +125 -125
  126. package/src/runtime-work-order-contracts.mjs +91 -91
  127. package/src/runtime-work-order-controller.mjs +1171 -1167
  128. package/src/runtime-worktree-executor.mjs +199 -199
  129. package/src/schedulability-compiler-v2.mjs +303 -303
  130. package/src/schedulability-verifier-v2.mjs +317 -317
  131. package/src/seam-apply.mjs +549 -549
  132. package/src/seam-commit-shared.mjs +22 -22
  133. package/src/seam-commit-transform.mjs +81 -81
  134. package/src/seam-commit.mjs +18 -18
  135. package/src/seam-cost.mjs +322 -322
  136. package/src/seam-derivation.mjs +188 -188
  137. package/src/seam-gate.mjs +146 -146
  138. package/src/seam-proposal-contracts.mjs +446 -446
  139. package/src/seam-proposal-queries.mjs +521 -521
  140. package/src/seam-proposal.mjs +2011 -2011
  141. package/src/seam-ref.mjs +33 -33
  142. package/src/seam-rewrite.mjs +286 -286
  143. package/src/seam-transform.mjs +554 -554
  144. package/src/seam-verification.mjs +260 -260
  145. package/src/sensor-adapter.mjs +432 -432
  146. package/src/sensor-cli.mjs +139 -139
  147. package/src/sensor-diff.mjs +661 -661
  148. package/src/sensor-node-runtime.mjs +53 -53
  149. package/src/sensor-runtime.mjs +52 -52
  150. package/src/timestamp-contract.mjs +8 -8
  151. package/src/todo-audit-pending.mjs +91 -91
  152. package/src/todo-chain.mjs +178 -178
  153. package/src/todo-cli.mjs +3141 -3126
  154. package/src/todo-contracts.mjs +728 -728
  155. package/src/todo-dashboard-registry.mjs +573 -573
  156. package/src/todo-dispatch-shape.mjs +190 -190
  157. package/src/todo-gantt-html-independence.mjs +239 -239
  158. package/src/todo-gantt-html-shared.mjs +226 -226
  159. package/src/todo-gantt-html-style.mjs +131 -131
  160. package/src/todo-gantt-html.mjs +248 -248
  161. package/src/todo-gantt-layout.mjs +974 -974
  162. package/src/todo-gantt-live.mjs +361 -361
  163. package/src/todo-gantt-nested.mjs +263 -263
  164. package/src/todo-gantt-presentation.mjs +217 -217
  165. package/src/todo-gantt-scope.mjs +123 -123
  166. package/src/todo-gantt-svg.mjs +353 -353
  167. package/src/todo-independence-contracts.mjs +595 -595
  168. package/src/todo-independence-guidance.mjs +322 -322
  169. package/src/todo-independence.mjs +640 -640
  170. package/src/todo-markdown-renderer.mjs +260 -260
  171. package/src/todo-migration.mjs +448 -448
  172. package/src/todo-narrative-anchor.mjs +130 -130
  173. package/src/todo-note-store.mjs +629 -625
  174. package/src/todo-parallel-candidates.mjs +114 -114
  175. package/src/todo-revision.mjs +995 -995
  176. package/src/todo-split.mjs +472 -472
  177. package/src/todo-status.mjs +690 -690
  178. package/src/todo-store-git-transaction.mjs +418 -418
  179. package/src/todo-store.mjs +4322 -4314
  180. package/src/treatment-compiler.mjs +728 -728
  181. package/src/treatment-runner.mjs +656 -656
  182. package/src/witness-scaffold.mjs +180 -180
@@ -1,297 +1,297 @@
1
- import { createConnection, isIP } from 'node:net';
2
- import { networkInterfaces } from 'node:os';
3
- import * as clack from '@clack/prompts';
4
-
5
- import { resolveBridgeListenAddress } from './bridge-address.mjs';
6
- import { registerBridgeUpstream } from './bridge-registrar.mjs';
7
- import {
8
- BridgeConfigError, configureBridge, disableBridge, readBridgeConfig, restoreBridgeConfig,
9
- normalizeBridgeAllowedHost, withBridgeOperationLock,
10
- } from './bridge-config.mjs';
11
- import {
12
- clearBridgeStopControl, ensureBridgeDaemon, readBridgeDaemonDescriptor,
13
- removeBridgeDaemonActiveMarker, removeBridgeDaemonDescriptor, requestBridgeDaemonStop,
14
- stopBridgeDaemon,
15
- } from './bridge-daemon.mjs';
16
- import {
17
- disableBridgeLaunchAgent, installBridgeLaunchAgent, restoreBridgeLaunchAgent,
18
- snapshotBridgeLaunchAgent,
19
- } from './bridge-launch-agent.mjs';
20
-
21
- // v2 adds the liveness fields. `enabled` only says the configuration is on;
22
- // it never said the bridge could actually be reached, which let a DHCP lease
23
- // change take the published surface down while status kept reporting health.
24
- const RESULT_SCHEMA = 'lattice.bridge_cli_result.v2';
25
- const REACHABILITY_PROBE_TIMEOUT_MS = 750;
26
-
27
- /** TCP connect probe. Answers "is anything accepting there right now". */
28
- export function probeBridgeListener({ address, port, timeoutMs = REACHABILITY_PROBE_TIMEOUT_MS }) {
29
- return new Promise((resolve) => {
30
- let settled = false;
31
- const finish = (value) => {
32
- if (settled) return;
33
- settled = true;
34
- socket.destroy();
35
- resolve(value);
36
- };
37
- const socket = createConnection({ host: address, port });
38
- socket.setTimeout(timeoutMs);
39
- socket.once('connect', () => finish(true));
40
- socket.once('timeout', () => finish(false));
41
- socket.once('error', () => finish(false));
42
- });
43
- }
44
-
45
- /**
46
- * Liveness of the configured listen address: does it still exist on this host,
47
- * did it move inside its subnet, and is anything accepting connections there.
48
- */
49
- async function bridgeLiveness(config, { interfaces = networkInterfaces(), probe = probeBridgeListener } = {}) {
50
- if (config === null || config.enabled !== true) {
51
- return { listen_state: 'unconfigured', effective_listen: null, listen_candidates: [],
52
- reachable: null, liveness_reason: null };
53
- }
54
- const resolved = resolveBridgeListenAddress({ configured: config.listen.address, interfaces });
55
- const effective = resolved.effective === null ? null
56
- : { address: resolved.effective, port: config.listen.port };
57
- const reachable = effective === null ? false
58
- : await probe({ address: effective.address, port: effective.port });
59
- return {
60
- listen_state: resolved.state,
61
- effective_listen: effective,
62
- listen_candidates: resolved.candidates,
63
- reachable,
64
- liveness_reason: resolved.reason ?? (reachable ? null : 'listener_not_accepting'),
65
- };
66
- }
67
-
68
- function fail(stderr, code, message) {
69
- stderr.write(`${JSON.stringify({ schema: 'lattice.cli_error.v2', code, message })}\n`);
70
- return 2;
71
- }
72
-
73
- function parseOptions(words) {
74
- const options = { address: undefined, port: undefined, upstream: undefined, allowedHosts: [] };
75
- for (let index = 0; index < words.length; index += 1) {
76
- const flag = words[index];
77
- if (flag === '--listen') {
78
- if (options.address !== undefined) throw new BridgeConfigError('USAGE', 'duplicate --listen');
79
- options.address = words[++index];
80
- }
81
- else if (flag === '--port') {
82
- if (options.port !== undefined) throw new BridgeConfigError('USAGE', 'duplicate --port');
83
- const value = words[++index];
84
- options.port = value === 'auto' ? null : Number(value);
85
- } else if (flag === '--dashboard') {
86
- if (options.upstream !== undefined) throw new BridgeConfigError('USAGE', 'upstream option is ambiguous');
87
- options.upstream = { mode: 'dashboard_descriptor' };
88
- } else if (flag === '--upstream') {
89
- if (options.upstream !== undefined) throw new BridgeConfigError('USAGE', 'upstream option is ambiguous');
90
- options.upstream = { mode: 'url', url: words[++index] };
91
- } else if (flag === '--allow-host') options.allowedHosts.push(words[++index]);
92
- else throw new BridgeConfigError('USAGE', `unknown bridge option: ${flag}`);
93
- if ((flag === '--listen' || flag === '--port' || flag === '--upstream' || flag === '--allow-host')
94
- && words[index] === undefined) throw new BridgeConfigError('USAGE', `missing value for ${flag}`);
95
- }
96
- return options;
97
- }
98
-
99
- function result(action, config, recovery = null, liveness = null) {
100
- return { schema: RESULT_SCHEMA, action, configured: config !== null, enabled: config?.enabled ?? false,
101
- listen: config?.listen ?? null, allowed_hosts: config?.allowed_hosts ?? null,
102
- upstream: config?.upstream ?? null, updated_at: config?.updated_at ?? null, recovery,
103
- listen_state: liveness?.listen_state ?? null,
104
- effective_listen: liveness?.effective_listen ?? null,
105
- listen_candidates: liveness?.listen_candidates ?? null,
106
- reachable: liveness?.reachable ?? null,
107
- liveness_reason: liveness?.liveness_reason ?? null };
108
- }
109
-
110
- export async function collectBridgeSetupWizard({ input, output, prompts = clack } = {}) {
111
- const common = { input, output };
112
- const enabled = await prompts.confirm({ ...common,
113
- message: 'Lattice dashboardのnetwork bridgeを有効にしますか?', initialValue: false });
114
- if (prompts.isCancel(enabled) || enabled !== true) return null;
115
- const address = await prompts.text({ ...common, message: 'listenするIP address',
116
- placeholder: '192.168.1.102', validate: (value) => isIP(value) === 0 ? 'IP literalを入力してください' : undefined });
117
- if (prompts.isCancel(address)) return null;
118
- const portMode = await prompts.select({ ...common, message: 'bridge port', initialValue: 'auto', options: [
119
- { value: 'auto', label: '自動選択', hint: '49152–65535からexclusive bind' },
120
- { value: 'custom', label: '指定する' },
121
- ] });
122
- if (prompts.isCancel(portMode)) return null;
123
- let port = null;
124
- if (portMode === 'custom') {
125
- const selected = await prompts.text({ ...common, message: 'port (49152–65535)',
126
- validate: (value) => /^\d+$/u.test(value) && Number(value) >= 49_152 && Number(value) <= 65_535
127
- ? undefined : '49152–65535の整数を入力してください' });
128
- if (prompts.isCancel(selected)) return null;
129
- port = Number(selected);
130
- }
131
- const upstreamMode = await prompts.select({ ...common, message: 'upstream', initialValue: 'dashboard', options: [
132
- { value: 'dashboard', label: '現在のLattice dashboard', hint: '再起動時もdescriptorから追従' },
133
- { value: 'custom', label: '固定URL' },
134
- ] });
135
- if (prompts.isCancel(upstreamMode)) return null;
136
- let upstream = { mode: 'dashboard_descriptor' };
137
- if (upstreamMode === 'custom') {
138
- const url = await prompts.text({ ...common, message: 'upstream HTTP(S) URL',
139
- placeholder: 'http://127.0.0.1:4318/' });
140
- if (prompts.isCancel(url)) return null;
141
- upstream = { mode: 'url', url };
142
- }
143
- const publicHost = await prompts.text({ ...common,
144
- message: '追加で許可する公開hostname(なければ空欄)', placeholder: 'lattice.kitepon.dev',
145
- validate: (value) => {
146
- if (value === '') return undefined;
147
- try { normalizeBridgeAllowedHost(value); return undefined; } catch {
148
- return 'portやschemeを含まないhostnameを入力してください';
149
- }
150
- } });
151
- if (prompts.isCancel(publicHost)) return null;
152
- return { address, port, upstream, allowedHosts: publicHost === '' ? [] : [publicHost] };
153
- }
154
-
155
- export async function runBridgeCli({ argv, stdout, stderr, env = process.env,
156
- stdin = process.stdin, daemon = { ensure: ensureBridgeDaemon, requestStop: requestBridgeDaemonStop,
157
- stop: stopBridgeDaemon, clearStop: clearBridgeStopControl },
158
- launchAgent = { snapshot: snapshotBridgeLaunchAgent, install: installBridgeLaunchAgent,
159
- disable: disableBridgeLaunchAgent, restore: restoreBridgeLaunchAgent },
160
- prompts = clack, probe = probeBridgeListener, interfaces = networkInterfaces() } = {}) {
161
- if (!Array.isArray(argv)) {
162
- return fail(stderr, 'USAGE', 'usage: lattice bridge <setup|reconfigure|status|disable|register> [options] --json');
163
- }
164
- const wizard = argv.length === 1 && argv[0] === 'setup';
165
- if (!wizard && argv.at(-1) !== '--json') {
166
- return fail(stderr, 'USAGE', 'usage: lattice bridge <setup|reconfigure|status|disable|register> [options] --json');
167
- }
168
- if (wizard && (!stdin?.isTTY || !stdout?.isTTY)) {
169
- return fail(stderr, 'BRIDGE_SETUP_REQUIRES_TTY',
170
- 'interactive setup requires a TTY; use lattice bridge setup --listen <IP> --port auto --dashboard --json');
171
- }
172
- const [command, ...words] = wizard ? argv : argv.slice(0, -1);
173
- try {
174
- if (command === 'register' && words.length === 0) {
175
- const current = await readBridgeConfig({ env });
176
- if (current === null || current.enabled !== true) {
177
- return fail(stderr, 'BRIDGE_DISABLED', 'bridge is not enabled; nothing to register');
178
- }
179
- const resolved = resolveBridgeListenAddress({ configured: current.listen.address, interfaces });
180
- if (resolved.effective === null) {
181
- return fail(stderr, 'BRIDGE_LISTEN_ADDRESS_ABSENT',
182
- 'configured bridge listen address is not present on this host');
183
- }
184
- const registration = await registerBridgeUpstream({ port: current.listen.port, env });
185
- stdout.write(`${JSON.stringify(registration)}\n`);
186
- return registration.state === 'failed' ? 1 : 0;
187
- }
188
- let config;
189
- let recovery = null;
190
- if (command === 'status' && words.length === 0) config = await readBridgeConfig({ env });
191
- else if (command === 'disable' && words.length === 0) {
192
- config = await withBridgeOperationLock({ env }, async () => {
193
- let previous;
194
- let descriptor = null;
195
- let invalidConfig = false;
196
- let invalidDescriptor = false;
197
- try {
198
- previous = await readBridgeConfig({ env });
199
- } catch (error) {
200
- if (!['BRIDGE_CONFIG_INVALID', 'BRIDGE_CONFIG_MODE_INVALID'].includes(error?.code)) throw error;
201
- invalidConfig = true;
202
- }
203
- try { descriptor = await readBridgeDaemonDescriptor({ env }); } catch (error) {
204
- if (error?.code !== 'BRIDGE_DAEMON_DESCRIPTOR_INVALID') throw error;
205
- invalidDescriptor = true;
206
- }
207
- const agentSnapshot = await launchAgent.snapshot({ env });
208
- const witnessedListen = previous?.listen ?? (descriptor === null ? null
209
- : { address: descriptor.address, port: descriptor.port });
210
- let stopResult = null;
211
- try {
212
- await launchAgent.disable({ snapshot: agentSnapshot, listen: witnessedListen, env });
213
- if (!agentSnapshot.loaded) {
214
- const requestStop = daemon.requestStop ?? daemon.stop ?? requestBridgeDaemonStop;
215
- stopResult = await requestStop({ env, listen: witnessedListen });
216
- }
217
- const disabled = invalidConfig ? await restoreBridgeConfig(null, { env })
218
- : await disableBridge({ env });
219
- if (invalidConfig) recovery = 'invalid_config_removed_after_fail_closed_shutdown';
220
- else if (invalidDescriptor) recovery = 'invalid_descriptor_removed_after_fail_closed_shutdown';
221
- await removeBridgeDaemonDescriptor({ env });
222
- await removeBridgeDaemonActiveMarker({ env });
223
- const clearStop = daemon.clearStop ?? clearBridgeStopControl;
224
- await clearStop({ env });
225
- if (stopResult?.state === 'not_running' && recovery === null) recovery = 'bridge_was_not_running';
226
- return disabled;
227
- } catch (error) {
228
- try {
229
- await launchAgent.restore({ snapshot: agentSnapshot, listen: witnessedListen,
230
- config: previous, env });
231
- }
232
- catch (rollbackError) {
233
- throw new BridgeConfigError('BRIDGE_ROLLBACK_FAILED',
234
- `bridge disable failed and LaunchAgent rollback failed: ${rollbackError.message}`, undefined, error);
235
- }
236
- throw error;
237
- }
238
- });
239
- }
240
- else if (command === 'setup' || command === 'reconfigure') {
241
- const options = wizard ? await collectBridgeSetupWizard({ input: stdin, output: stdout, prompts })
242
- : parseOptions(words);
243
- if (options === null) {
244
- stdout.write('Lattice bridge setupを取り消しました。設定は変更していません。\n');
245
- return 0;
246
- }
247
- if (command === 'setup' && options.address === undefined) {
248
- throw new BridgeConfigError('USAGE', 'setup requires explicit --listen <IP> opt-in');
249
- }
250
- config = await withBridgeOperationLock({ env }, async () => {
251
- const clearStop = daemon.clearStop ?? clearBridgeStopControl;
252
- await clearStop({ env });
253
- const current = await readBridgeConfig({ env });
254
- const agentSnapshot = await launchAgent.snapshot({ env });
255
- const configured = await configureBridge({
256
- address: options.address ?? current?.listen.address,
257
- port: options.port === undefined ? (command === 'reconfigure' ? current?.listen.port ?? null : null) : options.port,
258
- upstream: options.upstream ?? current?.upstream ?? { mode: 'dashboard_descriptor' }, env,
259
- allowedHosts: options.allowedHosts.length > 0 ? options.allowedHosts
260
- : current?.allowed_hosts?.filter((host) => host !== current.listen.address) ?? [],
261
- reuseCurrentPort: options.port === undefined,
262
- });
263
- try {
264
- if (!agentSnapshot.loaded && current !== null) {
265
- const requestStop = daemon.requestStop ?? daemon.stop ?? requestBridgeDaemonStop;
266
- await requestStop({ env, listen: current.listen });
267
- await clearStop({ env });
268
- }
269
- await launchAgent.install({ config: configured, previousListen: current?.listen ?? null, env });
270
- } catch (error) {
271
- try {
272
- await restoreBridgeConfig(current, { env });
273
- await clearStop({ env });
274
- await launchAgent.restore({ snapshot: agentSnapshot, listen: configured.listen,
275
- config: current, env });
276
- if (current?.enabled && !agentSnapshot.loaded) await daemon.ensure({ env });
277
- } catch (rollbackError) {
278
- throw new BridgeConfigError('BRIDGE_ROLLBACK_FAILED',
279
- `bridge setup failed and rollback failed: ${rollbackError.message}`, undefined, error);
280
- }
281
- throw error;
282
- }
283
- return configured;
284
- });
285
- } else return fail(stderr, 'USAGE', 'unknown bridge command or options');
286
- // Liveness is only meaningful for a read: the mutating commands have just
287
- // reconfigured the daemon and the socket may not have settled yet.
288
- const liveness = command === 'status' ? await bridgeLiveness(config, { probe, interfaces }) : null;
289
- if (wizard) stdout.write(`Lattice bridgeを${config.listen.address}:${config.listen.port}で有効にしました。\n`);
290
- else stdout.write(`${JSON.stringify(result(command, config, recovery, liveness))}\n`);
291
- return 0;
292
- } catch (error) {
293
- stderr.write(`${JSON.stringify({ schema: 'lattice.cli_error.v2',
294
- code: error?.code ?? 'BRIDGE_FAILED', message: error?.message ?? 'bridge command failed' })}\n`);
295
- return error?.code === 'USAGE' ? 2 : 1;
296
- }
297
- }
1
+ import { createConnection, isIP } from 'node:net';
2
+ import { networkInterfaces } from 'node:os';
3
+ import * as clack from '@clack/prompts';
4
+
5
+ import { resolveBridgeListenAddress } from './bridge-address.mjs';
6
+ import { registerBridgeUpstream } from './bridge-registrar.mjs';
7
+ import {
8
+ BridgeConfigError, configureBridge, disableBridge, readBridgeConfig, restoreBridgeConfig,
9
+ normalizeBridgeAllowedHost, withBridgeOperationLock,
10
+ } from './bridge-config.mjs';
11
+ import {
12
+ clearBridgeStopControl, ensureBridgeDaemon, readBridgeDaemonDescriptor,
13
+ removeBridgeDaemonActiveMarker, removeBridgeDaemonDescriptor, requestBridgeDaemonStop,
14
+ stopBridgeDaemon,
15
+ } from './bridge-daemon.mjs';
16
+ import {
17
+ disableBridgeLaunchAgent, installBridgeLaunchAgent, restoreBridgeLaunchAgent,
18
+ snapshotBridgeLaunchAgent,
19
+ } from './bridge-launch-agent.mjs';
20
+
21
+ // v2 adds the liveness fields. `enabled` only says the configuration is on;
22
+ // it never said the bridge could actually be reached, which let a DHCP lease
23
+ // change take the published surface down while status kept reporting health.
24
+ const RESULT_SCHEMA = 'lattice.bridge_cli_result.v2';
25
+ const REACHABILITY_PROBE_TIMEOUT_MS = 750;
26
+
27
+ /** TCP connect probe. Answers "is anything accepting there right now". */
28
+ export function probeBridgeListener({ address, port, timeoutMs = REACHABILITY_PROBE_TIMEOUT_MS }) {
29
+ return new Promise((resolve) => {
30
+ let settled = false;
31
+ const finish = (value) => {
32
+ if (settled) return;
33
+ settled = true;
34
+ socket.destroy();
35
+ resolve(value);
36
+ };
37
+ const socket = createConnection({ host: address, port });
38
+ socket.setTimeout(timeoutMs);
39
+ socket.once('connect', () => finish(true));
40
+ socket.once('timeout', () => finish(false));
41
+ socket.once('error', () => finish(false));
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Liveness of the configured listen address: does it still exist on this host,
47
+ * did it move inside its subnet, and is anything accepting connections there.
48
+ */
49
+ async function bridgeLiveness(config, { interfaces = networkInterfaces(), probe = probeBridgeListener } = {}) {
50
+ if (config === null || config.enabled !== true) {
51
+ return { listen_state: 'unconfigured', effective_listen: null, listen_candidates: [],
52
+ reachable: null, liveness_reason: null };
53
+ }
54
+ const resolved = resolveBridgeListenAddress({ configured: config.listen.address, interfaces });
55
+ const effective = resolved.effective === null ? null
56
+ : { address: resolved.effective, port: config.listen.port };
57
+ const reachable = effective === null ? false
58
+ : await probe({ address: effective.address, port: effective.port });
59
+ return {
60
+ listen_state: resolved.state,
61
+ effective_listen: effective,
62
+ listen_candidates: resolved.candidates,
63
+ reachable,
64
+ liveness_reason: resolved.reason ?? (reachable ? null : 'listener_not_accepting'),
65
+ };
66
+ }
67
+
68
+ function fail(stderr, code, message) {
69
+ stderr.write(`${JSON.stringify({ schema: 'lattice.cli_error.v2', code, message })}\n`);
70
+ return 2;
71
+ }
72
+
73
+ function parseOptions(words) {
74
+ const options = { address: undefined, port: undefined, upstream: undefined, allowedHosts: [] };
75
+ for (let index = 0; index < words.length; index += 1) {
76
+ const flag = words[index];
77
+ if (flag === '--listen') {
78
+ if (options.address !== undefined) throw new BridgeConfigError('USAGE', 'duplicate --listen');
79
+ options.address = words[++index];
80
+ }
81
+ else if (flag === '--port') {
82
+ if (options.port !== undefined) throw new BridgeConfigError('USAGE', 'duplicate --port');
83
+ const value = words[++index];
84
+ options.port = value === 'auto' ? null : Number(value);
85
+ } else if (flag === '--dashboard') {
86
+ if (options.upstream !== undefined) throw new BridgeConfigError('USAGE', 'upstream option is ambiguous');
87
+ options.upstream = { mode: 'dashboard_descriptor' };
88
+ } else if (flag === '--upstream') {
89
+ if (options.upstream !== undefined) throw new BridgeConfigError('USAGE', 'upstream option is ambiguous');
90
+ options.upstream = { mode: 'url', url: words[++index] };
91
+ } else if (flag === '--allow-host') options.allowedHosts.push(words[++index]);
92
+ else throw new BridgeConfigError('USAGE', `unknown bridge option: ${flag}`);
93
+ if ((flag === '--listen' || flag === '--port' || flag === '--upstream' || flag === '--allow-host')
94
+ && words[index] === undefined) throw new BridgeConfigError('USAGE', `missing value for ${flag}`);
95
+ }
96
+ return options;
97
+ }
98
+
99
+ function result(action, config, recovery = null, liveness = null) {
100
+ return { schema: RESULT_SCHEMA, action, configured: config !== null, enabled: config?.enabled ?? false,
101
+ listen: config?.listen ?? null, allowed_hosts: config?.allowed_hosts ?? null,
102
+ upstream: config?.upstream ?? null, updated_at: config?.updated_at ?? null, recovery,
103
+ listen_state: liveness?.listen_state ?? null,
104
+ effective_listen: liveness?.effective_listen ?? null,
105
+ listen_candidates: liveness?.listen_candidates ?? null,
106
+ reachable: liveness?.reachable ?? null,
107
+ liveness_reason: liveness?.liveness_reason ?? null };
108
+ }
109
+
110
+ export async function collectBridgeSetupWizard({ input, output, prompts = clack } = {}) {
111
+ const common = { input, output };
112
+ const enabled = await prompts.confirm({ ...common,
113
+ message: 'Lattice dashboardのnetwork bridgeを有効にしますか?', initialValue: false });
114
+ if (prompts.isCancel(enabled) || enabled !== true) return null;
115
+ const address = await prompts.text({ ...common, message: 'listenするIP address',
116
+ placeholder: '192.168.1.102', validate: (value) => isIP(value) === 0 ? 'IP literalを入力してください' : undefined });
117
+ if (prompts.isCancel(address)) return null;
118
+ const portMode = await prompts.select({ ...common, message: 'bridge port', initialValue: 'auto', options: [
119
+ { value: 'auto', label: '自動選択', hint: '49152–65535からexclusive bind' },
120
+ { value: 'custom', label: '指定する' },
121
+ ] });
122
+ if (prompts.isCancel(portMode)) return null;
123
+ let port = null;
124
+ if (portMode === 'custom') {
125
+ const selected = await prompts.text({ ...common, message: 'port (49152–65535)',
126
+ validate: (value) => /^\d+$/u.test(value) && Number(value) >= 49_152 && Number(value) <= 65_535
127
+ ? undefined : '49152–65535の整数を入力してください' });
128
+ if (prompts.isCancel(selected)) return null;
129
+ port = Number(selected);
130
+ }
131
+ const upstreamMode = await prompts.select({ ...common, message: 'upstream', initialValue: 'dashboard', options: [
132
+ { value: 'dashboard', label: '現在のLattice dashboard', hint: '再起動時もdescriptorから追従' },
133
+ { value: 'custom', label: '固定URL' },
134
+ ] });
135
+ if (prompts.isCancel(upstreamMode)) return null;
136
+ let upstream = { mode: 'dashboard_descriptor' };
137
+ if (upstreamMode === 'custom') {
138
+ const url = await prompts.text({ ...common, message: 'upstream HTTP(S) URL',
139
+ placeholder: 'http://127.0.0.1:4318/' });
140
+ if (prompts.isCancel(url)) return null;
141
+ upstream = { mode: 'url', url };
142
+ }
143
+ const publicHost = await prompts.text({ ...common,
144
+ message: '追加で許可する公開hostname(なければ空欄)', placeholder: 'lattice.kitepon.dev',
145
+ validate: (value) => {
146
+ if (value === '') return undefined;
147
+ try { normalizeBridgeAllowedHost(value); return undefined; } catch {
148
+ return 'portやschemeを含まないhostnameを入力してください';
149
+ }
150
+ } });
151
+ if (prompts.isCancel(publicHost)) return null;
152
+ return { address, port, upstream, allowedHosts: publicHost === '' ? [] : [publicHost] };
153
+ }
154
+
155
+ export async function runBridgeCli({ argv, stdout, stderr, env = process.env,
156
+ stdin = process.stdin, daemon = { ensure: ensureBridgeDaemon, requestStop: requestBridgeDaemonStop,
157
+ stop: stopBridgeDaemon, clearStop: clearBridgeStopControl },
158
+ launchAgent = { snapshot: snapshotBridgeLaunchAgent, install: installBridgeLaunchAgent,
159
+ disable: disableBridgeLaunchAgent, restore: restoreBridgeLaunchAgent },
160
+ prompts = clack, probe = probeBridgeListener, interfaces = networkInterfaces() } = {}) {
161
+ if (!Array.isArray(argv)) {
162
+ return fail(stderr, 'USAGE', 'usage: lattice bridge <setup|reconfigure|status|disable|register> [options] --json');
163
+ }
164
+ const wizard = argv.length === 1 && argv[0] === 'setup';
165
+ if (!wizard && argv.at(-1) !== '--json') {
166
+ return fail(stderr, 'USAGE', 'usage: lattice bridge <setup|reconfigure|status|disable|register> [options] --json');
167
+ }
168
+ if (wizard && (!stdin?.isTTY || !stdout?.isTTY)) {
169
+ return fail(stderr, 'BRIDGE_SETUP_REQUIRES_TTY',
170
+ 'interactive setup requires a TTY; use lattice bridge setup --listen <IP> --port auto --dashboard --json');
171
+ }
172
+ const [command, ...words] = wizard ? argv : argv.slice(0, -1);
173
+ try {
174
+ if (command === 'register' && words.length === 0) {
175
+ const current = await readBridgeConfig({ env });
176
+ if (current === null || current.enabled !== true) {
177
+ return fail(stderr, 'BRIDGE_DISABLED', 'bridge is not enabled; nothing to register');
178
+ }
179
+ const resolved = resolveBridgeListenAddress({ configured: current.listen.address, interfaces });
180
+ if (resolved.effective === null) {
181
+ return fail(stderr, 'BRIDGE_LISTEN_ADDRESS_ABSENT',
182
+ 'configured bridge listen address is not present on this host');
183
+ }
184
+ const registration = await registerBridgeUpstream({ port: current.listen.port, env });
185
+ stdout.write(`${JSON.stringify(registration)}\n`);
186
+ return registration.state === 'failed' ? 1 : 0;
187
+ }
188
+ let config;
189
+ let recovery = null;
190
+ if (command === 'status' && words.length === 0) config = await readBridgeConfig({ env });
191
+ else if (command === 'disable' && words.length === 0) {
192
+ config = await withBridgeOperationLock({ env }, async () => {
193
+ let previous;
194
+ let descriptor = null;
195
+ let invalidConfig = false;
196
+ let invalidDescriptor = false;
197
+ try {
198
+ previous = await readBridgeConfig({ env });
199
+ } catch (error) {
200
+ if (!['BRIDGE_CONFIG_INVALID', 'BRIDGE_CONFIG_MODE_INVALID'].includes(error?.code)) throw error;
201
+ invalidConfig = true;
202
+ }
203
+ try { descriptor = await readBridgeDaemonDescriptor({ env }); } catch (error) {
204
+ if (error?.code !== 'BRIDGE_DAEMON_DESCRIPTOR_INVALID') throw error;
205
+ invalidDescriptor = true;
206
+ }
207
+ const agentSnapshot = await launchAgent.snapshot({ env });
208
+ const witnessedListen = previous?.listen ?? (descriptor === null ? null
209
+ : { address: descriptor.address, port: descriptor.port });
210
+ let stopResult = null;
211
+ try {
212
+ await launchAgent.disable({ snapshot: agentSnapshot, listen: witnessedListen, env });
213
+ if (!agentSnapshot.loaded) {
214
+ const requestStop = daemon.requestStop ?? daemon.stop ?? requestBridgeDaemonStop;
215
+ stopResult = await requestStop({ env, listen: witnessedListen });
216
+ }
217
+ const disabled = invalidConfig ? await restoreBridgeConfig(null, { env })
218
+ : await disableBridge({ env });
219
+ if (invalidConfig) recovery = 'invalid_config_removed_after_fail_closed_shutdown';
220
+ else if (invalidDescriptor) recovery = 'invalid_descriptor_removed_after_fail_closed_shutdown';
221
+ await removeBridgeDaemonDescriptor({ env });
222
+ await removeBridgeDaemonActiveMarker({ env });
223
+ const clearStop = daemon.clearStop ?? clearBridgeStopControl;
224
+ await clearStop({ env });
225
+ if (stopResult?.state === 'not_running' && recovery === null) recovery = 'bridge_was_not_running';
226
+ return disabled;
227
+ } catch (error) {
228
+ try {
229
+ await launchAgent.restore({ snapshot: agentSnapshot, listen: witnessedListen,
230
+ config: previous, env });
231
+ }
232
+ catch (rollbackError) {
233
+ throw new BridgeConfigError('BRIDGE_ROLLBACK_FAILED',
234
+ `bridge disable failed and LaunchAgent rollback failed: ${rollbackError.message}`, undefined, error);
235
+ }
236
+ throw error;
237
+ }
238
+ });
239
+ }
240
+ else if (command === 'setup' || command === 'reconfigure') {
241
+ const options = wizard ? await collectBridgeSetupWizard({ input: stdin, output: stdout, prompts })
242
+ : parseOptions(words);
243
+ if (options === null) {
244
+ stdout.write('Lattice bridge setupを取り消しました。設定は変更していません。\n');
245
+ return 0;
246
+ }
247
+ if (command === 'setup' && options.address === undefined) {
248
+ throw new BridgeConfigError('USAGE', 'setup requires explicit --listen <IP> opt-in');
249
+ }
250
+ config = await withBridgeOperationLock({ env }, async () => {
251
+ const clearStop = daemon.clearStop ?? clearBridgeStopControl;
252
+ await clearStop({ env });
253
+ const current = await readBridgeConfig({ env });
254
+ const agentSnapshot = await launchAgent.snapshot({ env });
255
+ const configured = await configureBridge({
256
+ address: options.address ?? current?.listen.address,
257
+ port: options.port === undefined ? (command === 'reconfigure' ? current?.listen.port ?? null : null) : options.port,
258
+ upstream: options.upstream ?? current?.upstream ?? { mode: 'dashboard_descriptor' }, env,
259
+ allowedHosts: options.allowedHosts.length > 0 ? options.allowedHosts
260
+ : current?.allowed_hosts?.filter((host) => host !== current.listen.address) ?? [],
261
+ reuseCurrentPort: options.port === undefined,
262
+ });
263
+ try {
264
+ if (!agentSnapshot.loaded && current !== null) {
265
+ const requestStop = daemon.requestStop ?? daemon.stop ?? requestBridgeDaemonStop;
266
+ await requestStop({ env, listen: current.listen });
267
+ await clearStop({ env });
268
+ }
269
+ await launchAgent.install({ config: configured, previousListen: current?.listen ?? null, env });
270
+ } catch (error) {
271
+ try {
272
+ await restoreBridgeConfig(current, { env });
273
+ await clearStop({ env });
274
+ await launchAgent.restore({ snapshot: agentSnapshot, listen: configured.listen,
275
+ config: current, env });
276
+ if (current?.enabled && !agentSnapshot.loaded) await daemon.ensure({ env });
277
+ } catch (rollbackError) {
278
+ throw new BridgeConfigError('BRIDGE_ROLLBACK_FAILED',
279
+ `bridge setup failed and rollback failed: ${rollbackError.message}`, undefined, error);
280
+ }
281
+ throw error;
282
+ }
283
+ return configured;
284
+ });
285
+ } else return fail(stderr, 'USAGE', 'unknown bridge command or options');
286
+ // Liveness is only meaningful for a read: the mutating commands have just
287
+ // reconfigured the daemon and the socket may not have settled yet.
288
+ const liveness = command === 'status' ? await bridgeLiveness(config, { probe, interfaces }) : null;
289
+ if (wizard) stdout.write(`Lattice bridgeを${config.listen.address}:${config.listen.port}で有効にしました。\n`);
290
+ else stdout.write(`${JSON.stringify(result(command, config, recovery, liveness))}\n`);
291
+ return 0;
292
+ } catch (error) {
293
+ stderr.write(`${JSON.stringify({ schema: 'lattice.cli_error.v2',
294
+ code: error?.code ?? 'BRIDGE_FAILED', message: error?.message ?? 'bridge command failed' })}\n`);
295
+ return error?.code === 'USAGE' ? 2 : 1;
296
+ }
297
+ }