@caoruhua/open-claude-remote 0.1.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 (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/dist/api/auth-routes.d.ts +4 -0
  4. package/dist/api/auth-routes.d.ts.map +1 -0
  5. package/dist/api/auth-routes.js +7 -0
  6. package/dist/api/auth-routes.js.map +1 -0
  7. package/dist/api/config-routes.d.ts +4 -0
  8. package/dist/api/config-routes.d.ts.map +1 -0
  9. package/dist/api/config-routes.js +180 -0
  10. package/dist/api/config-routes.js.map +1 -0
  11. package/dist/api/health-routes.d.ts +3 -0
  12. package/dist/api/health-routes.d.ts.map +1 -0
  13. package/dist/api/health-routes.js +9 -0
  14. package/dist/api/health-routes.js.map +1 -0
  15. package/dist/api/hook-routes.d.ts +4 -0
  16. package/dist/api/hook-routes.d.ts.map +1 -0
  17. package/dist/api/hook-routes.js +32 -0
  18. package/dist/api/hook-routes.js.map +1 -0
  19. package/dist/api/instance-routes.d.ts +20 -0
  20. package/dist/api/instance-routes.d.ts.map +1 -0
  21. package/dist/api/instance-routes.js +128 -0
  22. package/dist/api/instance-routes.js.map +1 -0
  23. package/dist/api/push-routes.d.ts +5 -0
  24. package/dist/api/push-routes.d.ts.map +1 -0
  25. package/dist/api/push-routes.js +45 -0
  26. package/dist/api/push-routes.js.map +1 -0
  27. package/dist/api/router.d.ts +19 -0
  28. package/dist/api/router.d.ts.map +1 -0
  29. package/dist/api/router.js +37 -0
  30. package/dist/api/router.js.map +1 -0
  31. package/dist/api/status-routes.d.ts +5 -0
  32. package/dist/api/status-routes.d.ts.map +1 -0
  33. package/dist/api/status-routes.js +17 -0
  34. package/dist/api/status-routes.js.map +1 -0
  35. package/dist/attach.d.ts +9 -0
  36. package/dist/attach.d.ts.map +1 -0
  37. package/dist/attach.js +155 -0
  38. package/dist/attach.js.map +1 -0
  39. package/dist/auth/auth-middleware.d.ts +52 -0
  40. package/dist/auth/auth-middleware.d.ts.map +1 -0
  41. package/dist/auth/auth-middleware.js +124 -0
  42. package/dist/auth/auth-middleware.js.map +1 -0
  43. package/dist/auth/rate-limiter.d.ts +37 -0
  44. package/dist/auth/rate-limiter.d.ts.map +1 -0
  45. package/dist/auth/rate-limiter.js +81 -0
  46. package/dist/auth/rate-limiter.js.map +1 -0
  47. package/dist/auth/token-generator.d.ts +9 -0
  48. package/dist/auth/token-generator.d.ts.map +1 -0
  49. package/dist/auth/token-generator.js +15 -0
  50. package/dist/auth/token-generator.js.map +1 -0
  51. package/dist/cli-utils.d.ts +19 -0
  52. package/dist/cli-utils.d.ts.map +1 -0
  53. package/dist/cli-utils.js +132 -0
  54. package/dist/cli-utils.js.map +1 -0
  55. package/dist/cli.d.ts +21 -0
  56. package/dist/cli.d.ts.map +1 -0
  57. package/dist/cli.js +58 -0
  58. package/dist/cli.js.map +1 -0
  59. package/dist/config.d.ts +146 -0
  60. package/dist/config.d.ts.map +1 -0
  61. package/dist/config.js +329 -0
  62. package/dist/config.js.map +1 -0
  63. package/dist/hooks/hook-receiver.d.ts +39 -0
  64. package/dist/hooks/hook-receiver.d.ts.map +1 -0
  65. package/dist/hooks/hook-receiver.js +46 -0
  66. package/dist/hooks/hook-receiver.js.map +1 -0
  67. package/dist/index.d.ts +3 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +353 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/logger/logger.d.ts +8 -0
  72. package/dist/logger/logger.d.ts.map +1 -0
  73. package/dist/logger/logger.js +90 -0
  74. package/dist/logger/logger.js.map +1 -0
  75. package/dist/notification/dingtalk-service.d.ts +24 -0
  76. package/dist/notification/dingtalk-service.d.ts.map +1 -0
  77. package/dist/notification/dingtalk-service.js +94 -0
  78. package/dist/notification/dingtalk-service.js.map +1 -0
  79. package/dist/pty/output-buffer.d.ts +25 -0
  80. package/dist/pty/output-buffer.d.ts.map +1 -0
  81. package/dist/pty/output-buffer.js +58 -0
  82. package/dist/pty/output-buffer.js.map +1 -0
  83. package/dist/pty/pty-manager.d.ts +45 -0
  84. package/dist/pty/pty-manager.d.ts.map +1 -0
  85. package/dist/pty/pty-manager.js +108 -0
  86. package/dist/pty/pty-manager.js.map +1 -0
  87. package/dist/pty/types.d.ts +11 -0
  88. package/dist/pty/types.d.ts.map +1 -0
  89. package/dist/pty/types.js +2 -0
  90. package/dist/pty/types.js.map +1 -0
  91. package/dist/pty/virtual-pty.d.ts +37 -0
  92. package/dist/pty/virtual-pty.d.ts.map +1 -0
  93. package/dist/pty/virtual-pty.js +161 -0
  94. package/dist/pty/virtual-pty.js.map +1 -0
  95. package/dist/push/push-service.d.ts +87 -0
  96. package/dist/push/push-service.d.ts.map +1 -0
  97. package/dist/push/push-service.js +301 -0
  98. package/dist/push/push-service.js.map +1 -0
  99. package/dist/registry/instance-registry.d.ts +32 -0
  100. package/dist/registry/instance-registry.d.ts.map +1 -0
  101. package/dist/registry/instance-registry.js +115 -0
  102. package/dist/registry/instance-registry.js.map +1 -0
  103. package/dist/registry/instance-spawner.d.ts +33 -0
  104. package/dist/registry/instance-spawner.d.ts.map +1 -0
  105. package/dist/registry/instance-spawner.js +91 -0
  106. package/dist/registry/instance-spawner.js.map +1 -0
  107. package/dist/registry/port-finder.d.ts +8 -0
  108. package/dist/registry/port-finder.d.ts.map +1 -0
  109. package/dist/registry/port-finder.js +35 -0
  110. package/dist/registry/port-finder.js.map +1 -0
  111. package/dist/registry/shared-token.d.ts +11 -0
  112. package/dist/registry/shared-token.d.ts.map +1 -0
  113. package/dist/registry/shared-token.js +82 -0
  114. package/dist/registry/shared-token.js.map +1 -0
  115. package/dist/registry/stop-instances.d.ts +27 -0
  116. package/dist/registry/stop-instances.d.ts.map +1 -0
  117. package/dist/registry/stop-instances.js +212 -0
  118. package/dist/registry/stop-instances.js.map +1 -0
  119. package/dist/session/session-controller.d.ts +58 -0
  120. package/dist/session/session-controller.d.ts.map +1 -0
  121. package/dist/session/session-controller.js +273 -0
  122. package/dist/session/session-controller.js.map +1 -0
  123. package/dist/terminal/terminal-relay.d.ts +29 -0
  124. package/dist/terminal/terminal-relay.d.ts.map +1 -0
  125. package/dist/terminal/terminal-relay.js +106 -0
  126. package/dist/terminal/terminal-relay.js.map +1 -0
  127. package/dist/utils/ansi-filter.d.ts +41 -0
  128. package/dist/utils/ansi-filter.d.ts.map +1 -0
  129. package/dist/utils/ansi-filter.js +147 -0
  130. package/dist/utils/ansi-filter.js.map +1 -0
  131. package/dist/utils/file-lock.d.ts +23 -0
  132. package/dist/utils/file-lock.d.ts.map +1 -0
  133. package/dist/utils/file-lock.js +125 -0
  134. package/dist/utils/file-lock.js.map +1 -0
  135. package/dist/utils/ip-monitor.d.ts +39 -0
  136. package/dist/utils/ip-monitor.d.ts.map +1 -0
  137. package/dist/utils/ip-monitor.js +114 -0
  138. package/dist/utils/ip-monitor.js.map +1 -0
  139. package/dist/utils/network.d.ts +19 -0
  140. package/dist/utils/network.d.ts.map +1 -0
  141. package/dist/utils/network.js +54 -0
  142. package/dist/utils/network.js.map +1 -0
  143. package/dist/utils/pid-file.d.ts +10 -0
  144. package/dist/utils/pid-file.d.ts.map +1 -0
  145. package/dist/utils/pid-file.js +30 -0
  146. package/dist/utils/pid-file.js.map +1 -0
  147. package/dist/utils/qrcode-banner.d.ts +6 -0
  148. package/dist/utils/qrcode-banner.d.ts.map +1 -0
  149. package/dist/utils/qrcode-banner.js +16 -0
  150. package/dist/utils/qrcode-banner.js.map +1 -0
  151. package/dist/ws/ws-handler.d.ts +10 -0
  152. package/dist/ws/ws-handler.d.ts.map +1 -0
  153. package/dist/ws/ws-handler.js +36 -0
  154. package/dist/ws/ws-handler.js.map +1 -0
  155. package/dist/ws/ws-server.d.ts +78 -0
  156. package/dist/ws/ws-server.d.ts.map +1 -0
  157. package/dist/ws/ws-server.js +223 -0
  158. package/dist/ws/ws-server.js.map +1 -0
  159. package/frontend-dist/assets/index-BKudo1Dw.css +32 -0
  160. package/frontend-dist/assets/index-BqqB1hYe.js +141 -0
  161. package/frontend-dist/index.html +15 -0
  162. package/frontend-dist/sw.js +46 -0
  163. package/package.json +79 -0
  164. package/shared-dist/constants.d.ts +10 -0
  165. package/shared-dist/constants.d.ts.map +1 -0
  166. package/shared-dist/constants.js +10 -0
  167. package/shared-dist/constants.js.map +1 -0
  168. package/shared-dist/defaults.d.ts +30 -0
  169. package/shared-dist/defaults.d.ts.map +1 -0
  170. package/shared-dist/defaults.js +31 -0
  171. package/shared-dist/defaults.js.map +1 -0
  172. package/shared-dist/index.d.ts +5 -0
  173. package/shared-dist/index.d.ts.map +1 -0
  174. package/shared-dist/index.js +5 -0
  175. package/shared-dist/index.js.map +1 -0
  176. package/shared-dist/instance.d.ts +25 -0
  177. package/shared-dist/instance.d.ts.map +1 -0
  178. package/shared-dist/instance.js +7 -0
  179. package/shared-dist/instance.js.map +1 -0
  180. package/shared-dist/question-utils.d.ts +15 -0
  181. package/shared-dist/question-utils.d.ts.map +1 -0
  182. package/shared-dist/question-utils.js +24 -0
  183. package/shared-dist/question-utils.js.map +1 -0
  184. package/shared-dist/ws-protocol.d.ts +60 -0
  185. package/shared-dist/ws-protocol.d.ts.map +1 -0
  186. package/shared-dist/ws-protocol.js +5 -0
  187. package/shared-dist/ws-protocol.js.map +1 -0
@@ -0,0 +1,301 @@
1
+ import { logger } from '../logger/logger.js';
2
+ import { promises as fs } from 'fs';
3
+ import { join } from 'path';
4
+ import { withFileLockAsync } from '../utils/file-lock.js';
5
+ /**
6
+ * Web Push notification service.
7
+ * Manages VAPID keys, subscriptions, and notification delivery.
8
+ *
9
+ * 多实例支持:订阅数据通过文件持久化并跨实例共享。
10
+ * subscribe/unsubscribe 使用最终一致性语义——内存立即更新,
11
+ * 文件持久化异步执行(不阻塞调用方)。如果进程在持久化完成前崩溃,
12
+ * 该订阅需客户端重新注册。
13
+ */
14
+ export class PushService {
15
+ subscriptions = new Map();
16
+ vapidPublicKey;
17
+ vapidPrivateKey;
18
+ vapidSubject;
19
+ vapidInitPromise;
20
+ vapidKeysFile;
21
+ vapidLockPath;
22
+ subscriptionsFile;
23
+ subscriptionsLockPath;
24
+ dataDir;
25
+ constructor(baseDir) {
26
+ this.dataDir = baseDir;
27
+ this.vapidKeysFile = join(baseDir, 'vapid-keys.json');
28
+ this.vapidLockPath = this.vapidKeysFile + '.lock';
29
+ this.subscriptionsFile = join(baseDir, 'push-subscriptions.json');
30
+ this.subscriptionsLockPath = this.subscriptionsFile + '.lock';
31
+ this.vapidSubject = process.env['VAPID_SUBJECT'] ?? 'mailto:noreply@localhost';
32
+ const envPublic = process.env['VAPID_PUBLIC_KEY'];
33
+ const envPrivate = process.env['VAPID_PRIVATE_KEY'];
34
+ if (envPublic && envPrivate) {
35
+ this.vapidPublicKey = envPublic;
36
+ this.vapidPrivateKey = envPrivate;
37
+ logger.info('Using VAPID keys from environment variables');
38
+ // 即使使用环境变量,也要加载持久化的订阅
39
+ this.vapidInitPromise = this.loadSubscriptions();
40
+ }
41
+ else {
42
+ // Lazy init — 加载或生成 VAPID 密钥,然后加载订阅
43
+ this.vapidPublicKey = '';
44
+ this.vapidPrivateKey = '';
45
+ this.vapidInitPromise = this.initVapidAndSubscriptions();
46
+ }
47
+ }
48
+ /**
49
+ * 确保数据目录存在
50
+ */
51
+ async ensureDataDir() {
52
+ try {
53
+ await fs.mkdir(this.dataDir, { recursive: true });
54
+ }
55
+ catch (err) {
56
+ logger.error({ err, path: this.dataDir }, 'Failed to create data directory');
57
+ throw err;
58
+ }
59
+ }
60
+ /**
61
+ * 从文件加载 VAPID 密钥
62
+ */
63
+ async loadVapidKeys() {
64
+ try {
65
+ const content = await fs.readFile(this.vapidKeysFile, 'utf-8');
66
+ const keys = JSON.parse(content);
67
+ if (keys.publicKey && keys.privateKey) {
68
+ return keys;
69
+ }
70
+ }
71
+ catch {
72
+ // 文件不存在或解析失败
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * 保存 VAPID 密钥到文件
78
+ */
79
+ async saveVapidKeys(keys) {
80
+ await this.ensureDataDir();
81
+ await fs.writeFile(this.vapidKeysFile, JSON.stringify(keys, null, 2), { encoding: 'utf-8', mode: 0o600 });
82
+ }
83
+ /**
84
+ * 从文件加载订阅数据
85
+ */
86
+ async loadSubscriptions() {
87
+ try {
88
+ const content = await fs.readFile(this.subscriptionsFile, 'utf-8');
89
+ const data = JSON.parse(content);
90
+ for (const sub of data) {
91
+ this.subscriptions.set(sub.endpoint, sub);
92
+ }
93
+ logger.info({ count: this.subscriptions.size }, 'Loaded push subscriptions from file');
94
+ }
95
+ catch {
96
+ // 文件不存在或解析失败,不加载任何订阅
97
+ logger.debug('No existing push subscriptions file found');
98
+ }
99
+ }
100
+ /**
101
+ * 从文件读取所有订阅(包含其他实例写入的)。
102
+ * 通过文件锁保护,防止读到半写的 JSON。
103
+ */
104
+ async readSubscriptionsFromFile() {
105
+ try {
106
+ return await withFileLockAsync(this.subscriptionsLockPath, async () => {
107
+ const content = await fs.readFile(this.subscriptionsFile, 'utf-8');
108
+ return JSON.parse(content);
109
+ });
110
+ }
111
+ catch {
112
+ return [];
113
+ }
114
+ }
115
+ /**
116
+ * 锁定读-合并-写订阅变更。
117
+ * type='add': 将 sub 合并到文件中(按 endpoint 去重)
118
+ * type='remove': 从文件中移除一个或多个 endpoints
119
+ */
120
+ async persistSubscriptionChange(type, endpoints, sub) {
121
+ await withFileLockAsync(this.subscriptionsLockPath, async () => {
122
+ await this.ensureDataDir();
123
+ let fileSubs = [];
124
+ try {
125
+ const content = await fs.readFile(this.subscriptionsFile, 'utf-8');
126
+ fileSubs = JSON.parse(content);
127
+ }
128
+ catch { /* 文件不存在 */ }
129
+ const merged = new Map();
130
+ // 先加载文件中的
131
+ for (const s of fileSubs) {
132
+ merged.set(s.endpoint, s);
133
+ }
134
+ // 再合并内存中的
135
+ for (const s of this.subscriptions.values()) {
136
+ merged.set(s.endpoint, s);
137
+ }
138
+ if (type === 'add' && sub) {
139
+ merged.set(sub.endpoint, sub);
140
+ }
141
+ else if (type === 'remove') {
142
+ for (const ep of endpoints) {
143
+ merged.delete(ep);
144
+ }
145
+ }
146
+ const data = Array.from(merged.values());
147
+ await fs.writeFile(this.subscriptionsFile, JSON.stringify(data, null, 2), 'utf-8');
148
+ logger.debug({ count: data.length, type }, 'Persisted subscription change to file');
149
+ });
150
+ }
151
+ /**
152
+ * 初始化 VAPID 密钥和订阅数据。
153
+ * VAPID 密钥生成通过文件锁保护,防止多实例并发生成不同密钥对。
154
+ */
155
+ async initVapidAndSubscriptions() {
156
+ // 确保数据目录存在(锁目录在 baseDir 下,必须先创建父目录)
157
+ await this.ensureDataDir();
158
+ // 使用文件锁保护 VAPID 密钥的 check-then-generate 操作
159
+ await withFileLockAsync(this.vapidLockPath, async () => {
160
+ // 锁内 double-check:先读文件,另一个实例可能已写入
161
+ const savedKeys = await this.loadVapidKeys();
162
+ if (savedKeys) {
163
+ this.vapidPublicKey = savedKeys.publicKey;
164
+ this.vapidPrivateKey = savedKeys.privateKey;
165
+ logger.info('Loaded VAPID keys from file');
166
+ return;
167
+ }
168
+ // 生成新的 VAPID 密钥
169
+ try {
170
+ const webpush = (await import('web-push')).default;
171
+ const keys = webpush.generateVAPIDKeys();
172
+ this.vapidPublicKey = keys.publicKey;
173
+ this.vapidPrivateKey = keys.privateKey;
174
+ await this.saveVapidKeys(keys);
175
+ logger.info('Generated and saved VAPID keys to file');
176
+ }
177
+ catch (err) {
178
+ logger.warn({ err }, 'web-push not available, push notifications disabled');
179
+ }
180
+ });
181
+ // 加载持久化的订阅
182
+ await this.loadSubscriptions();
183
+ }
184
+ getVapidPublicKey() {
185
+ return this.vapidPublicKey;
186
+ }
187
+ async waitForVapidPublicKey(timeoutMs = 2000) {
188
+ if (this.vapidPublicKey)
189
+ return this.vapidPublicKey;
190
+ const timeoutPromise = new Promise((resolve) => {
191
+ setTimeout(() => resolve('timeout'), timeoutMs);
192
+ });
193
+ const result = await Promise.race([
194
+ this.vapidInitPromise.then(() => 'ready'),
195
+ timeoutPromise,
196
+ ]);
197
+ if (result === 'timeout') {
198
+ return this.vapidPublicKey || null;
199
+ }
200
+ return this.vapidPublicKey || null;
201
+ }
202
+ /**
203
+ * 添加推送订阅。
204
+ * 内存立即更新,文件持久化异步执行(最终一致性)。
205
+ */
206
+ subscribe(subscription) {
207
+ this.subscriptions.set(subscription.endpoint, subscription);
208
+ logger.info({ endpoint: subscription.endpoint }, 'Push subscription added');
209
+ this.persistSubscriptionChange('add', [subscription.endpoint], subscription).catch((err) => {
210
+ logger.error({ err }, 'Failed to persist push subscription add');
211
+ });
212
+ }
213
+ /**
214
+ * 移除推送订阅。
215
+ * 内存立即更新,文件持久化异步执行(最终一致性)。
216
+ */
217
+ unsubscribe(endpoint) {
218
+ const deleted = this.subscriptions.delete(endpoint);
219
+ if (deleted) {
220
+ logger.info({ endpoint }, 'Push subscription removed');
221
+ this.persistSubscriptionChange('remove', [endpoint]).catch((err) => {
222
+ logger.error({ err }, 'Failed to persist push subscription remove');
223
+ });
224
+ }
225
+ return deleted;
226
+ }
227
+ get subscriberCount() {
228
+ return this.subscriptions.size;
229
+ }
230
+ /**
231
+ * 验证订阅数据是否有效。
232
+ * p256dh 是 65 字节的 ECDH 公钥,Base64 URL-safe 编码后约 87 字符。
233
+ */
234
+ isValidSubscription(sub) {
235
+ const P256DH_MIN_LENGTH = 64;
236
+ return !!(sub.keys?.p256dh && sub.keys.p256dh.length >= P256DH_MIN_LENGTH);
237
+ }
238
+ async notifyAll(payload) {
239
+ if (!this.vapidPublicKey || !this.vapidPrivateKey) {
240
+ logger.warn('VAPID keys not ready, skipping push notification');
241
+ return;
242
+ }
243
+ // 发送前从文件重新加载所有订阅(获取其他实例添加的订阅),使用文件锁防止读到半写 JSON
244
+ const fileSubs = await this.readSubscriptionsFromFile();
245
+ for (const sub of fileSubs) {
246
+ if (!this.subscriptions.has(sub.endpoint)) {
247
+ // 只加载有效的订阅数据
248
+ if (this.isValidSubscription(sub)) {
249
+ this.subscriptions.set(sub.endpoint, sub);
250
+ }
251
+ else {
252
+ logger.warn({ endpoint: sub.endpoint }, 'Skipping subscription with invalid p256dh from file');
253
+ }
254
+ }
255
+ }
256
+ if (this.subscriptions.size === 0) {
257
+ logger.debug('No push subscribers, skipping notification');
258
+ return;
259
+ }
260
+ let webpush;
261
+ try {
262
+ webpush = (await import('web-push')).default;
263
+ }
264
+ catch {
265
+ logger.warn('web-push not available, skipping notification');
266
+ return;
267
+ }
268
+ webpush.setVapidDetails(this.vapidSubject, this.vapidPublicKey, this.vapidPrivateKey);
269
+ const body = JSON.stringify(payload);
270
+ const promises = [];
271
+ const expiredEndpoints = [];
272
+ for (const [endpoint, sub] of this.subscriptions) {
273
+ // 跳过无效订阅(防御性检查)
274
+ if (!this.isValidSubscription(sub)) {
275
+ logger.warn({ endpoint }, 'Skipping subscription with invalid p256dh');
276
+ continue;
277
+ }
278
+ const p = webpush.sendNotification({ endpoint, keys: sub.keys }, body).catch((err) => {
279
+ // 410 Gone = subscription expired, remove it
280
+ if (err.statusCode === 410) {
281
+ this.subscriptions.delete(endpoint);
282
+ expiredEndpoints.push(endpoint);
283
+ logger.info({ endpoint }, 'Removed expired push subscription');
284
+ }
285
+ else {
286
+ logger.error({ err, endpoint }, 'Failed to send push notification');
287
+ }
288
+ });
289
+ promises.push(p);
290
+ }
291
+ await Promise.allSettled(promises);
292
+ // 有过期订阅被删除时,从内存和文件中同时移除(锁定写)
293
+ if (expiredEndpoints.length > 0) {
294
+ this.persistSubscriptionChange('remove', expiredEndpoints).catch((err) => {
295
+ logger.error({ err }, 'Failed to persist push subscription cleanup');
296
+ });
297
+ }
298
+ logger.info({ count: this.subscriptions.size }, 'Push notifications sent');
299
+ }
300
+ }
301
+ //# sourceMappingURL=push-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-service.js","sourceRoot":"","sources":["../../src/push/push-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAsB1D;;;;;;;;GAQG;AACH,MAAM,OAAO,WAAW;IACd,aAAa,GAAG,IAAI,GAAG,EAAgC,CAAC;IACxD,cAAc,CAAS;IACvB,eAAe,CAAS;IACxB,YAAY,CAAS;IACrB,gBAAgB,CAAgB;IAEvB,aAAa,CAAS;IACtB,aAAa,CAAS;IACtB,iBAAiB,CAAS;IAC1B,qBAAqB,CAAS;IAC9B,OAAO,CAAS;IAEjC,YAAY,OAAe;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;QACtD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC;QAClD,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,OAAO,EAAE,yBAAyB,CAAC,CAAC;QAClE,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC,iBAAiB,GAAG,OAAO,CAAC;QAE9D,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,0BAA0B,CAAC;QAE/E,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAClD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QAEpD,IAAI,SAAS,IAAI,UAAU,EAAE,CAAC;YAC5B,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;YAChC,IAAI,CAAC,eAAe,GAAG,UAAU,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC,6CAA6C,CAAC,CAAC;YAC3D,sBAAsB;YACtB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACnD,CAAC;aAAM,CAAC;YACN,oCAAoC;YACpC,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;YACzB,IAAI,CAAC,eAAe,GAAG,EAAE,CAAC;YAC1B,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,yBAAyB,EAAE,CAAC;QAC3D,CAAC;IACH,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,aAAa;QACzB,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,iCAAiC,CAAC,CAAC;YAC7E,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,aAAa;QACzB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;YAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAc,CAAC;YAC9C,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBACtC,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,aAAa;QACf,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,aAAa,CAAC,IAAe;QACzC,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC3B,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5G,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,iBAAiB;QAC7B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;YACnE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAA2B,CAAC;YAC3D,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACvB,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC5C,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,qCAAqC,CAAC,CAAC;QACzF,CAAC;QAAC,MAAM,CAAC;YACP,qBAAqB;YACrB,MAAM,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,yBAAyB;QACrC,IAAI,CAAC;YACH,OAAO,MAAM,iBAAiB,CAAC,IAAI,CAAC,qBAAqB,EAAE,KAAK,IAAI,EAAE;gBACpE,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;gBACnE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAA2B,CAAC;YACvD,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,yBAAyB,CACrC,IAAsB,EACtB,SAAmB,EACnB,GAA0B;QAE1B,MAAM,iBAAiB,CAAC,IAAI,CAAC,qBAAqB,EAAE,KAAK,IAAI,EAAE;YAC7D,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;YAE3B,IAAI,QAAQ,GAA2B,EAAE,CAAC;YAC1C,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;gBACnE,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAA2B,CAAC;YAC3D,CAAC;YAAC,MAAM,CAAC,CAAC,WAAW,CAAC,CAAC;YAEvB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAgC,CAAC;YAEvD,UAAU;YACV,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;gBACzB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;YAC5B,CAAC;YAED,UAAU;YACV,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC5C,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;YAC5B,CAAC;YAED,IAAI,IAAI,KAAK,KAAK,IAAI,GAAG,EAAE,CAAC;gBAC1B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAChC,CAAC;iBAAM,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC7B,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;oBAC3B,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACpB,CAAC;YACH,CAAC;YAED,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;YACzC,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;YACnF,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,uCAAuC,CAAC,CAAC;QACtF,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,yBAAyB;QACrC,oCAAoC;QACpC,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC3B,2CAA2C;QAC3C,MAAM,iBAAiB,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,IAAI,EAAE;YACrD,kCAAkC;YAClC,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;YAC7C,IAAI,SAAS,EAAE,CAAC;gBACd,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC,SAAS,CAAC;gBAC1C,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC,UAAU,CAAC;gBAC5C,MAAM,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;gBAC3C,OAAO;YACT,CAAC;YAED,gBAAgB;YAChB,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;gBACnD,MAAM,IAAI,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;gBACzC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC;gBACrC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,UAAU,CAAC;gBACvC,MAAM,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;gBAC/B,MAAM,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC;YACxD,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,qDAAqD,CAAC,CAAC;YAC9E,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,WAAW;QACX,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;IACjC,CAAC;IAED,iBAAiB;QACf,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,qBAAqB,CAAC,YAAoB,IAAI;QAClD,IAAI,IAAI,CAAC,cAAc;YAAE,OAAO,IAAI,CAAC,cAAc,CAAC;QAEpD,MAAM,cAAc,GAAG,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,EAAE;YACxD,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC;QAClD,CAAC,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YAChC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAgB,CAAC;YAClD,cAAc;SACf,CAAC,CAAC;QAEH,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC;QACrC,CAAC;QAED,OAAO,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC;IACrC,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,YAAkC;QAC1C,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,YAAY,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QAC5D,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,YAAY,CAAC,QAAQ,EAAE,EAAE,yBAAyB,CAAC,CAAC;QAC5E,IAAI,CAAC,yBAAyB,CAAC,KAAK,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACzF,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,yCAAyC,CAAC,CAAC;QACnE,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,QAAgB;QAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,2BAA2B,CAAC,CAAC;YACvD,IAAI,CAAC,yBAAyB,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACjE,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,4CAA4C,CAAC,CAAC;YACtE,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC;IACjC,CAAC;IAED;;;OAGG;IACK,mBAAmB,CAAC,GAAyB;QACnD,MAAM,iBAAiB,GAAG,EAAE,CAAC;QAC7B,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,iBAAiB,CAAC,CAAC;IAC7E,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,OAAgC;QAC9C,IAAI,CAAC,IAAI,CAAC,cAAc,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;YAClD,MAAM,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC;YAChE,OAAO;QACT,CAAC;QAED,+CAA+C;QAC/C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,yBAAyB,EAAE,CAAC;QACxD,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC1C,aAAa;gBACb,IAAI,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;oBAClC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;gBAC5C,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,qDAAqD,CAAC,CAAC;gBACjG,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,IAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAClC,MAAM,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QAED,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;QAC/C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QAED,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;QAEtF,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAuB,EAAE,CAAC;QACxC,MAAM,gBAAgB,GAAa,EAAE,CAAC;QAEtC,KAAK,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACjD,gBAAgB;YAChB,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;gBACnC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,2CAA2C,CAAC,CAAC;gBACvE,SAAS;YACX,CAAC;YAED,MAAM,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAChC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,EAC5B,IAAI,CACL,CAAC,KAAK,CAAC,CAAC,GAAQ,EAAE,EAAE;gBACnB,6CAA6C;gBAC7C,IAAI,GAAG,CAAC,UAAU,KAAK,GAAG,EAAE,CAAC;oBAC3B,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;oBACpC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;oBAChC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,mCAAmC,CAAC,CAAC;gBACjE,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,kCAAkC,CAAC,CAAC;gBACtE,CAAC;YACH,CAAC,CAAC,CAAC;YACH,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAEnC,6BAA6B;QAC7B,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,yBAAyB,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACvE,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,6CAA6C,CAAC,CAAC;YACvE,CAAC,CAAC,CAAC;QACL,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,yBAAyB,CAAC,CAAC;IAC7E,CAAC;CACF"}
@@ -0,0 +1,32 @@
1
+ import type { InstanceInfo } from '@claude-remote/shared';
2
+ /**
3
+ * 实例注册表管理器。
4
+ * 操作 ~/.claude-remote/instances.json 实现多实例发现。
5
+ * 所有公共方法通过 mkdir-based 文件锁保护 read-modify-write 操作。
6
+ */
7
+ export declare class InstanceRegistryManager {
8
+ private readonly registryPath;
9
+ private readonly lockPath;
10
+ private readonly baseDir;
11
+ constructor(baseDir: string);
12
+ /**
13
+ * 注册实例信息(如已存在同 ID 则替换)。
14
+ */
15
+ register(info: InstanceInfo): void;
16
+ /**
17
+ * 注销实例。
18
+ */
19
+ unregister(instanceId: string): void;
20
+ /**
21
+ * 列出所有存活实例,自动清理僵尸进程。
22
+ * 使用异步文件锁,避免在 API 请求路径上阻塞事件循环。
23
+ */
24
+ list(): Promise<InstanceInfo[]>;
25
+ /**
26
+ * 更新实例的 host 字段(用于 IP 变化时更新)。
27
+ */
28
+ updateHost(instanceId: string, newHost: string): void;
29
+ private readRegistry;
30
+ private writeRegistry;
31
+ }
32
+ //# sourceMappingURL=instance-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-registry.d.ts","sourceRoot":"","sources":["../../src/registry/instance-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,uBAAuB,CAAC;AAgB5E;;;;GAIG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,EAAE,MAAM;IAM3B;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAUlC;;OAEG;IACH,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAYpC;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAgBrC;;OAEG;IACH,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAYrD,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,aAAa;CAStB"}
@@ -0,0 +1,115 @@
1
+ import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { REGISTRY_FILENAME } from '../../shared-dist/index.js';
4
+ import { logger } from '../logger/logger.js';
5
+ import { withFileLock, withFileLockAsync } from '../utils/file-lock.js';
6
+ /**
7
+ * 检查进程是否存活。
8
+ */
9
+ function isProcessAlive(pid) {
10
+ try {
11
+ process.kill(pid, 0);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ /**
19
+ * 实例注册表管理器。
20
+ * 操作 ~/.claude-remote/instances.json 实现多实例发现。
21
+ * 所有公共方法通过 mkdir-based 文件锁保护 read-modify-write 操作。
22
+ */
23
+ export class InstanceRegistryManager {
24
+ registryPath;
25
+ lockPath;
26
+ baseDir;
27
+ constructor(baseDir) {
28
+ this.baseDir = baseDir;
29
+ this.registryPath = join(baseDir, REGISTRY_FILENAME);
30
+ this.lockPath = this.registryPath + '.lock';
31
+ }
32
+ /**
33
+ * 注册实例信息(如已存在同 ID 则替换)。
34
+ */
35
+ register(info) {
36
+ withFileLock(this.lockPath, () => {
37
+ const data = this.readRegistry();
38
+ data.instances = data.instances.filter(i => i.instanceId !== info.instanceId);
39
+ data.instances.push(info);
40
+ this.writeRegistry(data);
41
+ logger.info({ instanceId: info.instanceId, name: info.name, port: info.port }, 'Instance registered');
42
+ });
43
+ }
44
+ /**
45
+ * 注销实例。
46
+ */
47
+ unregister(instanceId) {
48
+ withFileLock(this.lockPath, () => {
49
+ const data = this.readRegistry();
50
+ const before = data.instances.length;
51
+ data.instances = data.instances.filter(i => i.instanceId !== instanceId);
52
+ if (data.instances.length < before) {
53
+ this.writeRegistry(data);
54
+ logger.info({ instanceId }, 'Instance unregistered');
55
+ }
56
+ });
57
+ }
58
+ /**
59
+ * 列出所有存活实例,自动清理僵尸进程。
60
+ * 使用异步文件锁,避免在 API 请求路径上阻塞事件循环。
61
+ */
62
+ async list() {
63
+ return withFileLockAsync(this.lockPath, async () => {
64
+ const data = this.readRegistry();
65
+ const alive = data.instances.filter(i => isProcessAlive(i.pid));
66
+ if (alive.length < data.instances.length) {
67
+ const removed = data.instances.length - alive.length;
68
+ logger.info({ removed }, 'Cleaned up zombie instances');
69
+ data.instances = alive;
70
+ this.writeRegistry(data);
71
+ }
72
+ return alive;
73
+ });
74
+ }
75
+ /**
76
+ * 更新实例的 host 字段(用于 IP 变化时更新)。
77
+ */
78
+ updateHost(instanceId, newHost) {
79
+ withFileLock(this.lockPath, () => {
80
+ const data = this.readRegistry();
81
+ const instance = data.instances.find(i => i.instanceId === instanceId);
82
+ if (instance && instance.host !== newHost) {
83
+ instance.host = newHost;
84
+ this.writeRegistry(data);
85
+ logger.info({ instanceId, newHost }, 'Instance host updated');
86
+ }
87
+ });
88
+ }
89
+ readRegistry() {
90
+ if (!existsSync(this.registryPath)) {
91
+ return { version: 1, instances: [] };
92
+ }
93
+ try {
94
+ const content = readFileSync(this.registryPath, 'utf-8');
95
+ const data = JSON.parse(content);
96
+ if (!data.instances || !Array.isArray(data.instances)) {
97
+ return { version: 1, instances: [] };
98
+ }
99
+ return data;
100
+ }
101
+ catch (err) {
102
+ logger.warn({ err, path: this.registryPath }, 'Failed to read registry, starting fresh');
103
+ return { version: 1, instances: [] };
104
+ }
105
+ }
106
+ writeRegistry(data) {
107
+ if (!existsSync(this.baseDir)) {
108
+ mkdirSync(this.baseDir, { recursive: true, mode: 0o700 });
109
+ }
110
+ const tmpPath = `${this.registryPath}.tmp.${process.pid}`;
111
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2), { mode: 0o600 });
112
+ renameSync(tmpPath, this.registryPath);
113
+ }
114
+ }
115
+ //# sourceMappingURL=instance-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-registry.js","sourceRoot":"","sources":["../../src/registry/instance-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ export interface SpawnOptions {
2
+ /** 工作目录 */
3
+ cwd: string;
4
+ /** 实例名称 */
5
+ name?: string;
6
+ /** Claude 额外参数 */
7
+ claudeArgs?: string[];
8
+ /** 是否为 headless 模式(无终端) */
9
+ headless?: boolean;
10
+ /** 指定端口(可选,默认自动分配) */
11
+ port?: number;
12
+ }
13
+ export interface SpawnResult {
14
+ /** 进程 PID */
15
+ pid: number;
16
+ /** 工作目录 */
17
+ cwd: string;
18
+ /** 实例名称 */
19
+ name: string;
20
+ }
21
+ /**
22
+ * 实例创建服务。
23
+ * 通过 spawn 子进程启动新的 claude-remote 实例。
24
+ */
25
+ export declare class InstanceSpawner {
26
+ private readonly entryScript;
27
+ constructor();
28
+ /**
29
+ * 创建新实例。
30
+ */
31
+ spawn(options: SpawnOptions): Promise<SpawnResult>;
32
+ }
33
+ //# sourceMappingURL=instance-spawner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-spawner.d.ts","sourceRoot":"","sources":["../../src/registry/instance-spawner.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,YAAY;IAC3B,WAAW;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;;IAcrC;;OAEG;IACG,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;CAkFzD"}
@@ -0,0 +1,91 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { logger } from '../logger/logger.js';
6
+ /**
7
+ * 实例创建服务。
8
+ * 通过 spawn 子进程启动新的 claude-remote 实例。
9
+ */
10
+ export class InstanceSpawner {
11
+ entryScript;
12
+ constructor() {
13
+ // 获取当前模块所在目录
14
+ const currentDir = dirname(fileURLToPath(import.meta.url));
15
+ // 入口脚本路径(CLI 入口)
16
+ this.entryScript = resolve(currentDir, '../cli.js');
17
+ // 验证入口脚本存在
18
+ if (!existsSync(this.entryScript)) {
19
+ throw new Error(`Entry script not found: ${this.entryScript}`);
20
+ }
21
+ }
22
+ /**
23
+ * 创建新实例。
24
+ */
25
+ async spawn(options) {
26
+ const { cwd, name, claudeArgs = [], headless = true, port, } = options;
27
+ logger.info({
28
+ cwd,
29
+ name,
30
+ headless,
31
+ port,
32
+ }, 'InstanceSpawner: spawning new instance');
33
+ // 构建子进程参数
34
+ const args = [];
35
+ // 禁用 CLI 模式输出,改用环境变量控制
36
+ // headless 模式下传递 --no-terminal 标志
37
+ if (headless) {
38
+ args.push('--no-terminal');
39
+ }
40
+ if (name) {
41
+ args.push('--name', name);
42
+ }
43
+ if (port) {
44
+ args.push('--port', String(port));
45
+ }
46
+ // 添加 Claude 参数分隔符
47
+ if (claudeArgs.length > 0) {
48
+ args.push('--', ...claudeArgs);
49
+ }
50
+ return new Promise((resolve, reject) => {
51
+ const child = spawn(process.execPath, [this.entryScript, ...args], {
52
+ cwd,
53
+ detached: true, // 与父进程解绑,允许父进程退出后继续运行
54
+ stdio: 'ignore', // headless 模式忽略 stdio
55
+ // 继承父进程的环境变量,并添加自定义变量
56
+ env: {
57
+ ...process.env,
58
+ // 设置 headless 标志供 index.ts 使用
59
+ NO_TERMINAL: headless ? 'true' : 'false',
60
+ },
61
+ });
62
+ const childPid = child.pid;
63
+ child.on('error', (err) => {
64
+ logger.error({ err, cwd, name }, 'InstanceSpawner: failed to spawn');
65
+ reject(err);
66
+ });
67
+ // 子进程启动后立即 unref,让父进程可以独立退出
68
+ child.unref();
69
+ // 给子进程一点启动时间,然后检查是否存活
70
+ setTimeout(() => {
71
+ try {
72
+ process.kill(childPid, 0); // 检查进程是否存在
73
+ logger.info({
74
+ pid: childPid,
75
+ cwd,
76
+ name,
77
+ }, 'InstanceSpawner: instance spawned successfully');
78
+ resolve({
79
+ pid: childPid,
80
+ cwd,
81
+ name: name ?? cwd.split('/').pop() ?? 'unknown',
82
+ });
83
+ }
84
+ catch {
85
+ reject(new Error('Spawned process exited immediately'));
86
+ }
87
+ }, 500);
88
+ });
89
+ }
90
+ }
91
+ //# sourceMappingURL=instance-spawner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-spawner.js","sourceRoot":"","sources":["../../src/registry/instance-spawner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 从 preferredPort 开始递增查找可用端口。
3
+ * @param preferredPort 首选端口
4
+ * @param host 绑定地址
5
+ * @param maxAttempts 最大尝试次数(默认 100)
6
+ */
7
+ export declare function findAvailablePort(preferredPort: number, host: string, maxAttempts?: number): Promise<number>;
8
+ //# sourceMappingURL=port-finder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-finder.d.ts","sourceRoot":"","sources":["../../src/registry/port-finder.ts"],"names":[],"mappings":"AAgBA;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,MAAM,EACZ,WAAW,GAAE,MAAY,GACxB,OAAO,CAAC,MAAM,CAAC,CAcjB"}
@@ -0,0 +1,35 @@
1
+ import { createServer } from 'node:net';
2
+ import { logger } from '../logger/logger.js';
3
+ /**
4
+ * 检测指定端口是否可用。
5
+ */
6
+ function isPortAvailable(port, host) {
7
+ return new Promise((resolve) => {
8
+ const server = createServer();
9
+ server.once('error', () => resolve(false));
10
+ server.listen(port, host, () => {
11
+ server.close(() => resolve(true));
12
+ });
13
+ });
14
+ }
15
+ /**
16
+ * 从 preferredPort 开始递增查找可用端口。
17
+ * @param preferredPort 首选端口
18
+ * @param host 绑定地址
19
+ * @param maxAttempts 最大尝试次数(默认 100)
20
+ */
21
+ export async function findAvailablePort(preferredPort, host, maxAttempts = 100) {
22
+ for (let i = 0; i < maxAttempts; i++) {
23
+ const port = preferredPort + i;
24
+ if (port > 65535)
25
+ break;
26
+ if (await isPortAvailable(port, host)) {
27
+ if (i > 0) {
28
+ logger.info({ preferredPort, assignedPort: port }, 'Preferred port occupied, using alternative');
29
+ }
30
+ return port;
31
+ }
32
+ }
33
+ throw new Error(`No available port found starting from ${preferredPort} after ${maxAttempts} attempts`);
34
+ }
35
+ //# sourceMappingURL=port-finder.js.map