@logicseek/hello-cc 0.1.9 → 1.0.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +50 -3
  3. package/README.zh-CN.md +48 -3
  4. package/bin/hcc.mjs +3 -5656
  5. package/docs/README.md +8 -0
  6. package/docs/README.zh-CN.md +6 -0
  7. package/docs/architecture.md +29 -18
  8. package/docs/architecture.zh-CN.md +20 -10
  9. package/docs/commands.md +13 -4
  10. package/docs/commands.zh-CN.md +13 -4
  11. package/docs/guide.md +97 -3
  12. package/docs/guide.zh-CN.md +89 -2
  13. package/lib/cli/app.mjs +644 -0
  14. package/lib/cli/commands/coordination.mjs +249 -0
  15. package/lib/cli/commands/doctor.mjs +81 -0
  16. package/lib/cli/commands/gc.mjs +1002 -0
  17. package/lib/cli/commands/hook.mjs +205 -0
  18. package/lib/cli/commands/install.mjs +294 -0
  19. package/lib/cli/commands/lifecycle.mjs +101 -0
  20. package/lib/cli/commands/lock.mjs +197 -0
  21. package/lib/cli/commands/misc.mjs +76 -0
  22. package/lib/cli/commands/msg.mjs +152 -0
  23. package/lib/cli/commands/peer.mjs +249 -0
  24. package/lib/cli/commands/query.mjs +96 -0
  25. package/lib/cli/commands/run.mjs +463 -0
  26. package/lib/cli/commands/task.mjs +405 -0
  27. package/lib/cli/commands/team.mjs +131 -0
  28. package/lib/cli/commands/tmux.mjs +838 -0
  29. package/lib/cli/commands/up.mjs +135 -0
  30. package/lib/cli-args.mjs +16 -2
  31. package/lib/coordination-state.mjs +112 -51
  32. package/lib/core/coordination/automation.mjs +5 -8
  33. package/lib/core/coordination/clock-safety.mjs +323 -0
  34. package/lib/core/coordination/event-retention.mjs +17 -0
  35. package/lib/core/coordination/gc-plan.mjs +614 -0
  36. package/lib/core/coordination/lease-renewal.mjs +102 -0
  37. package/lib/core/coordination/lock-evidence.mjs +77 -0
  38. package/lib/core/coordination/optimistic-evidence.mjs +26 -0
  39. package/lib/core/coordination/tasks.mjs +156 -36
  40. package/lib/core/peers/evidence-runtime.mjs +312 -0
  41. package/lib/core/peers/evidence.mjs +103 -0
  42. package/lib/core/peers/hook-owner.mjs +32 -0
  43. package/lib/core/peers/liveness.mjs +44 -4
  44. package/lib/core/peers/peer-helpers.mjs +121 -0
  45. package/lib/core/peers/session.mjs +8 -2
  46. package/lib/core/peers/tmux-safety.mjs +290 -0
  47. package/lib/core/sessions/launch.mjs +13 -1
  48. package/lib/db/connection.mjs +104 -0
  49. package/lib/db/events.mjs +34 -0
  50. package/lib/db/migration-backup.mjs +225 -0
  51. package/lib/db/schema.mjs +102 -5
  52. package/lib/db/stores/peers.mjs +34 -0
  53. package/lib/integrations/peers/identity.mjs +62 -16
  54. package/lib/integrations/shims/script.mjs +111 -9
  55. package/lib/process/identity.mjs +222 -0
  56. package/lib/process/pty-lifecycle.mjs +149 -0
  57. package/lib/runtime/buffer-directory-lease.mjs +48 -0
  58. package/lib/runtime/buffer-evidence.mjs +336 -0
  59. package/lib/runtime/buffer-gc-protocol.mjs +167 -0
  60. package/lib/runtime/buffer-gc.mjs +270 -0
  61. package/lib/runtime/client.mjs +63 -7
  62. package/lib/runtime/fatal-shutdown.mjs +54 -0
  63. package/lib/runtime/project-path.mjs +139 -0
  64. package/lib/runtime/projects.mjs +320 -40
  65. package/lib/runtime/state.mjs +227 -27
  66. package/lib/shared/cleanup-error.mjs +54 -0
  67. package/lib/shared/clock-grace.mjs +106 -0
  68. package/lib/shared/errors.mjs +9 -0
  69. package/lib/shared/file-lock.mjs +377 -0
  70. package/lib/shared/json-file.mjs +6 -2
  71. package/lib/shared/redact.mjs +60 -0
  72. package/lib/shared/socket-lock-worker.mjs +250 -0
  73. package/lib/terminal/tmux.mjs +131 -27
  74. package/lib/tmux.mjs +4 -0
  75. package/lib/ui/help.mjs +53 -15
  76. package/lib/web/api-version.mjs +42 -0
  77. package/lib/web/auto-attach.mjs +185 -0
  78. package/lib/web/buffer-gc-runtime.mjs +175 -0
  79. package/lib/web/cookie-auth.mjs +119 -0
  80. package/lib/web/external-sessions.mjs +254 -0
  81. package/lib/web/http-routes.mjs +542 -0
  82. package/lib/web/http.mjs +135 -6
  83. package/lib/web/liveness-reaper.mjs +145 -0
  84. package/lib/web/peer-actions.mjs +132 -23
  85. package/lib/web/project-contexts.mjs +183 -0
  86. package/lib/web/pty-sessions.mjs +203 -0
  87. package/lib/web/runtime-main.mjs +717 -0
  88. package/lib/web/runtime.mjs +91 -36
  89. package/lib/web/session-serialize.mjs +0 -0
  90. package/lib/web/startup.mjs +335 -0
  91. package/lib/web/tls.mjs +365 -0
  92. package/lib/web/tmux-sessions.mjs +1361 -0
  93. package/lib/web/tmux-stream.mjs +166 -0
  94. package/lib/web/ui-template.mjs +152 -20
  95. package/package.json +6 -3
  96. package/scripts/check-free-identifiers.mjs +290 -0
  97. package/scripts/regression.mjs +5404 -273
  98. package/docs/architecture-split-review.md +0 -217
  99. package/docs/layout-split-stack-audit.md +0 -298
@@ -0,0 +1,377 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { Worker } from 'node:worker_threads';
5
+ import { aggregateCleanupFailure } from './cleanup-error.mjs';
6
+
7
+ const NO_ERROR = Symbol('no-file-lock-error');
8
+ const STATE_INDEX = 0;
9
+ const DETAIL_INDEX = 1;
10
+ const STATE = Object.freeze({
11
+ STARTING: 0,
12
+ ACQUIRED: 1,
13
+ BUSY: 2,
14
+ TIMEOUT: 3,
15
+ FAILED: 4,
16
+ RELEASED: 5,
17
+ RELEASE_FAILED: 6
18
+ });
19
+ const HOST = '127.0.0.1';
20
+ const PORT_BASE = 20_000;
21
+ const PORT_COUNT = 40_000;
22
+ const PORT_CANDIDATE_COUNT = 4;
23
+ const STARTUP_GRACE_MS = 5000;
24
+ const RELEASE_GRACE_MS = 5000;
25
+ const WORKER_URL = new URL('./socket-lock-worker.mjs', import.meta.url);
26
+ const WORKER_BOOTSTRAP = String.raw`
27
+ import('node:worker_threads').then(({ parentPort, workerData }) => {
28
+ const state = new Int32Array(workerData.stateBuffer);
29
+ const fail = () => {
30
+ Atomics.store(state, 1, 1);
31
+ Atomics.store(state, 0, workerData.states.FAILED);
32
+ Atomics.notify(state, 0);
33
+ parentPort.close();
34
+ };
35
+ import(workerData.moduleUrl).then((module) => {
36
+ if (typeof module.runSocketLockWorker !== 'function') throw new TypeError('invalid worker module');
37
+ module.runSocketLockWorker(workerData);
38
+ }).catch(fail);
39
+ }).catch(() => {});
40
+ `;
41
+
42
+ function sameObject(left, right) {
43
+ return left?.dev === right?.dev && left?.ino === right?.ino;
44
+ }
45
+
46
+ function invalidLock(message, cause = null) {
47
+ const error = new Error(message, cause ? { cause } : undefined);
48
+ error.code = 'ERR_INVALID_FILE_LOCK';
49
+ return error;
50
+ }
51
+
52
+ function busyError(key) {
53
+ const error = new Error(`file lock is busy: ${key}`);
54
+ error.code = 'ERR_FILE_LOCK_BUSY';
55
+ return error;
56
+ }
57
+
58
+ function timeoutError(key) {
59
+ const error = new Error(`timed out waiting for file lock: ${key}`);
60
+ error.code = 'ERR_FILE_LOCK_TIMEOUT';
61
+ return error;
62
+ }
63
+
64
+ function workerError(key, phase, cause = null) {
65
+ const error = new Error(
66
+ `file lock worker ${phase} failed: ${key}`,
67
+ cause ? { cause } : undefined
68
+ );
69
+ error.code = phase === 'release'
70
+ ? 'ERR_FILE_LOCK_RELEASE_FAILED'
71
+ : 'ERR_FILE_LOCK_WORKER_START_FAILED';
72
+ return error;
73
+ }
74
+
75
+ function startupTimeoutError(key) {
76
+ const error = new Error(`file lock worker did not start in time: ${key}`);
77
+ error.code = 'ERR_FILE_LOCK_WORKER_START_TIMEOUT';
78
+ return error;
79
+ }
80
+
81
+ function canonicalTarget(value, { createParent = true } = {}) {
82
+ if (typeof value !== 'string' || value.length === 0) {
83
+ throw new TypeError('file lock target must be a non-empty path');
84
+ }
85
+ const supplied = path.resolve(value);
86
+ const suppliedParent = path.dirname(supplied);
87
+ if (createParent) fs.mkdirSync(suppliedParent, { recursive: true, mode: 0o700 });
88
+ const canonicalParent = fs.realpathSync.native(suppliedParent);
89
+ const parentIdentity = fs.statSync(canonicalParent);
90
+ if (!parentIdentity.isDirectory()) {
91
+ throw invalidLock(`file lock parent is not a directory: ${suppliedParent}`);
92
+ }
93
+ const canonicalKey = path.join(canonicalParent, path.basename(supplied));
94
+ return {
95
+ suppliedKey: supplied,
96
+ suppliedParent,
97
+ canonicalParent,
98
+ canonicalTarget: canonicalKey,
99
+ parentIdentity,
100
+ key: canonicalKey
101
+ };
102
+ }
103
+
104
+ function assertStableParent(location) {
105
+ const canonicalParent = fs.realpathSync.native(location.suppliedParent);
106
+ const parentIdentity = fs.statSync(canonicalParent);
107
+ if (canonicalParent !== location.canonicalParent ||
108
+ !sameObject(parentIdentity, location.parentIdentity) ||
109
+ !parentIdentity.isDirectory()) {
110
+ throw invalidLock('file lock parent changed during acquisition');
111
+ }
112
+ }
113
+
114
+ function endpointForLocation(location) {
115
+ return endpointForKey(location.key);
116
+ }
117
+
118
+ function endpointForKey(key) {
119
+ const digest = createHash('sha256').update(key).digest();
120
+ return {
121
+ host: HOST,
122
+ port: PORT_BASE + digest.readUInt32BE(0) % PORT_COUNT,
123
+ key
124
+ };
125
+ }
126
+
127
+ function candidatePortsForKey(key) {
128
+ const ports = [];
129
+ for (let index = 0; ports.length < PORT_CANDIDATE_COUNT; index += 1) {
130
+ const digest = index === 0
131
+ ? createHash('sha256').update(key).digest()
132
+ : createHash('sha256').update(`hcc-file-lock-port-v1\0${index}\0${key}`).digest();
133
+ const port = PORT_BASE + digest.readUInt32BE(0) % PORT_COUNT;
134
+ if (!ports.includes(port)) ports.push(port);
135
+ }
136
+ return ports;
137
+ }
138
+
139
+ function lockIdentityForKey(key) {
140
+ return createHash('sha256').update(`hcc-file-lock-v1\0${key}`).digest('hex');
141
+ }
142
+
143
+ function lockTargetsForLocation(location) {
144
+ return [...new Set([location.suppliedKey, location.key])]
145
+ .sort()
146
+ .map((key) => ({
147
+ identity: lockIdentityForKey(key),
148
+ ports: candidatePortsForKey(key)
149
+ }));
150
+ }
151
+
152
+ function endpointsForLocation(location) {
153
+ const byPort = new Map();
154
+ for (const key of [location.suppliedKey, location.key].sort()) {
155
+ const endpoint = endpointForKey(key);
156
+ if (!byPort.has(endpoint.port)) byPort.set(endpoint.port, endpoint);
157
+ }
158
+ return [...byPort.values()].sort((left, right) => left.port - right.port);
159
+ }
160
+
161
+ // The primary endpoint remains stable for diagnostics. Acquisition uses
162
+ // additional deterministic candidates plus an identity handshake so an
163
+ // unrelated local listener cannot impersonate this target's lock.
164
+ export function fileLockEndpoint(target) {
165
+ const location = canonicalTarget(target);
166
+ assertStableParent(location);
167
+ return endpointForLocation(location);
168
+ }
169
+
170
+ export function fileLockEndpoints(target) {
171
+ const location = canonicalTarget(target);
172
+ assertStableParent(location);
173
+ return endpointsForLocation(location);
174
+ }
175
+
176
+ function combineCleanup(primary, cleanup, message, phase, key) {
177
+ if (primary === NO_ERROR) return cleanup;
178
+ return aggregateCleanupFailure(primary, cleanup, message, { phase, lockPath: key });
179
+ }
180
+
181
+ function defaultWorkerFactory({ workerSource, workerData }) {
182
+ // The eval bootstrap owns module-load failures and reports them through the
183
+ // shared state before closing its parent port. No parent execArgv is needed.
184
+ return new Worker(workerSource, { eval: true, workerData, execArgv: [] });
185
+ }
186
+
187
+ function injectedFailure(worker, state, phase, key) {
188
+ const detail = Atomics.load(state, DETAIL_INDEX);
189
+ try {
190
+ const failure = worker?.failure?.(detail, phase);
191
+ if (failure) return failure;
192
+ } catch (error) {
193
+ return workerError(key, phase, error);
194
+ }
195
+ return workerError(key, phase);
196
+ }
197
+
198
+ function terminateWorker(worker, primary, key) {
199
+ if (!worker) return primary;
200
+ try {
201
+ const pending = worker.terminate();
202
+ if (pending && typeof pending.then === 'function') {
203
+ pending.catch(() => {});
204
+ const cleanup = new Error(`file lock worker termination is unconfirmed: ${key}`);
205
+ cleanup.code = 'ERR_FILE_LOCK_WORKER_TERMINATION_UNCONFIRMED';
206
+ return combineCleanup(
207
+ primary,
208
+ cleanup,
209
+ 'file lock operation failed and asynchronous worker termination cannot be confirmed',
210
+ 'terminate',
211
+ key
212
+ );
213
+ }
214
+ return primary;
215
+ } catch (cleanup) {
216
+ return combineCleanup(
217
+ primary,
218
+ cleanup,
219
+ 'file lock operation and worker termination both failed',
220
+ 'terminate',
221
+ key
222
+ );
223
+ }
224
+ }
225
+
226
+ function acquisitionError(status, resources) {
227
+ if (status === STATE.BUSY) return busyError(resources.location.key);
228
+ if (status === STATE.TIMEOUT) return timeoutError(resources.location.key);
229
+ if (status === STATE.FAILED) {
230
+ return injectedFailure(resources.worker, resources.state, 'startup', resources.location.key);
231
+ }
232
+ return startupTimeoutError(resources.location.key);
233
+ }
234
+
235
+ function acquireWorker(location, options, workerFactory) {
236
+ const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 2);
237
+ const state = new Int32Array(buffer);
238
+ const endpoints = endpointsForLocation(location);
239
+ const targets = lockTargetsForLocation(location);
240
+ const resources = { location, endpoints, state, worker: null };
241
+ let primary = NO_ERROR;
242
+ try {
243
+ resources.worker = workerFactory({
244
+ workerSource: WORKER_BOOTSTRAP,
245
+ workerUrl: WORKER_URL,
246
+ workerData: {
247
+ stateBuffer: buffer,
248
+ states: STATE,
249
+ targets,
250
+ moduleUrl: WORKER_URL.href,
251
+ timeoutMs: options.timeoutMs,
252
+ retryMs: options.retryMs,
253
+ nonblocking: options.nonblocking
254
+ },
255
+ state,
256
+ states: STATE
257
+ });
258
+ if (!resources.worker || typeof resources.worker.unref !== 'function' ||
259
+ typeof resources.worker.postMessage !== 'function' ||
260
+ typeof resources.worker.terminate !== 'function') {
261
+ throw new TypeError('file lock worker factory returned an invalid worker');
262
+ }
263
+ if (typeof resources.worker.on === 'function') {
264
+ // A module-load failure happens before the Worker can publish FAILED.
265
+ // Suppress the otherwise-unhandled event; the bounded startup wait below
266
+ // converts this path to ERR_FILE_LOCK_WORKER_START_TIMEOUT.
267
+ resources.worker.on('error', () => {});
268
+ }
269
+ resources.worker.unref();
270
+ const waitMs = STARTUP_GRACE_MS +
271
+ (options.nonblocking ? 0 : Math.ceil(options.timeoutMs)) + 100;
272
+ Atomics.wait(state, STATE_INDEX, STATE.STARTING, waitMs);
273
+ const status = Atomics.load(state, STATE_INDEX);
274
+ if (status !== STATE.ACQUIRED) throw acquisitionError(status, resources);
275
+ assertStableParent(location);
276
+ return resources;
277
+ } catch (error) {
278
+ primary = error;
279
+ }
280
+ if (Atomics.load(state, STATE_INDEX) === STATE.ACQUIRED) {
281
+ throw releaseWorker(resources, primary);
282
+ }
283
+ if ([STATE.BUSY, STATE.TIMEOUT, STATE.FAILED].includes(
284
+ Atomics.load(state, STATE_INDEX)
285
+ )) {
286
+ throw primary;
287
+ }
288
+ throw terminateWorker(resources.worker, primary, location.key);
289
+ }
290
+
291
+ function releaseWorker(resources, primary) {
292
+ let error = primary;
293
+ let mustTerminate = false;
294
+ try {
295
+ resources.worker.postMessage({ type: 'release' });
296
+ Atomics.wait(resources.state, STATE_INDEX, STATE.ACQUIRED, RELEASE_GRACE_MS);
297
+ const status = Atomics.load(resources.state, STATE_INDEX);
298
+ if (status !== STATE.RELEASED) {
299
+ const cleanup = status === STATE.RELEASE_FAILED
300
+ ? injectedFailure(resources.worker, resources.state, 'release', resources.location.key)
301
+ : workerError(resources.location.key, 'release');
302
+ mustTerminate = status !== STATE.FAILED;
303
+ error = combineCleanup(
304
+ error,
305
+ cleanup,
306
+ 'file lock operation and worker release both failed',
307
+ 'release',
308
+ resources.location.key
309
+ );
310
+ }
311
+ } catch (cleanup) {
312
+ mustTerminate = true;
313
+ error = combineCleanup(
314
+ error,
315
+ cleanup,
316
+ 'file lock operation and worker release both failed',
317
+ 'release',
318
+ resources.location.key
319
+ );
320
+ }
321
+ return mustTerminate
322
+ ? terminateWorker(resources.worker, error, resources.location.key)
323
+ : error;
324
+ }
325
+
326
+ export function createFileLock({ workerFactory = defaultWorkerFactory } = {}) {
327
+ if (typeof workerFactory !== 'function') {
328
+ throw new TypeError('file lock workerFactory must be a function');
329
+ }
330
+ return function withConfiguredFileLock(
331
+ target,
332
+ fn,
333
+ options = {}
334
+ ) {
335
+ const timeoutMs = options.timeoutMs === undefined ? 5000 : options.timeoutMs;
336
+ const retryMs = options.retryMs === undefined ? 25 : options.retryMs;
337
+ const nonblocking = options.nonblocking === undefined ? false : options.nonblocking;
338
+ const createParent = options.createParent === undefined ? true : options.createParent;
339
+ if (typeof fn !== 'function') throw new TypeError('file lock callback must be a function');
340
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
341
+ throw new RangeError('file lock timeoutMs must be a non-negative finite number');
342
+ }
343
+ if (!Number.isFinite(retryMs) || retryMs <= 0) {
344
+ throw new RangeError('file lock retryMs must be a positive finite number');
345
+ }
346
+ if (typeof nonblocking !== 'boolean') {
347
+ throw new TypeError('file lock nonblocking must be a boolean');
348
+ }
349
+ if (typeof createParent !== 'boolean') {
350
+ throw new TypeError('file lock createParent must be a boolean');
351
+ }
352
+ if (fn.constructor?.name === 'AsyncFunction') {
353
+ throw new TypeError('withFileLock requires a synchronous callback');
354
+ }
355
+
356
+ const location = canonicalTarget(target, { createParent });
357
+ const resources = acquireWorker(location, { timeoutMs, retryMs, nonblocking }, workerFactory);
358
+ let result;
359
+ let primary = NO_ERROR;
360
+ try {
361
+ // Protected I/O must use this fixed path. The supplied parent alias may
362
+ // be retargeted after acquisition and belongs to a different lock domain.
363
+ result = fn(location.canonicalTarget);
364
+ if (result && typeof result.then === 'function') {
365
+ Promise.resolve(result).catch(() => {});
366
+ throw new TypeError('withFileLock requires a synchronous callback');
367
+ }
368
+ } catch (error) {
369
+ primary = error;
370
+ }
371
+ const finalError = releaseWorker(resources, primary);
372
+ if (finalError !== NO_ERROR) throw finalError;
373
+ return result;
374
+ };
375
+ }
376
+
377
+ export const withFileLock = createFileLock();
@@ -6,9 +6,13 @@ export function readJsonSafe(filePath) {
6
6
  catch { return null; }
7
7
  }
8
8
 
9
- export function writeJsonSafe(filePath, data) {
9
+ export function writeJsonSafe(filePath, data, opts = {}) {
10
10
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
11
11
  const tmpPath = filePath + '.tmp.' + process.pid;
12
- fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
12
+ const writeOpts = opts.mode !== undefined ? { mode: opts.mode } : undefined;
13
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', writeOpts);
13
14
  fs.renameSync(tmpPath, filePath);
15
+ if (opts.mode !== undefined) {
16
+ try { fs.chmodSync(filePath, opts.mode); } catch {}
17
+ }
14
18
  }
@@ -0,0 +1,60 @@
1
+ const REDACTED = '[REDACTED]';
2
+ const CIRCULAR = '[Circular]';
3
+
4
+ function isSecretKey(key) {
5
+ const normalized = String(key || '').toLowerCase().replace(/[^a-z0-9]/g, '');
6
+ return normalized === 'authorization' ||
7
+ normalized === 'cookie' ||
8
+ normalized === 'setcookie' ||
9
+ normalized.includes('secret') ||
10
+ normalized.endsWith('password') ||
11
+ normalized.endsWith('credential') ||
12
+ normalized.endsWith('credentials') ||
13
+ normalized.endsWith('apikey') ||
14
+ normalized.endsWith('privatekey') ||
15
+ normalized.endsWith('token');
16
+ }
17
+
18
+ function redactString(value) {
19
+ return String(value)
20
+ .replace(/\b(?:cookie|set-cookie)\s*:\s*[^\r\n]*/gi, (header) => `${header.split(':', 1)[0]}: ${REDACTED}`)
21
+ .replace(/\bBearer\s+[^\s,;]+/gi, `Bearer ${REDACTED}`)
22
+ .replace(/(--token(?:=|\s+))(?:"[^"]*"|'[^']*'|[^\s]+)/gi, `$1${REDACTED}`)
23
+ .replace(/([?&](?:token|access_token|api_key)=)[^&#\s]*/gi, `$1${REDACTED}`)
24
+ .replace(/(hcc_sid=)[^;\s]*/gi, `$1${REDACTED}`);
25
+ }
26
+
27
+ export function redactCliArgs(args) {
28
+ const values = Array.from(args || [], (entry) => String(entry));
29
+ return values.map((entry, index) => {
30
+ if (index > 0 && values[index - 1] === '--token') return REDACTED;
31
+ if (entry.startsWith('--token=')) return `--token=${REDACTED}`;
32
+ return redactString(entry);
33
+ });
34
+ }
35
+
36
+ export function redactSecrets(value, seen = new WeakSet()) {
37
+ if (typeof value === 'string') return redactString(value);
38
+ if (value === null || typeof value !== 'object') return value;
39
+ if (seen.has(value)) return CIRCULAR;
40
+ seen.add(value);
41
+
42
+ try {
43
+ if (value instanceof Error) {
44
+ return {
45
+ name: value.name,
46
+ message: redactString(value.message || ''),
47
+ stack: redactString(value.stack || '')
48
+ };
49
+ }
50
+ if (Array.isArray(value)) return value.map((entry) => redactSecrets(entry, seen));
51
+
52
+ const clone = {};
53
+ for (const [key, entry] of Object.entries(value)) {
54
+ clone[key] = isSecretKey(key) ? REDACTED : redactSecrets(entry, seen);
55
+ }
56
+ return clone;
57
+ } finally {
58
+ seen.delete(value);
59
+ }
60
+ }
@@ -0,0 +1,250 @@
1
+ import net from 'node:net';
2
+ import { parentPort } from 'node:worker_threads';
3
+
4
+ const STATE_INDEX = 0;
5
+ const DETAIL_INDEX = 1;
6
+ const PROTOCOL_PREFIX = 'HCC_FILE_LOCK_V1 ';
7
+ const PROBE_TIMEOUT_MS = 25;
8
+
9
+ export function runSocketLockWorker(workerData) {
10
+ const state = new Int32Array(workerData.stateBuffer);
11
+ const states = workerData.states;
12
+ const targets = workerData.targets;
13
+ const acceptedSockets = new Set();
14
+ let servers = [];
15
+ let closeOperation = null;
16
+ let contentionDeadline = null;
17
+ let releasing = false;
18
+ let terminalFailureRequested = false;
19
+ let finished = false;
20
+ let generation = 0;
21
+
22
+ function publish(status, detail = 0) {
23
+ Atomics.store(state, DETAIL_INDEX, detail);
24
+ Atomics.store(state, STATE_INDEX, status);
25
+ Atomics.notify(state, STATE_INDEX);
26
+ }
27
+
28
+ function finish(status, detail = 0) {
29
+ if (finished) return;
30
+ if (terminalFailureRequested) {
31
+ status = states.FAILED;
32
+ detail = 1;
33
+ }
34
+ finished = true;
35
+ publish(status, detail);
36
+ parentPort.close();
37
+ }
38
+
39
+ function closeServers(callback) {
40
+ if (closeOperation !== null) {
41
+ closeOperation.callbacks.push(callback);
42
+ if (closeOperation.cleanupStarted) destroyAcceptedSockets();
43
+ return;
44
+ }
45
+ const closing = servers;
46
+ servers = [];
47
+ const operation = {
48
+ callbacks: [callback],
49
+ cleanupStarted: false,
50
+ closing,
51
+ completed: false,
52
+ firstError: null,
53
+ remaining: closing.length
54
+ };
55
+ closeOperation = operation;
56
+ const complete = () => {
57
+ if (operation.completed || !operation.cleanupStarted || operation.remaining !== 0) return;
58
+ operation.completed = true;
59
+ closeOperation = null;
60
+ const callbacks = operation.callbacks;
61
+ operation.callbacks = [];
62
+ operation.closing = [];
63
+ for (const waiting of callbacks) waiting(operation.firstError);
64
+ };
65
+ const closed = (error = null) => {
66
+ if (error && !operation.firstError) operation.firstError = error;
67
+ operation.remaining -= 1;
68
+ complete();
69
+ };
70
+ for (const server of operation.closing) {
71
+ let settled = false;
72
+ const serverClosed = (error = null) => {
73
+ if (settled) return;
74
+ settled = true;
75
+ closed(error);
76
+ };
77
+ try {
78
+ server.close(serverClosed);
79
+ } catch (error) {
80
+ serverClosed(error);
81
+ }
82
+ }
83
+ destroyAcceptedSockets();
84
+ operation.cleanupStarted = true;
85
+ complete();
86
+ }
87
+
88
+ function requestTerminalFailure() {
89
+ if (finished) return;
90
+ terminalFailureRequested = true;
91
+ generation += 1;
92
+ closeServers(() => finish(states.FAILED, 1));
93
+ }
94
+
95
+ function retryAfterContention() {
96
+ if (workerData.nonblocking) {
97
+ finish(states.BUSY);
98
+ return;
99
+ }
100
+ if (contentionDeadline === null) {
101
+ contentionDeadline = performance.now() + workerData.timeoutMs;
102
+ }
103
+ const remaining = contentionDeadline - performance.now();
104
+ if (remaining <= 0) {
105
+ finish(states.TIMEOUT);
106
+ return;
107
+ }
108
+ setTimeout(attemptAll, Math.min(workerData.retryMs, remaining));
109
+ }
110
+
111
+ function retryCleanly(attemptGeneration) {
112
+ if (finished || attemptGeneration !== generation) return;
113
+ generation += 1;
114
+ closeServers((error) => {
115
+ if (error) requestTerminalFailure();
116
+ else if (!terminalFailureRequested) retryAfterContention();
117
+ });
118
+ }
119
+
120
+ function lockBanner(identity) {
121
+ return `${PROTOCOL_PREFIX}${identity}\n`;
122
+ }
123
+
124
+ function serveLock(socket, identity) {
125
+ acceptedSockets.add(socket);
126
+ socket.once('close', () => acceptedSockets.delete(socket));
127
+ socket.on('error', () => {});
128
+ socket.end(lockBanner(identity));
129
+ }
130
+
131
+ function destroyAcceptedSockets() {
132
+ for (const socket of acceptedSockets) {
133
+ try { socket.destroy(); } catch {}
134
+ }
135
+ }
136
+
137
+ function probeOccupant(port, identity, callback) {
138
+ const expected = lockBanner(identity);
139
+ let received = '';
140
+ let settled = false;
141
+ let socket;
142
+ const settle = (matching) => {
143
+ if (settled) return;
144
+ settled = true;
145
+ clearTimeout(timer);
146
+ try { socket?.destroy(); } catch {}
147
+ callback(matching);
148
+ };
149
+ const timer = setTimeout(() => settle(false), PROBE_TIMEOUT_MS);
150
+ try {
151
+ socket = net.createConnection({ host: '127.0.0.1', port });
152
+ socket.setEncoding('utf8');
153
+ socket.on('data', (chunk) => {
154
+ received += chunk;
155
+ if (received.includes('\n') || received.length >= expected.length) {
156
+ settle(received === expected);
157
+ }
158
+ });
159
+ socket.once('end', () => settle(received === expected));
160
+ socket.once('error', () => settle(false));
161
+ } catch {
162
+ settle(false);
163
+ }
164
+ }
165
+
166
+ function bindCandidate(targetIndex, candidateIndex, boundForTarget, attemptGeneration) {
167
+ if (finished || attemptGeneration !== generation) return;
168
+ const target = targets[targetIndex];
169
+ if (candidateIndex === target.ports.length) {
170
+ if (boundForTarget === 0) {
171
+ retryCleanly(attemptGeneration);
172
+ } else {
173
+ bindTarget(targetIndex + 1, attemptGeneration);
174
+ }
175
+ return;
176
+ }
177
+ const port = target.ports[candidateIndex];
178
+ let candidate;
179
+ try {
180
+ candidate = net.createServer((socket) => serveLock(socket, target.identity));
181
+ const bindError = (error) => {
182
+ if (finished || attemptGeneration !== generation) return;
183
+ if (error?.code !== 'EADDRINUSE') {
184
+ requestTerminalFailure();
185
+ return;
186
+ }
187
+ probeOccupant(port, target.identity, (matching) => {
188
+ if (finished || attemptGeneration !== generation) return;
189
+ if (matching) retryCleanly(attemptGeneration);
190
+ else bindCandidate(targetIndex, candidateIndex + 1, boundForTarget, attemptGeneration);
191
+ });
192
+ };
193
+ candidate.once('error', bindError);
194
+ candidate.listen({ host: '127.0.0.1', port, exclusive: true }, () => {
195
+ if (finished || attemptGeneration !== generation) {
196
+ try { candidate.close(); } catch {}
197
+ return;
198
+ }
199
+ candidate.off('error', bindError);
200
+ candidate.on('error', requestTerminalFailure);
201
+ servers.push(candidate);
202
+ bindCandidate(targetIndex, candidateIndex + 1, boundForTarget + 1, attemptGeneration);
203
+ });
204
+ } catch {
205
+ requestTerminalFailure();
206
+ }
207
+ }
208
+
209
+ function bindTarget(index, attemptGeneration) {
210
+ if (finished || attemptGeneration !== generation) return;
211
+ if (index === targets.length) {
212
+ publish(states.ACQUIRED);
213
+ return;
214
+ }
215
+ bindCandidate(index, 0, 0, attemptGeneration);
216
+ }
217
+
218
+ function attemptAll() {
219
+ if (finished) return;
220
+ if (contentionDeadline !== null && performance.now() >= contentionDeadline) {
221
+ finish(states.TIMEOUT);
222
+ return;
223
+ }
224
+ const attemptGeneration = generation;
225
+ bindTarget(0, attemptGeneration);
226
+ }
227
+
228
+ parentPort.on('message', (message) => {
229
+ if (message?.type !== 'release' || releasing ||
230
+ Atomics.load(state, STATE_INDEX) !== states.ACQUIRED) return;
231
+ releasing = true;
232
+ generation += 1;
233
+ closeServers((error) => {
234
+ if (terminalFailureRequested) finish(states.FAILED, 1);
235
+ else finish(error ? states.RELEASE_FAILED : states.RELEASED, error ? 1 : 0);
236
+ });
237
+ });
238
+
239
+ try {
240
+ if (!Array.isArray(targets) || targets.length === 0 || targets.some((target) =>
241
+ !/^[a-f0-9]{64}$/.test(target?.identity) ||
242
+ !Array.isArray(target?.ports) || target.ports.length === 0 ||
243
+ target.ports.some((port) => !Number.isInteger(port) || port < 1 || port > 65535))) {
244
+ throw new TypeError('file lock worker requires valid targets');
245
+ }
246
+ attemptAll();
247
+ } catch {
248
+ requestTerminalFailure();
249
+ }
250
+ }