vibe-coding-master 0.3.4 → 0.3.6

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;
@@ -0,0 +1,5 @@
1
+ export function registerDiagnosticsRoutes(app, deps) {
2
+ app.get("/api/diagnostics/runtime", async () => {
3
+ return deps.diagnosticsService.getRuntimeDiagnostics();
4
+ });
5
+ }
@@ -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());
@@ -802,6 +826,11 @@ export function createGatewayService(deps) {
802
826
  preview: output.text,
803
827
  error: output.translationError
804
828
  });
829
+ },
830
+ getDiagnostics() {
831
+ return {
832
+ polling: isRunning()
833
+ };
805
834
  }
806
835
  };
807
836
  function getLatestPmReply(settings) {
@@ -17,6 +17,7 @@ import { createNodeFileSystemAdapter } from "./adapters/filesystem.js";
17
17
  import { createNodePtyTerminalRuntime } from "./runtime/node-pty-runtime.js";
18
18
  import { createOpenAiCompatibleTranslationProvider } from "./adapters/translation-provider.js";
19
19
  import { registerGatewayRoutes } from "./api/gateway-routes.js";
20
+ import { registerDiagnosticsRoutes } from "./api/diagnostics-routes.js";
20
21
  import { createWeixinIlinkChannel } from "./gateway/channels/weixin-ilink-channel.js";
21
22
  import { createGatewayAuditLog } from "./gateway/gateway-audit-log.js";
22
23
  import { createGatewayService } from "./gateway/gateway-service.js";
@@ -30,6 +31,7 @@ import { createRoundService } from "./services/round-service.js";
30
31
  import { createStatusService } from "./services/status-service.js";
31
32
  import { createTaskService } from "./services/task-service.js";
32
33
  import { createTranslationService } from "./services/translation-service.js";
34
+ import { createDiagnosticsService } from "./services/diagnostics-service.js";
33
35
  import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
34
36
  import { registerArtifactRoutes } from "./api/artifact-routes.js";
35
37
  import { registerClaudeHookRoutes } from "./api/claude-hook-routes.js";
@@ -54,10 +56,12 @@ export async function createServer(deps, options = {}) {
54
56
  error: {
55
57
  code: vcmError.code,
56
58
  message: vcmError.message,
57
- hint: vcmError.hint
59
+ hint: vcmError.hint,
60
+ runtime: deps.diagnosticsService.getErrorRuntimeInfo()
58
61
  }
59
62
  });
60
63
  });
64
+ registerDiagnosticsRoutes(app, { diagnosticsService: deps.diagnosticsService });
61
65
  registerAppSettingsRoutes(app, { appSettings: deps.appSettings });
62
66
  registerClaudeHookRoutes(app, { claudeHookService: deps.claudeHookService });
63
67
  registerCodexHookRoutes(app, { codexHookService: deps.codexHookService });
@@ -111,7 +115,7 @@ export async function createServer(deps, options = {}) {
111
115
  await deps.gatewayService.start();
112
116
  });
113
117
  app.addHook("onClose", async () => {
114
- deps.gatewayService.stop();
118
+ await deps.gatewayService.stop();
115
119
  });
116
120
  if (options.staticDir) {
117
121
  await app.register(fastifyStatic, {
@@ -148,7 +152,7 @@ export function createDefaultServerDeps(options = {}) {
148
152
  const runtime = createNodePtyTerminalRuntime({ fs });
149
153
  const registry = createSessionRegistry();
150
154
  const artifactService = createArtifactService(fs);
151
- const projectService = createProjectService({ fs, git, claude, appSettings });
155
+ const projectService = createProjectService({ fs, git, appSettings });
152
156
  const harnessService = createHarnessService({
153
157
  fs,
154
158
  runtime,
@@ -248,6 +252,12 @@ export function createDefaultServerDeps(options = {}) {
248
252
  sessionService,
249
253
  roundService
250
254
  });
255
+ const diagnosticsService = createDiagnosticsService({
256
+ appRoot: getAppRoot(),
257
+ runtime,
258
+ gatewayService,
259
+ translationService
260
+ });
251
261
  return {
252
262
  appSettings,
253
263
  projectService,
@@ -264,7 +274,8 @@ export function createDefaultServerDeps(options = {}) {
264
274
  statusService,
265
275
  translationService,
266
276
  gatewayService,
267
- runtime
277
+ runtime,
278
+ diagnosticsService
268
279
  };
269
280
  }
270
281
  export function getDefaultStaticDir() {
@@ -0,0 +1,74 @@
1
+ import { readFileSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ export function createDiagnosticsService(deps) {
5
+ const version = readPackageVersion(deps.appRoot);
6
+ return {
7
+ async getRuntimeDiagnostics() {
8
+ const runtimeSessions = deps.runtime.listSessions();
9
+ return {
10
+ version,
11
+ pid: process.pid,
12
+ cwd: process.cwd(),
13
+ execPath: process.execPath,
14
+ nodeVersion: process.version,
15
+ platform: process.platform,
16
+ arch: process.arch,
17
+ uptimeSeconds: Math.round(process.uptime()),
18
+ fdCount: await readFdCount(),
19
+ openFilesLimit: await readOpenFilesLimit(),
20
+ runtimeSessions: {
21
+ total: runtimeSessions.length,
22
+ running: runtimeSessions.filter((session) => session.status === "running").length
23
+ },
24
+ gateway: deps.gatewayService.getDiagnostics(),
25
+ translation: deps.translationService.getDiagnostics()
26
+ };
27
+ },
28
+ getErrorRuntimeInfo() {
29
+ return {
30
+ version,
31
+ pid: process.pid,
32
+ cwd: process.cwd()
33
+ };
34
+ }
35
+ };
36
+ }
37
+ function readPackageVersion(appRoot) {
38
+ const packageJsonPath = path.join(appRoot, "package.json");
39
+ try {
40
+ const raw = JSON.parse(readFileSync(packageJsonPath, "utf8"));
41
+ return typeof raw.version === "string" && raw.version.trim() ? raw.version : "unknown";
42
+ }
43
+ catch {
44
+ return process.env.npm_package_version || "unknown";
45
+ }
46
+ }
47
+ async function readFdCount() {
48
+ try {
49
+ return (await fs.readdir("/proc/self/fd")).length;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function readOpenFilesLimit() {
56
+ try {
57
+ const limits = await fs.readFile("/proc/self/limits", "utf8");
58
+ const line = limits.split("\n").find((candidate) => candidate.startsWith("Max open files"));
59
+ if (!line) {
60
+ return null;
61
+ }
62
+ const match = line.match(/^Max open files\s+(\S+)\s+(\S+)/);
63
+ if (!match) {
64
+ return null;
65
+ }
66
+ return {
67
+ soft: match[1] ?? "unknown",
68
+ hard: match[2] ?? "unknown"
69
+ };
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
@@ -139,9 +139,6 @@ export function createProjectService(deps) {
139
139
  warnings.push(`Unable to read Git ahead/behind status. ${getErrorHint(caught)}`);
140
140
  }
141
141
  }
142
- if (!(await deps.claude.isAvailable(config.claudeCommand))) {
143
- warnings.push("Claude Code command is not available. You can still inspect artifacts, but sessions will not start.");
144
- }
145
142
  const pullDisabledReason = getPullDisabledReason({ isDirty, upstreamBranch });
146
143
  return {
147
144
  repoRoot,
@@ -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) {
@@ -795,6 +799,21 @@ export function createTranslationService(deps) {
795
799
  userPrompt: prompt.userPrompt
796
800
  });
797
801
  return result.text.trim();
802
+ },
803
+ getDiagnostics() {
804
+ let transcriptWatchers = 0;
805
+ let listeners = 0;
806
+ for (const state of sessionStates.values()) {
807
+ if (state.unsubscribeTranscript) {
808
+ transcriptWatchers += 1;
809
+ }
810
+ listeners += state.listeners.size;
811
+ }
812
+ return {
813
+ sessions: sessionStates.size,
814
+ transcriptWatchers,
815
+ listeners
816
+ };
798
817
  }
799
818
  };
800
819
  async function writeToCurrentRole(repoRoot, taskSlug, role, text) {
@@ -0,0 +1 @@
1
+ export {};