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