vibe-coding-master 0.3.3 → 0.3.5

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.
@@ -1,58 +1,70 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  export function createNodeFileSystemAdapter() {
4
+ const runFileOperation = createFileOperationRunner();
4
5
  return {
5
6
  async pathExists(targetPath) {
6
7
  try {
7
- await fs.access(targetPath);
8
+ await runFileOperation(() => fs.access(targetPath));
8
9
  return true;
9
10
  }
10
- catch {
11
- return false;
11
+ catch (error) {
12
+ if (isMissingPathError(error)) {
13
+ return false;
14
+ }
15
+ throw error;
12
16
  }
13
17
  },
14
18
  async ensureDir(targetPath) {
15
- await fs.mkdir(targetPath, { recursive: true });
19
+ await runFileOperation(() => fs.mkdir(targetPath, { recursive: true }));
16
20
  },
17
21
  async readDir(targetPath) {
18
- return fs.readdir(targetPath);
22
+ return runFileOperation(() => fs.readdir(targetPath));
19
23
  },
20
24
  async readText(targetPath) {
21
- return fs.readFile(targetPath, "utf8");
25
+ return runFileOperation(() => fs.readFile(targetPath, "utf8"));
22
26
  },
23
27
  async readTextTail(targetPath, maxBytes) {
24
- const handle = await fs.open(targetPath, "r");
25
- try {
26
- const stat = await handle.stat();
27
- const length = Math.min(Math.max(0, Math.floor(maxBytes)), stat.size);
28
- const position = stat.size - length;
29
- const buffer = Buffer.alloc(length);
30
- await handle.read(buffer, 0, length, position);
31
- return buffer.toString("utf8");
32
- }
33
- finally {
34
- await handle.close();
35
- }
28
+ return runFileOperation(async () => {
29
+ const handle = await fs.open(targetPath, "r");
30
+ try {
31
+ const stat = await handle.stat();
32
+ const length = Math.min(Math.max(0, Math.floor(maxBytes)), stat.size);
33
+ const position = stat.size - length;
34
+ const buffer = Buffer.alloc(length);
35
+ await handle.read(buffer, 0, length, position);
36
+ return buffer.toString("utf8");
37
+ }
38
+ finally {
39
+ await handle.close();
40
+ }
41
+ });
36
42
  },
37
43
  async writeText(targetPath, content) {
38
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
39
- await fs.writeFile(targetPath, content, "utf8");
44
+ await runFileOperation(async () => {
45
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
46
+ await fs.writeFile(targetPath, content, "utf8");
47
+ });
40
48
  },
41
49
  async appendText(targetPath, content) {
42
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
43
- await fs.appendFile(targetPath, content, "utf8");
50
+ await runFileOperation(async () => {
51
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
52
+ await fs.appendFile(targetPath, content, "utf8");
53
+ });
44
54
  },
45
55
  async readJson(targetPath) {
46
- return JSON.parse(await fs.readFile(targetPath, "utf8"));
56
+ return JSON.parse(await runFileOperation(() => fs.readFile(targetPath, "utf8")));
47
57
  },
48
58
  async writeJson(targetPath, value) {
49
59
  await this.writeText(targetPath, `${JSON.stringify(value, null, 2)}\n`);
50
60
  },
51
61
  async writeJsonAtomic(targetPath, value) {
52
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
53
- const tempPath = `${targetPath}.${process.pid}.tmp`;
54
- await fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
55
- await fs.rename(tempPath, targetPath);
62
+ await runFileOperation(async () => {
63
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
64
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
65
+ await fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
66
+ await fs.rename(tempPath, targetPath);
67
+ });
56
68
  },
57
69
  async ensureFile(targetPath, content, options = {}) {
58
70
  if (!options.overwrite && await this.pathExists(targetPath)) {
@@ -62,13 +74,75 @@ export function createNodeFileSystemAdapter() {
62
74
  return true;
63
75
  },
64
76
  async removePath(targetPath, options = {}) {
65
- await fs.rm(targetPath, {
77
+ await runFileOperation(() => fs.rm(targetPath, {
66
78
  recursive: options.recursive ?? false,
67
79
  force: options.force ?? false
68
- });
80
+ }));
81
+ }
82
+ };
83
+ }
84
+ const DEFAULT_FILE_OPERATION_CONCURRENCY = 32;
85
+ const OPEN_FILE_RETRY_DELAYS_MS = [25, 50, 100, 200, 400, 800];
86
+ function createFileOperationRunner(maxConcurrent = DEFAULT_FILE_OPERATION_CONCURRENCY) {
87
+ let active = 0;
88
+ const waiters = [];
89
+ async function acquire() {
90
+ if (active < maxConcurrent) {
91
+ active += 1;
92
+ return;
93
+ }
94
+ await new Promise((resolve) => waiters.push(resolve));
95
+ }
96
+ function release() {
97
+ const next = waiters.shift();
98
+ if (next) {
99
+ next();
100
+ return;
101
+ }
102
+ active = Math.max(0, active - 1);
103
+ }
104
+ return async (operation) => {
105
+ await acquire();
106
+ try {
107
+ return await retryOpenFileLimit(operation);
108
+ }
109
+ finally {
110
+ release();
69
111
  }
70
112
  };
71
113
  }
114
+ async function retryOpenFileLimit(operation) {
115
+ let attempt = 0;
116
+ for (;;) {
117
+ try {
118
+ return await operation();
119
+ }
120
+ catch (error) {
121
+ const delayMs = OPEN_FILE_RETRY_DELAYS_MS[attempt];
122
+ if (!isOpenFileLimitError(error) || delayMs === undefined) {
123
+ throw error;
124
+ }
125
+ attempt += 1;
126
+ await delay(delayMs);
127
+ }
128
+ }
129
+ }
130
+ function isOpenFileLimitError(error) {
131
+ const code = getErrorCode(error);
132
+ return code === "EMFILE" || code === "ENFILE";
133
+ }
134
+ function isMissingPathError(error) {
135
+ const code = getErrorCode(error);
136
+ return code === "ENOENT" || code === "ENOTDIR";
137
+ }
138
+ function getErrorCode(error) {
139
+ return typeof error === "object" && error !== null && "code" in error
140
+ ? String(error.code)
141
+ : undefined;
142
+ }
143
+ function delay(ms) {
144
+ return new Promise((resolve) => setTimeout(resolve, ms));
145
+ }
72
146
  export function resolveRepoPath(repoRoot, repoRelativePath) {
73
147
  if (path.isAbsolute(repoRelativePath)) {
74
148
  return repoRelativePath;
@@ -60,6 +60,11 @@ export function registerTranslationRoutes(app, deps) {
60
60
  await deps.translationService.clearSession(request.params.sessionId);
61
61
  return { ok: true };
62
62
  });
63
+ app.post("/api/translation/sessions/:sessionId/stop", async (request) => {
64
+ await requireCurrentProject(deps.projectService);
65
+ await deps.translationService.stopSession(request.params.sessionId);
66
+ return { ok: true };
67
+ });
63
68
  app.post("/api/translation/sessions/:sessionId/retry/:translationId", async (request) => {
64
69
  await requireCurrentProject(deps.projectService);
65
70
  return deps.translationService.retryTranslation(request.params.sessionId, request.params.translationId);
@@ -47,14 +47,14 @@ export function createWeixinIlinkChannel(options = {}) {
47
47
  async function fetchJson(input) {
48
48
  const controller = input.timeoutMs !== undefined ? new AbortController() : undefined;
49
49
  const timeout = controller ? setTimeout(() => controller.abort(), input.timeoutMs) : undefined;
50
- const signal = combineSignals(controller?.signal, input.signal);
50
+ const combinedSignal = combineSignals(controller?.signal, input.signal);
51
51
  const url = new URL(input.endpoint, ensureTrailingSlash(input.requestBaseUrl));
52
52
  try {
53
53
  const response = await fetchImpl(url, {
54
54
  method: input.method,
55
55
  headers: input.jsonHeaders === false ? buildIlinkAppHeaders() : buildJsonHeaders(input.token),
56
56
  body: input.body === undefined ? undefined : JSON.stringify(input.body),
57
- signal
57
+ signal: combinedSignal.signal
58
58
  });
59
59
  const rawText = await response.text();
60
60
  if (!response.ok) {
@@ -66,6 +66,7 @@ export function createWeixinIlinkChannel(options = {}) {
66
66
  if (timeout) {
67
67
  clearTimeout(timeout);
68
68
  }
69
+ combinedSignal.cleanup();
69
70
  }
70
71
  }
71
72
  async function post(input) {
@@ -285,10 +286,22 @@ function randomWechatUin() {
285
286
  }
286
287
  function combineSignals(first, second) {
287
288
  if (!first) {
288
- return second;
289
+ return {
290
+ signal: second,
291
+ cleanup() { }
292
+ };
289
293
  }
290
294
  if (!second) {
291
- return first;
295
+ return {
296
+ signal: first,
297
+ cleanup() { }
298
+ };
299
+ }
300
+ if (first === second) {
301
+ return {
302
+ signal: first,
303
+ cleanup() { }
304
+ };
292
305
  }
293
306
  const controller = new AbortController();
294
307
  const abort = () => controller.abort();
@@ -297,7 +310,13 @@ function combineSignals(first, second) {
297
310
  if (first.aborted || second.aborted) {
298
311
  controller.abort();
299
312
  }
300
- return controller.signal;
313
+ return {
314
+ signal: controller.signal,
315
+ cleanup() {
316
+ first.removeEventListener("abort", abort);
317
+ second.removeEventListener("abort", abort);
318
+ }
319
+ };
301
320
  }
302
321
  function stringOrUndefined(value) {
303
322
  return typeof value === "string" ? value : undefined;
@@ -25,28 +25,52 @@ export function createGatewayService(deps) {
25
25
  const now = deps.now ?? (() => new Date().toISOString());
26
26
  let pollAbort = null;
27
27
  let pollLoopPromise = null;
28
+ let pollStartingPromise = null;
28
29
  let qrLogin = null;
29
30
  let lastFailedTranslation = null;
30
31
  function isRunning() {
31
32
  return Boolean(pollAbort && !pollAbort.signal.aborted);
32
33
  }
33
34
  async function ensurePolling() {
34
- const settings = await deps.settings.loadSettings();
35
- if (!settings.binding.token || isRunning()) {
35
+ if (isRunning()) {
36
36
  return;
37
37
  }
38
- pollAbort = new AbortController();
39
- pollLoopPromise = pollLoop(pollAbort.signal).finally(() => {
40
- pollAbort = null;
41
- pollLoopPromise = null;
38
+ if (pollStartingPromise) {
39
+ await pollStartingPromise;
40
+ return;
41
+ }
42
+ pollStartingPromise = (async () => {
43
+ const settings = await deps.settings.loadSettings();
44
+ if (!settings.binding.token || isRunning()) {
45
+ return;
46
+ }
47
+ const controller = new AbortController();
48
+ pollAbort = controller;
49
+ const loop = pollLoop(controller.signal).finally(() => {
50
+ if (pollAbort === controller) {
51
+ pollAbort = null;
52
+ }
53
+ if (pollLoopPromise === loop) {
54
+ pollLoopPromise = null;
55
+ }
56
+ });
57
+ pollLoopPromise = loop;
58
+ })().finally(() => {
59
+ pollStartingPromise = null;
42
60
  });
61
+ await pollStartingPromise;
43
62
  }
44
63
  async function stopPolling() {
64
+ await pollStartingPromise?.catch(() => undefined);
45
65
  if (!pollAbort) {
46
66
  return;
47
67
  }
48
- pollAbort.abort();
68
+ const controller = pollAbort;
69
+ controller.abort();
49
70
  await pollLoopPromise?.catch(() => undefined);
71
+ if (pollAbort === controller) {
72
+ pollAbort = null;
73
+ }
50
74
  }
51
75
  function toAccount(settings) {
52
76
  if (!settings.binding.token) {
@@ -643,7 +667,7 @@ export function createGatewayService(deps) {
643
667
  await ensurePolling();
644
668
  },
645
669
  stop() {
646
- void stopPolling();
670
+ return stopPolling();
647
671
  },
648
672
  async getStatus() {
649
673
  const settings = await syncDesktopContext(await deps.settings.loadSettings());
@@ -33,16 +33,22 @@ export function createNodePtyTerminalRuntime(deps) {
33
33
  startedAt: now(),
34
34
  exitCode: null
35
35
  };
36
+ const logWriter = createTerminalLogWriter(deps.fs, input.logPath, deps.onLogWriteError ?? defaultLogWriteErrorHandler);
36
37
  const entry = {
37
38
  input,
38
39
  session,
39
40
  process: child,
40
- listeners: new Set()
41
+ listeners: new Set(),
42
+ logWriter,
43
+ disposed: false
41
44
  };
42
45
  entries.set(session.id, entry);
43
46
  child.onData((data) => {
47
+ if (entry.disposed) {
48
+ return;
49
+ }
44
50
  entry.session.lastOutputAt = now();
45
- void deps.fs.appendText(input.logPath, data);
51
+ entry.logWriter.append(data);
46
52
  emit(entry, {
47
53
  sessionId: session.id,
48
54
  taskSlug: input.taskSlug,
@@ -52,8 +58,13 @@ export function createNodePtyTerminalRuntime(deps) {
52
58
  });
53
59
  });
54
60
  child.onExit(({ exitCode }) => {
61
+ if (entry.disposed) {
62
+ return;
63
+ }
64
+ entry.disposed = true;
55
65
  entry.session.status = exitCode === 0 ? "exited" : "crashed";
56
66
  entry.session.exitCode = exitCode;
67
+ void entry.logWriter.close();
57
68
  emit(entry, {
58
69
  sessionId: session.id,
59
70
  taskSlug: input.taskSlug,
@@ -102,12 +113,16 @@ export function createNodePtyTerminalRuntime(deps) {
102
113
  },
103
114
  async stop(sessionId) {
104
115
  const entry = getEntry(entries, sessionId);
116
+ entry.disposed = true;
105
117
  entry.session.status = "exited";
106
118
  entry.process.kill();
119
+ await entry.logWriter.close();
107
120
  },
108
121
  async restart(sessionId) {
109
122
  const entry = getEntry(entries, sessionId);
123
+ entry.disposed = true;
110
124
  entry.process.kill();
125
+ await entry.logWriter.close();
111
126
  return create(entry.input, sessionId);
112
127
  },
113
128
  subscribe(sessionId, listener, options = {}) {
@@ -139,12 +154,40 @@ export function createNodePtyTerminalRuntime(deps) {
139
154
  }
140
155
  };
141
156
  }
157
+ export function createTerminalLogWriter(fs, logPath, onError) {
158
+ let closed = false;
159
+ let pending = Promise.resolve();
160
+ return {
161
+ append(data) {
162
+ if (closed || !data) {
163
+ return;
164
+ }
165
+ pending = pending
166
+ .then(() => fs.appendText(logPath, data))
167
+ .catch((error) => {
168
+ try {
169
+ onError(error, logPath);
170
+ }
171
+ catch {
172
+ // Logging must never break terminal output delivery or future log writes.
173
+ }
174
+ });
175
+ },
176
+ async close() {
177
+ closed = true;
178
+ await pending.catch(() => undefined);
179
+ }
180
+ };
181
+ }
142
182
  async function readTerminalReplayText(fs, logPath) {
143
183
  const data = fs.readTextTail
144
184
  ? await fs.readTextTail(logPath, TERMINAL_REPLAY_TAIL_LIMIT_BYTES)
145
185
  : tailTerminalReplay(await fs.readText(logPath));
146
186
  return tailTerminalReplay(data);
147
187
  }
188
+ function defaultLogWriteErrorHandler(error, logPath) {
189
+ console.warn(`[VCM] Failed to append terminal log: ${logPath}`, error);
190
+ }
148
191
  export function tailTerminalReplay(data, limitBytes = TERMINAL_REPLAY_TAIL_LIMIT_BYTES) {
149
192
  if (limitBytes <= 0 || Buffer.byteLength(data, "utf8") <= limitBytes) {
150
193
  return data;
@@ -111,7 +111,7 @@ export async function createServer(deps, options = {}) {
111
111
  await deps.gatewayService.start();
112
112
  });
113
113
  app.addHook("onClose", async () => {
114
- deps.gatewayService.stop();
114
+ await deps.gatewayService.stop();
115
115
  });
116
116
  if (options.staticDir) {
117
117
  await app.register(fastifyStatic, {
@@ -703,6 +703,10 @@ export function createTranslationService(deps) {
703
703
  });
704
704
  return () => {
705
705
  state.listeners.delete(listener);
706
+ if (state.listeners.size === 0 && state.unsubscribeTranscript) {
707
+ state.unsubscribeTranscript();
708
+ state.unsubscribeTranscript = undefined;
709
+ }
706
710
  };
707
711
  },
708
712
  async clearSession(sessionId) {