tabctl 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,667 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __commonJS = (cb, mod) => function __require() {
5
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
6
+ };
7
+
8
+ // dist/shared/config.js
9
+ var require_config = __commonJS({
10
+ "dist/shared/config.js"(exports2) {
11
+ "use strict";
12
+ var __importDefault2 = exports2 && exports2.__importDefault || function(mod) {
13
+ return mod && mod.__esModule ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports2, "__esModule", { value: true });
16
+ exports2.resolveSocketPath = resolveSocketPath;
17
+ exports2.resetConfig = resetConfig;
18
+ exports2.expandEnvVars = expandEnvVars;
19
+ exports2.resolveConfig = resolveConfig;
20
+ var os_1 = __importDefault2(require("os"));
21
+ var path_1 = __importDefault2(require("path"));
22
+ var crypto_12 = __importDefault2(require("crypto"));
23
+ var fs_12 = __importDefault2(require("fs"));
24
+ function defaultConfigBase() {
25
+ if (process.platform === "win32") {
26
+ return process.env.APPDATA || path_1.default.join(os_1.default.homedir(), "AppData", "Roaming");
27
+ }
28
+ return path_1.default.join(os_1.default.homedir(), ".config");
29
+ }
30
+ function defaultStateBase() {
31
+ if (process.platform === "win32") {
32
+ return process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), "AppData", "Local");
33
+ }
34
+ return path_1.default.join(os_1.default.homedir(), ".local", "state");
35
+ }
36
+ function resolveSocketPath(dataDir) {
37
+ if (process.platform === "win32") {
38
+ const hash = crypto_12.default.createHash("sha256").update(dataDir).digest("hex").slice(0, 12);
39
+ return `\\\\.\\pipe\\tabctl-${hash}`;
40
+ }
41
+ return path_1.default.join(dataDir, "tabctl.sock");
42
+ }
43
+ var cached;
44
+ function resetConfig() {
45
+ cached = void 0;
46
+ }
47
+ function expandEnvVars(value) {
48
+ return value.replace(/\$\{(\w+)\}|\$(\w+)/g, (match, braced, bare) => {
49
+ const varName = braced || bare;
50
+ return process.env[varName] ?? match;
51
+ });
52
+ }
53
+ function resolveConfig(profileName) {
54
+ if (!profileName && cached)
55
+ return cached;
56
+ const configDir = process.env.TABCTL_CONFIG_DIR || path_1.default.join(process.env.XDG_CONFIG_HOME || defaultConfigBase(), "tabctl");
57
+ let fileConfig = {};
58
+ try {
59
+ const raw = fs_12.default.readFileSync(path_1.default.join(configDir, "config.json"), "utf-8");
60
+ fileConfig = JSON.parse(raw);
61
+ } catch {
62
+ }
63
+ let dataDir;
64
+ if (typeof fileConfig.dataDir === "string" && fileConfig.dataDir) {
65
+ dataDir = expandEnvVars(fileConfig.dataDir);
66
+ if (!path_1.default.isAbsolute(dataDir)) {
67
+ throw new Error(`dataDir in config.json must be an absolute path (got: ${dataDir}). Use $HOME or full paths.`);
68
+ }
69
+ } else if (process.env.TABCTL_CONFIG_DIR) {
70
+ dataDir = path_1.default.join(configDir, "data");
71
+ } else {
72
+ dataDir = path_1.default.join(process.env.XDG_STATE_HOME || defaultStateBase(), "tabctl");
73
+ }
74
+ const baseDataDir = dataDir;
75
+ const explicitProfile = profileName;
76
+ const effectiveProfile = profileName || process.env.TABCTL_PROFILE;
77
+ let activeProfileName;
78
+ if (effectiveProfile) {
79
+ try {
80
+ const raw = fs_12.default.readFileSync(path_1.default.join(configDir, "profiles.json"), "utf-8");
81
+ const registry = JSON.parse(raw);
82
+ const profile = registry.profiles[effectiveProfile];
83
+ if (profile) {
84
+ dataDir = profile.dataDir;
85
+ activeProfileName = effectiveProfile;
86
+ } else if (explicitProfile) {
87
+ throw new Error(`Profile "${explicitProfile}" not found in profiles.json`);
88
+ }
89
+ } catch (err) {
90
+ if (err instanceof Error && err.message.includes("not found in profiles.json")) {
91
+ throw err;
92
+ }
93
+ if (explicitProfile) {
94
+ throw new Error(`Profile "${explicitProfile}" not found: no profiles.json`);
95
+ }
96
+ }
97
+ } else {
98
+ try {
99
+ const raw = fs_12.default.readFileSync(path_1.default.join(configDir, "profiles.json"), "utf-8");
100
+ const registry = JSON.parse(raw);
101
+ if (registry.default && registry.profiles[registry.default]) {
102
+ dataDir = registry.profiles[registry.default].dataDir;
103
+ activeProfileName = registry.default;
104
+ }
105
+ } catch {
106
+ }
107
+ }
108
+ const result = {
109
+ configDir,
110
+ dataDir,
111
+ baseDataDir,
112
+ socketPath: process.env.TABCTL_SOCKET || resolveSocketPath(dataDir),
113
+ undoLog: path_1.default.join(dataDir, "undo.jsonl"),
114
+ wrapperDir: dataDir,
115
+ policyPath: path_1.default.join(configDir, "policy.json"),
116
+ activeProfileName
117
+ };
118
+ if (!profileName) {
119
+ cached = result;
120
+ }
121
+ return result;
122
+ }
123
+ }
124
+ });
125
+
126
+ // dist/shared/version.js
127
+ var require_version = __commonJS({
128
+ "dist/shared/version.js"(exports2) {
129
+ "use strict";
130
+ Object.defineProperty(exports2, "__esModule", { value: true });
131
+ exports2.DIRTY = exports2.GIT_SHA = exports2.VERSION = exports2.BASE_VERSION = void 0;
132
+ exports2.BASE_VERSION = "0.3.1";
133
+ exports2.VERSION = "0.3.1";
134
+ exports2.GIT_SHA = null;
135
+ exports2.DIRTY = false;
136
+ }
137
+ });
138
+
139
+ // dist/host/lib/undo.js
140
+ var require_undo = __commonJS({
141
+ "dist/host/lib/undo.js"(exports2) {
142
+ "use strict";
143
+ var __importDefault2 = exports2 && exports2.__importDefault || function(mod) {
144
+ return mod && mod.__esModule ? mod : { "default": mod };
145
+ };
146
+ Object.defineProperty(exports2, "__esModule", { value: true });
147
+ exports2.appendUndoRecord = appendUndoRecord;
148
+ exports2.readUndoRecords = readUndoRecords;
149
+ exports2.filterByRetention = filterByRetention;
150
+ exports2.findUndoRecord = findUndoRecord;
151
+ exports2.findLatestUndoRecord = findLatestUndoRecord;
152
+ var fs_12 = __importDefault2(require("fs"));
153
+ var path_1 = __importDefault2(require("path"));
154
+ var DEFAULT_RETENTION_DAYS = 30;
155
+ function appendUndoRecord(filePath, record) {
156
+ const dir = path_1.default.dirname(filePath);
157
+ fs_12.default.mkdirSync(dir, { recursive: true, mode: 448 });
158
+ fs_12.default.appendFileSync(filePath, `${JSON.stringify(record)}
159
+ `, "utf8");
160
+ }
161
+ function readUndoRecords(filePath) {
162
+ try {
163
+ const content = fs_12.default.readFileSync(filePath, "utf8");
164
+ const lines = content.split("\n").filter(Boolean);
165
+ const records = [];
166
+ for (const line of lines) {
167
+ try {
168
+ records.push(JSON.parse(line));
169
+ } catch {
170
+ }
171
+ }
172
+ return records;
173
+ } catch {
174
+ return [];
175
+ }
176
+ }
177
+ function filterByRetention(records, retentionDays = DEFAULT_RETENTION_DAYS, now = Date.now()) {
178
+ const cutoff = now - retentionDays * 24 * 60 * 60 * 1e3;
179
+ return records.filter((record) => {
180
+ const createdAt = record.createdAt;
181
+ return !createdAt || createdAt >= cutoff;
182
+ });
183
+ }
184
+ function findUndoRecord(filePath, txid, retentionDays = DEFAULT_RETENTION_DAYS, now = Date.now()) {
185
+ const records = filterByRetention(readUndoRecords(filePath), retentionDays, now);
186
+ for (let i = records.length - 1; i >= 0; i -= 1) {
187
+ if (records[i].txid === txid) {
188
+ return records[i];
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+ function findLatestUndoRecord(filePath, retentionDays = DEFAULT_RETENTION_DAYS, now = Date.now()) {
194
+ const records = filterByRetention(readUndoRecords(filePath), retentionDays, now);
195
+ if (!records.length) {
196
+ return null;
197
+ }
198
+ return records[records.length - 1] || null;
199
+ }
200
+ }
201
+ });
202
+
203
+ // dist/host/lib/handlers.js
204
+ var require_handlers = __commonJS({
205
+ "dist/host/lib/handlers.js"(exports2) {
206
+ "use strict";
207
+ Object.defineProperty(exports2, "__esModule", { value: true });
208
+ exports2.respond = respond;
209
+ exports2.refreshTimeout = refreshTimeout;
210
+ exports2.forwardToExtension = forwardToExtension;
211
+ exports2.handleNativeMessage = handleNativeMessage2;
212
+ exports2.handleCliRequest = handleCliRequest2;
213
+ var version_1 = require_version();
214
+ var undo_1 = require_undo();
215
+ var REQUEST_TIMEOUT_MS = 3e4;
216
+ var MAX_RESPONSE_BYTES = 20 * 1024 * 1024;
217
+ var HISTORY_LIMIT_DEFAULT = 20;
218
+ var RETENTION_DAYS = 30;
219
+ var UNDO_ACTIONS = /* @__PURE__ */ new Set([
220
+ "archive",
221
+ "close",
222
+ "group-update",
223
+ "group-ungroup",
224
+ "group-assign",
225
+ "move-tab",
226
+ "move-group",
227
+ "merge-window"
228
+ ]);
229
+ var LOCAL_ACTIONS = /* @__PURE__ */ new Set(["history", "undo", "version"]);
230
+ function respond(socket, payload) {
231
+ const serialized = JSON.stringify(payload);
232
+ if (Buffer.byteLength(serialized, "utf8") > MAX_RESPONSE_BYTES) {
233
+ socket.write(`${JSON.stringify({
234
+ ok: false,
235
+ action: payload.action,
236
+ requestId: payload.requestId,
237
+ component: "host",
238
+ version: version_1.VERSION,
239
+ error: { message: "Response too large", hint: "Reduce scope or use --out to write files." }
240
+ })}
241
+ `);
242
+ return;
243
+ }
244
+ socket.write(`${serialized}
245
+ `);
246
+ }
247
+ function refreshTimeout(deps2, pendingRequest, requestId) {
248
+ clearTimeout(pendingRequest.timeout);
249
+ pendingRequest.timeout = setTimeout(() => {
250
+ deps2.pending.delete(requestId);
251
+ respond(pendingRequest.socket, {
252
+ ok: false,
253
+ action: pendingRequest.action,
254
+ requestId,
255
+ component: "host",
256
+ version: version_1.VERSION,
257
+ error: { message: "Request timed out" }
258
+ });
259
+ }, REQUEST_TIMEOUT_MS);
260
+ }
261
+ function forwardToExtension(deps2, socket, request, overrides = {}) {
262
+ const requestId = request.id || deps2.createId("req");
263
+ const txid = overrides.txid || null;
264
+ const params = { ...request.params || {} };
265
+ if (txid) {
266
+ params.txid = txid;
267
+ }
268
+ if (!LOCAL_ACTIONS.has(request.action)) {
269
+ params.client = {
270
+ component: "host",
271
+ version: version_1.VERSION
272
+ };
273
+ }
274
+ deps2.pending.set(requestId, {
275
+ socket,
276
+ action: request.action,
277
+ txid,
278
+ timeout: setTimeout(() => {
279
+ deps2.pending.delete(requestId);
280
+ respond(socket, {
281
+ ok: false,
282
+ action: request.action,
283
+ requestId,
284
+ component: "host",
285
+ version: version_1.VERSION,
286
+ error: { message: "Request timed out" }
287
+ });
288
+ }, REQUEST_TIMEOUT_MS)
289
+ });
290
+ deps2.sendNative({ id: requestId, action: request.action, params });
291
+ }
292
+ function handleNativeMessage2(deps2, payload) {
293
+ let message;
294
+ try {
295
+ message = JSON.parse(payload);
296
+ } catch (error) {
297
+ const err = error;
298
+ deps2.log("Failed to parse native message", err.message);
299
+ return;
300
+ }
301
+ const messageId = message.id;
302
+ if (!messageId) {
303
+ return;
304
+ }
305
+ const pendingRequest = deps2.pending.get(messageId);
306
+ if (!pendingRequest) {
307
+ return;
308
+ }
309
+ if (message.progress) {
310
+ refreshTimeout(deps2, pendingRequest, messageId);
311
+ respond(pendingRequest.socket, {
312
+ ok: true,
313
+ action: pendingRequest.action,
314
+ requestId: messageId,
315
+ progress: true,
316
+ component: "host",
317
+ version: version_1.VERSION,
318
+ data: message.data || {}
319
+ });
320
+ return;
321
+ }
322
+ const messageData = message.data || {};
323
+ const extensionVersion = typeof messageData.version === "string" ? messageData.version : null;
324
+ const extensionComponent = typeof messageData.component === "string" ? messageData.component : null;
325
+ if (extensionVersion && extensionVersion !== version_1.VERSION) {
326
+ deps2.log(`Version mismatch: host ${version_1.VERSION}, extension ${extensionVersion}`);
327
+ }
328
+ clearTimeout(pendingRequest.timeout);
329
+ deps2.pending.delete(messageId);
330
+ if (!message.ok) {
331
+ respond(pendingRequest.socket, {
332
+ ok: false,
333
+ action: pendingRequest.action,
334
+ requestId: messageId,
335
+ component: "host",
336
+ version: version_1.VERSION,
337
+ error: message.error || { message: "Unknown error" }
338
+ });
339
+ return;
340
+ }
341
+ if (pendingRequest.action === "analyze") {
342
+ const analysisId = deps2.createId("analysis");
343
+ deps2.analyses.set(analysisId, {
344
+ createdAt: Date.now(),
345
+ data: messageData
346
+ });
347
+ respond(pendingRequest.socket, {
348
+ ok: true,
349
+ action: "analyze",
350
+ requestId: messageId,
351
+ component: "host",
352
+ version: version_1.VERSION,
353
+ data: {
354
+ ...messageData,
355
+ extensionVersion,
356
+ extensionComponent,
357
+ hostBaseVersion: version_1.BASE_VERSION,
358
+ hostGitSha: version_1.GIT_SHA,
359
+ hostDirty: version_1.DIRTY,
360
+ analysisId
361
+ }
362
+ });
363
+ return;
364
+ }
365
+ if (UNDO_ACTIONS.has(pendingRequest.action)) {
366
+ const record = {
367
+ txid: pendingRequest.txid,
368
+ createdAt: Date.now(),
369
+ action: pendingRequest.action,
370
+ summary: messageData.summary || {},
371
+ undo: messageData.undo || null
372
+ };
373
+ if (record.undo) {
374
+ (0, undo_1.appendUndoRecord)(deps2.undoLog, record);
375
+ }
376
+ respond(pendingRequest.socket, {
377
+ ok: true,
378
+ action: pendingRequest.action,
379
+ requestId: messageId,
380
+ component: "host",
381
+ version: version_1.VERSION,
382
+ data: {
383
+ ...messageData,
384
+ extensionVersion,
385
+ extensionComponent,
386
+ hostBaseVersion: version_1.BASE_VERSION,
387
+ hostGitSha: version_1.GIT_SHA,
388
+ hostDirty: version_1.DIRTY,
389
+ txid: pendingRequest.txid
390
+ }
391
+ });
392
+ return;
393
+ }
394
+ respond(pendingRequest.socket, {
395
+ ok: true,
396
+ action: pendingRequest.action,
397
+ requestId: messageId,
398
+ component: "host",
399
+ version: version_1.VERSION,
400
+ data: {
401
+ ...messageData,
402
+ extensionVersion,
403
+ extensionComponent,
404
+ hostBaseVersion: version_1.BASE_VERSION,
405
+ hostGitSha: version_1.GIT_SHA,
406
+ hostDirty: version_1.DIRTY
407
+ }
408
+ });
409
+ }
410
+ function handleCliRequest2(deps2, socket, request) {
411
+ if (!request || typeof request !== "object") {
412
+ respond(socket, { ok: false, error: { message: "Invalid request" } });
413
+ return;
414
+ }
415
+ const action = request.action;
416
+ if (!action) {
417
+ respond(socket, { ok: false, error: { message: "Missing action" } });
418
+ return;
419
+ }
420
+ if (action === "history") {
421
+ const limit = Number.isFinite(request.params?.limit) ? Number(request.params?.limit) : HISTORY_LIMIT_DEFAULT;
422
+ const records = (0, undo_1.readUndoRecords)(deps2.undoLog);
423
+ const filtered = (0, undo_1.filterByRetention)(records, RETENTION_DAYS);
424
+ respond(socket, {
425
+ ok: true,
426
+ action,
427
+ requestId: request.id || null,
428
+ component: "host",
429
+ version: version_1.VERSION,
430
+ data: filtered.slice(-limit)
431
+ });
432
+ return;
433
+ }
434
+ if (action === "version") {
435
+ respond(socket, {
436
+ ok: true,
437
+ action,
438
+ requestId: request.id || null,
439
+ component: "host",
440
+ version: version_1.VERSION,
441
+ data: {
442
+ version: version_1.VERSION,
443
+ baseVersion: version_1.BASE_VERSION,
444
+ gitSha: version_1.GIT_SHA,
445
+ dirty: version_1.DIRTY,
446
+ component: "host"
447
+ }
448
+ });
449
+ return;
450
+ }
451
+ if (action === "undo") {
452
+ const txid = request.params?.txid;
453
+ const latest = request.params?.latest === true;
454
+ if (!txid && !latest) {
455
+ respond(socket, {
456
+ ok: false,
457
+ action,
458
+ component: "host",
459
+ version: version_1.VERSION,
460
+ error: {
461
+ message: "Missing txid",
462
+ hint: "Use tabctl history --json to find a txid, or run tabctl undo --latest"
463
+ }
464
+ });
465
+ return;
466
+ }
467
+ const record = txid ? (0, undo_1.findUndoRecord)(deps2.undoLog, txid, RETENTION_DAYS) : (0, undo_1.findLatestUndoRecord)(deps2.undoLog, RETENTION_DAYS);
468
+ if (!record) {
469
+ respond(socket, { ok: false, action, component: "host", version: version_1.VERSION, error: { message: "Undo record not found" } });
470
+ return;
471
+ }
472
+ forwardToExtension(deps2, socket, {
473
+ id: request.id,
474
+ action: "undo",
475
+ params: { record }
476
+ });
477
+ return;
478
+ }
479
+ if (action === "close" && request.params?.mode === "apply") {
480
+ const analysisId = request.params.analysisId;
481
+ const analysis = analysisId ? deps2.analyses.get(analysisId) : void 0;
482
+ if (!analysis) {
483
+ respond(socket, { ok: false, action, component: "host", version: version_1.VERSION, error: { message: "Unknown analysisId" } });
484
+ return;
485
+ }
486
+ const candidates = analysis.data.candidates || [];
487
+ const tabIds = candidates.map((candidate) => candidate.tabId).filter(Boolean);
488
+ const expectedUrls = {};
489
+ for (const candidate of candidates) {
490
+ if (candidate.tabId) {
491
+ expectedUrls[String(candidate.tabId)] = candidate.url;
492
+ }
493
+ }
494
+ if (!tabIds.length) {
495
+ respond(socket, {
496
+ ok: true,
497
+ action,
498
+ requestId: request.id || null,
499
+ component: "host",
500
+ version: version_1.VERSION,
501
+ data: {
502
+ txid: null,
503
+ summary: { closedTabs: 0, skippedTabs: 0 },
504
+ skipped: []
505
+ }
506
+ });
507
+ return;
508
+ }
509
+ const txid = deps2.createId("tx");
510
+ forwardToExtension(deps2, socket, {
511
+ id: request.id,
512
+ action: "close",
513
+ params: {
514
+ mode: "apply",
515
+ tabIds,
516
+ expectedUrls
517
+ }
518
+ }, { txid });
519
+ return;
520
+ }
521
+ if (UNDO_ACTIONS.has(action)) {
522
+ const txid = deps2.createId("tx");
523
+ forwardToExtension(deps2, socket, request, { txid });
524
+ return;
525
+ }
526
+ forwardToExtension(deps2, socket, request);
527
+ }
528
+ }
529
+ });
530
+
531
+ // dist/host/host.js
532
+ var __importDefault = exports && exports.__importDefault || function(mod) {
533
+ return mod && mod.__esModule ? mod : { "default": mod };
534
+ };
535
+ Object.defineProperty(exports, "__esModule", { value: true });
536
+ var fs_1 = __importDefault(require("fs"));
537
+ var net_1 = __importDefault(require("net"));
538
+ var crypto_1 = __importDefault(require("crypto"));
539
+ var config_1 = require_config();
540
+ var handlers_1 = require_handlers();
541
+ var config;
542
+ try {
543
+ config = (0, config_1.resolveConfig)();
544
+ } catch (err) {
545
+ process.stderr.write(`[tabctl-host] Fatal: ${err.message}
546
+ `);
547
+ process.exit(1);
548
+ }
549
+ var SOCKET_DIR = config.dataDir;
550
+ var SOCKET_PATH = config.socketPath;
551
+ var pending = /* @__PURE__ */ new Map();
552
+ var analyses = /* @__PURE__ */ new Map();
553
+ function log(...args) {
554
+ process.stderr.write(`[tabctl-host] ${args.join(" ")}
555
+ `);
556
+ }
557
+ function ensureDir() {
558
+ fs_1.default.mkdirSync(SOCKET_DIR, { recursive: true, mode: 448 });
559
+ }
560
+ function createId(prefix) {
561
+ return `${prefix}-${Date.now()}-${crypto_1.default.randomBytes(4).toString("hex")}`;
562
+ }
563
+ function sendNative(message) {
564
+ const json = JSON.stringify(message);
565
+ const length = Buffer.byteLength(json);
566
+ const buffer = Buffer.alloc(4 + length);
567
+ buffer.writeUInt32LE(length, 0);
568
+ buffer.write(json, 4);
569
+ process.stdout.write(buffer);
570
+ }
571
+ var deps = {
572
+ pending,
573
+ analyses,
574
+ undoLog: config.undoLog,
575
+ createId,
576
+ sendNative,
577
+ log
578
+ };
579
+ function handleNativeMessage(payload) {
580
+ (0, handlers_1.handleNativeMessage)(deps, payload);
581
+ }
582
+ function handleCliRequest(socket, request) {
583
+ (0, handlers_1.handleCliRequest)(deps, socket, request);
584
+ }
585
+ var nativeBuffer = Buffer.alloc(0);
586
+ process.stdin.on("data", (chunk) => {
587
+ nativeBuffer = Buffer.concat([nativeBuffer, chunk]);
588
+ while (nativeBuffer.length >= 4) {
589
+ const length = nativeBuffer.readUInt32LE(0);
590
+ if (nativeBuffer.length < 4 + length) {
591
+ return;
592
+ }
593
+ const payload = nativeBuffer.slice(4, 4 + length).toString("utf8");
594
+ nativeBuffer = nativeBuffer.slice(4 + length);
595
+ handleNativeMessage(payload);
596
+ }
597
+ });
598
+ process.stdin.on("end", () => {
599
+ log("Extension disconnected, exiting");
600
+ cleanupAndExit(0);
601
+ });
602
+ function startSocketServer() {
603
+ ensureDir();
604
+ if (process.platform !== "win32" && fs_1.default.existsSync(SOCKET_PATH)) {
605
+ fs_1.default.unlinkSync(SOCKET_PATH);
606
+ }
607
+ const server2 = net_1.default.createServer((socket) => {
608
+ socket.setEncoding("utf8");
609
+ let buffer = "";
610
+ socket.on("data", (data) => {
611
+ buffer += data;
612
+ let index;
613
+ while ((index = buffer.indexOf("\n")) >= 0) {
614
+ const line = buffer.slice(0, index).trim();
615
+ buffer = buffer.slice(index + 1);
616
+ if (!line) {
617
+ continue;
618
+ }
619
+ let request;
620
+ try {
621
+ request = JSON.parse(line);
622
+ } catch {
623
+ (0, handlers_1.respond)(socket, { ok: false, error: { message: "Invalid JSON" } });
624
+ continue;
625
+ }
626
+ handleCliRequest(socket, request);
627
+ }
628
+ });
629
+ socket.on("error", (error) => {
630
+ log("CLI socket error", error.message);
631
+ });
632
+ });
633
+ let retries = 0;
634
+ const maxRetries = process.platform === "win32" ? 5 : 0;
635
+ server2.on("error", (err) => {
636
+ if (err.code === "EADDRINUSE" && retries < maxRetries) {
637
+ retries++;
638
+ log(`Socket in use, retrying (${retries}/${maxRetries})\u2026`);
639
+ setTimeout(() => server2.listen(SOCKET_PATH), 500);
640
+ } else {
641
+ throw err;
642
+ }
643
+ });
644
+ server2.listen(SOCKET_PATH, () => {
645
+ if (process.platform !== "win32") {
646
+ try {
647
+ fs_1.default.chmodSync(SOCKET_PATH, 384);
648
+ } catch {
649
+ }
650
+ }
651
+ log(`Listening on ${SOCKET_PATH}`);
652
+ });
653
+ return server2;
654
+ }
655
+ function cleanupAndExit(code) {
656
+ try {
657
+ if (process.platform !== "win32" && fs_1.default.existsSync(SOCKET_PATH)) {
658
+ fs_1.default.unlinkSync(SOCKET_PATH);
659
+ }
660
+ } catch {
661
+ }
662
+ process.exit(code);
663
+ }
664
+ var server = startSocketServer();
665
+ process.on("SIGINT", () => cleanupAndExit(0));
666
+ process.on("SIGTERM", () => cleanupAndExit(0));
667
+ server.on("close", () => cleanupAndExit(0));
package/dist/host/host.js CHANGED
@@ -71,7 +71,8 @@ process.stdin.on("end", () => {
71
71
  });
72
72
  function startSocketServer() {
73
73
  ensureDir();
74
- if (fs_1.default.existsSync(SOCKET_PATH)) {
74
+ // Named pipes on Windows don't use filesystem paths; skip cleanup
75
+ if (process.platform !== "win32" && fs_1.default.existsSync(SOCKET_PATH)) {
75
76
  fs_1.default.unlinkSync(SOCKET_PATH);
76
77
  }
77
78
  const server = net_1.default.createServer((socket) => {
@@ -101,15 +102,33 @@ function startSocketServer() {
101
102
  log("CLI socket error", error.message);
102
103
  });
103
104
  });
105
+ let retries = 0;
106
+ const maxRetries = process.platform === "win32" ? 5 : 0;
107
+ server.on("error", (err) => {
108
+ if (err.code === "EADDRINUSE" && retries < maxRetries) {
109
+ retries++;
110
+ log(`Socket in use, retrying (${retries}/${maxRetries})…`);
111
+ setTimeout(() => server.listen(SOCKET_PATH), 500);
112
+ }
113
+ else {
114
+ throw err;
115
+ }
116
+ });
104
117
  server.listen(SOCKET_PATH, () => {
105
- fs_1.default.chmodSync(SOCKET_PATH, 0o600);
118
+ if (process.platform !== "win32") {
119
+ try {
120
+ fs_1.default.chmodSync(SOCKET_PATH, 0o600);
121
+ }
122
+ catch { /* ignore on platforms without chmod */ }
123
+ }
106
124
  log(`Listening on ${SOCKET_PATH}`);
107
125
  });
108
126
  return server;
109
127
  }
110
128
  function cleanupAndExit(code) {
111
129
  try {
112
- if (fs_1.default.existsSync(SOCKET_PATH)) {
130
+ // Named pipes on Windows don't need filesystem cleanup
131
+ if (process.platform !== "win32" && fs_1.default.existsSync(SOCKET_PATH)) {
113
132
  fs_1.default.unlinkSync(SOCKET_PATH);
114
133
  }
115
134
  }