revdeck 0.1.0 → 0.2.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.
package/dist/daemon.js CHANGED
@@ -74,6 +74,16 @@ var init_locale = __esm({
74
74
  }
75
75
  });
76
76
 
77
+ // src/core/types.ts
78
+ var LEVELS, REACTIONS;
79
+ var init_types = __esm({
80
+ "src/core/types.ts"() {
81
+ "use strict";
82
+ LEVELS = ["blocker", "major", "minor", "question"];
83
+ REACTIONS = ["thumbsup", "thumbsdown", "smile", "tada", "confused", "heart", "rocket", "eyes"];
84
+ }
85
+ });
86
+
77
87
  // src/core/errors.ts
78
88
  function messageOf(error) {
79
89
  if (error instanceof Error) return error.message;
@@ -289,11 +299,11 @@ function diffStats(files) {
289
299
  function changedPaths(files) {
290
300
  return files.map((file) => file.path);
291
301
  }
292
- function findFile(files, path8) {
293
- return files.find((file) => file.path === path8) ?? files.find((file) => file.oldPath === path8);
302
+ function findFile(files, path9) {
303
+ return files.find((file) => file.path === path9) ?? files.find((file) => file.oldPath === path9);
294
304
  }
295
- function findNewLine(files, path8, line) {
296
- const file = findFile(files, path8);
305
+ function findNewLine(files, path9, line) {
306
+ const file = findFile(files, path9);
297
307
  if (!file) return void 0;
298
308
  for (const hunk of file.hunks) {
299
309
  for (const item of hunk.lines) {
@@ -302,8 +312,8 @@ function findNewLine(files, path8, line) {
302
312
  }
303
313
  return void 0;
304
314
  }
305
- function snippetAround(files, path8, line, radius = 6) {
306
- const file = findFile(files, path8);
315
+ function snippetAround(files, path9, line, radius = 6) {
316
+ const file = findFile(files, path9);
307
317
  if (!file || line == null) return null;
308
318
  const flat = file.hunks.flatMap((hunk) => hunk.lines);
309
319
  const target = flat.findIndex((item) => item.newLine === line);
@@ -316,9 +326,9 @@ function snippetAround(files, path8, line, radius = 6) {
316
326
  target: item.newLine === line
317
327
  }));
318
328
  }
319
- function linePosition(files, path8, line) {
320
- const file = findFile(files, path8);
321
- const found = findNewLine(files, path8, line);
329
+ function linePosition(files, path9, line) {
330
+ const file = findFile(files, path9);
331
+ const found = findNewLine(files, path9, line);
322
332
  if (!file || !found) return null;
323
333
  return { newPath: file.path, newLine: line, oldPath: file.oldPath, oldLine: found.kind === " " ? found.oldLine : null };
324
334
  }
@@ -429,8 +439,8 @@ async function withTempJson(data, fn) {
429
439
  await rm(dir, { recursive: true, force: true });
430
440
  }
431
441
  }
432
- function query(path8, params) {
433
- const url = new URL(path8.startsWith("/") ? path8 : `/${path8}`, "http://local");
442
+ function query(path9, params) {
443
+ const url = new URL(path9.startsWith("/") ? path9 : `/${path9}`, "http://local");
434
444
  for (const [key, value] of Object.entries(params)) {
435
445
  if (value !== void 0 && value !== "") url.searchParams.set(key, String(value));
436
446
  }
@@ -459,6 +469,7 @@ var EMPTY_SHA, GitLabProvider;
459
469
  var init_gitlab = __esm({
460
470
  "src/providers/gitlab.ts"() {
461
471
  "use strict";
472
+ init_types();
462
473
  init_diff();
463
474
  init_errors();
464
475
  init_redact();
@@ -475,7 +486,7 @@ var init_gitlab = __esm({
475
486
  ref;
476
487
  options;
477
488
  platform = "gitlab";
478
- capabilities = { reactions: true, resolve: true, nativeDrafts: true };
489
+ capabilities = { reactions: true, resolve: true, nativeDrafts: true, attachments: true };
479
490
  run;
480
491
  me = null;
481
492
  infoCache = null;
@@ -643,6 +654,24 @@ rename to ${newPath}
643
654
  await this.call(["--method", "PUT", this.endpoint(`/discussions/${encodeURIComponent(threadId)}`), "--input", file]);
644
655
  });
645
656
  }
657
+ async reactions(noteId) {
658
+ const base = this.endpoint(`/notes/${encodeURIComponent(noteId)}/award_emoji`);
659
+ const items = await this.paged(base);
660
+ const me = await this.currentUser();
661
+ const grouped = /* @__PURE__ */ new Map();
662
+ for (const item of items) {
663
+ if (!REACTIONS.includes(item.name)) continue;
664
+ const reaction = grouped.get(item.name) ?? { name: item.name, count: 0, users: [], mine: false };
665
+ reaction.count += 1;
666
+ const username = item.user?.username;
667
+ if (username) {
668
+ reaction.users.push(username);
669
+ if (username === me.username) reaction.mine = true;
670
+ }
671
+ grouped.set(item.name, reaction);
672
+ }
673
+ return [...grouped.values()].sort((left, right) => left.name.localeCompare(right.name));
674
+ }
646
675
  async react(noteId, emoji) {
647
676
  const base = this.endpoint(`/notes/${encodeURIComponent(noteId)}/award_emoji`);
648
677
  const existing = await this.json([query(base, { per_page: 100 })]);
@@ -657,6 +686,16 @@ rename to ${newPath}
657
686
  });
658
687
  return { active: true };
659
688
  }
689
+ async upload(input) {
690
+ const endpoint = `projects/${this.projectRef}/uploads`;
691
+ const uploaded = await this.json(["--method", "POST", endpoint, "--form", `file=@${input.path}`]);
692
+ const raw = uploaded.full_path || uploaded.url || "";
693
+ if (!raw) throw new ProviderError(tr("GitLab \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043D\u044B\u0439 \u0444\u0430\u0439\u043B", "GitLab did not return a link to the uploaded file"));
694
+ const absolute = raw.startsWith("http://") || raw.startsWith("https://") ? raw : `https://${this.ref.host}${raw.startsWith("/") ? "" : "/"}${raw}`;
695
+ const label = input.name.replace(/[[\]\r\n]/g, " ").trim() || "attachment";
696
+ const markdown = input.contentType.startsWith("image/") || input.contentType.startsWith("video/") ? `![${label}](${absolute})` : `[${label}](${absolute})`;
697
+ return { name: input.name, url: absolute, markdown };
698
+ }
660
699
  async createThread(input) {
661
700
  const info = await this.info();
662
701
  if (!info.commit.head || info.commit.head === EMPTY_SHA) {
@@ -693,7 +732,7 @@ __export(github_exports, {
693
732
  GitHubProvider: () => GitHubProvider
694
733
  });
695
734
  import { tmpdir as tmpdir3 } from "node:os";
696
- var THREADS_QUERY, REACTIONS, GitHubProvider;
735
+ var THREADS_QUERY, REACTIONS2, CONTENT_TO_NAME, GitHubProvider;
697
736
  var init_github = __esm({
698
737
  "src/providers/github.ts"() {
699
738
  "use strict";
@@ -717,7 +756,7 @@ var init_github = __esm({
717
756
  }
718
757
  }
719
758
  }`;
720
- REACTIONS = {
759
+ REACTIONS2 = {
721
760
  thumbsup: "+1",
722
761
  thumbsdown: "-1",
723
762
  smile: "laugh",
@@ -727,6 +766,7 @@ var init_github = __esm({
727
766
  rocket: "rocket",
728
767
  eyes: "eyes"
729
768
  };
769
+ CONTENT_TO_NAME = Object.fromEntries(Object.entries(REACTIONS2).map(([name, content]) => [content, name]));
730
770
  GitHubProvider = class {
731
771
  constructor(ref, options) {
732
772
  this.ref = ref;
@@ -742,7 +782,7 @@ var init_github = __esm({
742
782
  ref;
743
783
  options;
744
784
  platform = "github";
745
- capabilities = { reactions: true, resolve: true, nativeDrafts: false };
785
+ capabilities = { reactions: true, resolve: true, nativeDrafts: false, attachments: false };
746
786
  run;
747
787
  owner;
748
788
  name;
@@ -945,8 +985,27 @@ rename to ${newPath}
945
985
  const mutation = resolved ? "mutation($id: ID!) { resolveReviewThread(input: { threadId: $id }) { thread { isResolved } } }" : "mutation($id: ID!) { unresolveReviewThread(input: { threadId: $id }) { thread { isResolved } } }";
946
986
  await this.graphql(mutation, { id: threadId });
947
987
  }
988
+ async reactions(noteId) {
989
+ const base = `repos/${this.repo}/pulls/comments/${encodeURIComponent(noteId)}/reactions`;
990
+ const items = await this.paged(base);
991
+ const me = await this.currentUser();
992
+ const grouped = /* @__PURE__ */ new Map();
993
+ for (const item of items) {
994
+ const name = CONTENT_TO_NAME[item.content];
995
+ if (!name) continue;
996
+ const reaction = grouped.get(name) ?? { name, count: 0, users: [], mine: false };
997
+ reaction.count += 1;
998
+ const login = item.user?.login;
999
+ if (login) {
1000
+ reaction.users.push(login);
1001
+ if (login === me.username) reaction.mine = true;
1002
+ }
1003
+ grouped.set(name, reaction);
1004
+ }
1005
+ return [...grouped.values()].sort((left, right) => left.name.localeCompare(right.name));
1006
+ }
948
1007
  async react(noteId, emoji) {
949
- const content = REACTIONS[emoji];
1008
+ const content = REACTIONS2[emoji];
950
1009
  if (!content) throw new ProviderError(tr(`GitHub \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0440\u0435\u0430\u043A\u0446\u0438\u044E ${emoji}`, `GitHub does not support reaction ${emoji}`));
951
1010
  const base = `repos/${this.repo}/pulls/comments/${noteId}/reactions`;
952
1011
  const existing = await this.json([query(base, { per_page: 100 })]);
@@ -961,6 +1020,12 @@ rename to ${newPath}
961
1020
  });
962
1021
  return { active: true };
963
1022
  }
1023
+ async upload(_input) {
1024
+ throw new ProviderError(
1025
+ tr("GitHub \u043D\u0435 \u0434\u0430\u0451\u0442 \u043F\u0443\u0431\u043B\u0438\u0447\u043D\u043E\u0433\u043E API \u0434\u043B\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0439", "GitHub has no public API for uploading attachments"),
1026
+ tr("\u0432\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u0430\u0431\u0441\u043E\u043B\u044E\u0442\u043D\u0443\u044E \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0444\u0430\u0439\u043B \u0432\u0440\u0443\u0447\u043D\u0443\u044E; \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F GitLab", "paste an absolute file URL manually; uploads are supported for GitLab only")
1027
+ );
1028
+ }
964
1029
  async createThread(input) {
965
1030
  if (input.line === null) {
966
1031
  const comment2 = await withTempJson(
@@ -992,7 +1057,7 @@ rename to ${newPath}
992
1057
  import { existsSync as existsSync4, readFileSync } from "node:fs";
993
1058
  import { appendFile, open as open2, readFile as readFile3, unlink as unlink3 } from "node:fs/promises";
994
1059
  import net2 from "node:net";
995
- import path7 from "node:path";
1060
+ import path8 from "node:path";
996
1061
  import { fileURLToPath } from "node:url";
997
1062
  import { parseArgs } from "node:util";
998
1063
 
@@ -1084,7 +1149,7 @@ async function writeFileAtomic(file, data, mode = 384) {
1084
1149
  init_redact();
1085
1150
 
1086
1151
  // src/version.ts
1087
- var VERSION = "0.1.0";
1152
+ var VERSION = "0.2.0";
1088
1153
  var PROTOCOL_VERSION = 1;
1089
1154
 
1090
1155
  // src/daemon/main.ts
@@ -1095,10 +1160,8 @@ import { DatabaseSync } from "node:sqlite";
1095
1160
  import { mkdirSync } from "node:fs";
1096
1161
  import path2 from "node:path";
1097
1162
 
1098
- // src/core/types.ts
1099
- var LEVELS = ["blocker", "major", "minor", "question"];
1100
-
1101
1163
  // src/core/refs.ts
1164
+ init_types();
1102
1165
  function reviewId(ref) {
1103
1166
  return `${ref.platform}:${ref.host}/${ref.project}!${ref.number}`;
1104
1167
  }
@@ -1639,6 +1702,9 @@ init_redact();
1639
1702
  init_errors();
1640
1703
  import { EventEmitter } from "node:events";
1641
1704
  import { createHash as createHash2, randomBytes } from "node:crypto";
1705
+ import { mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
1706
+ import { tmpdir as tmpdir4 } from "node:os";
1707
+ import path6 from "node:path";
1642
1708
 
1643
1709
  // src/core/config.ts
1644
1710
  import { z } from "zod";
@@ -1691,8 +1757,8 @@ function globToRegExp(glob) {
1691
1757
  function escapeRegExp(value) {
1692
1758
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1693
1759
  }
1694
- function classifyFile(path8) {
1695
- const lower = path8.toLowerCase();
1760
+ function classifyFile(path9) {
1761
+ const lower = path9.toLowerCase();
1696
1762
  if (/(^|\/)(node_modules|dist|build|coverage|vendor)\//.test(lower)) return "generated";
1697
1763
  if (/\.(md|mdx|rst|txt|adoc)$/.test(lower) || /(^|\/)(docs?|adr)\//.test(lower)) return "docs";
1698
1764
  if (/(^|\/)(__tests__|tests?|spec|e2e)\//.test(lower) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(lower) || /_test\.go$/.test(lower)) return "test";
@@ -2004,7 +2070,8 @@ init_locale();
2004
2070
  var CONTROL_OR_HIDDEN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u00ad\u061c\u115f\u1160\u180e\u200b-\u200f\u2028-\u202e\u2060-\u206f\u2800\u3164\ufeff\uffa0]/;
2005
2071
  var QUICK_ACTION = /^[\s\p{Z}\p{Cf}]*\/[a-z_]+(?=[\s\p{Z}\p{Cf}]|$)/imu;
2006
2072
  var MASS_MENTION = /(^|[^\w@./-])@(all|here|everyone)(?![\w-])/i;
2007
- var MD_IMAGE = /!\[[^\]]*\]\s*\(|!\[[^\]]*\]\s*\[/;
2073
+ var MD_IMAGE_REF = /!\[[^\]]*\]\s*\[/;
2074
+ var MD_IMAGE_LINK = /!\[[^\]]*\]\s*\(\s*<?([^)\s>]+)>?(?:\s+["'][^"']*["'])?\s*\)/g;
2008
2075
  var HTML_TAG = /<(?:img|script|iframe|style|video|audio|object|embed|svg|math)\b/i;
2009
2076
  var HTML_COMMENT = /<!--/;
2010
2077
  var MAX_LENGTH = 64 * 1024;
@@ -2017,15 +2084,113 @@ function sanitizeBody(body, platform, options = {}) {
2017
2084
  if (CONTROL_OR_HIDDEN.test(text2)) return { ok: false, reason: tr("\u0432 \u0442\u0435\u043A\u0441\u0442\u0435 \u0435\u0441\u0442\u044C \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u044E\u0449\u0438\u0435 \u0438\u043B\u0438 \u043D\u0435\u0432\u0438\u0434\u0438\u043C\u044B\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B", "the text contains control or invisible characters"), text: text2 };
2018
2085
  if (HTML_COMMENT.test(text2)) return { ok: false, reason: tr("\u0432 \u0442\u0435\u043A\u0441\u0442\u0435 \u0435\u0441\u0442\u044C HTML-\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439", "the text contains an HTML comment"), text: text2 };
2019
2086
  if (HTML_TAG.test(text2)) return { ok: false, reason: tr("\u0432 \u0442\u0435\u043A\u0441\u0442\u0435 \u0435\u0441\u0442\u044C HTML-\u0442\u0435\u0433", "the text contains an HTML tag"), text: text2 };
2020
- if (MD_IMAGE.test(text2)) return { ok: false, reason: tr("\u0432 \u0442\u0435\u043A\u0441\u0442\u0435 \u0435\u0441\u0442\u044C \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430: \u0437\u0430\u043C\u0435\u043D\u0438\u0442\u0435 \u0441\u0441\u044B\u043B\u043A\u043E\u0439", "the text contains an image: replace it with a link"), text: text2 };
2087
+ const imageProblem = imageIssue(text2, options.host);
2088
+ if (imageProblem) return { ok: false, reason: imageProblem, text: text2 };
2021
2089
  if (MASS_MENTION.test(text2)) return { ok: false, reason: tr("\u043C\u0430\u0441\u0441\u043E\u0432\u043E\u0435 \u0443\u043F\u043E\u043C\u0438\u043D\u0430\u043D\u0438\u0435: \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u044B\u0445 \u043B\u044E\u0434\u0435\u0439", "mass mention: name specific people"), text: text2 };
2022
2090
  if (checkQuickActions && QUICK_ACTION.test(text2)) {
2023
2091
  return { ok: false, reason: tr("\u0441\u0442\u0440\u043E\u043A\u0430-\u043A\u043E\u043C\u0430\u043D\u0434\u0430 GitLab (/approve, /merge \u2026): \u043F\u0435\u0440\u0435\u0444\u0440\u0430\u0437\u0438\u0440\u0443\u0439\u0442\u0435", "GitLab quick action line (/approve, /merge \u2026): rephrase it"), text: text2 };
2024
2092
  }
2025
2093
  return { ok: true, reason: "", text: text2 };
2026
2094
  }
2095
+ function imageIssue(text2, host) {
2096
+ if (MD_IMAGE_REF.test(text2)) {
2097
+ return tr("\u0432 \u0442\u0435\u043A\u0441\u0442\u0435 \u0435\u0441\u0442\u044C \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u0441\u043E \u0441\u0441\u044B\u043B\u043E\u0447\u043D\u043E\u0439 \u0441\u043D\u043E\u0441\u043A\u043E\u0439: \u0432\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u0430\u0431\u0441\u043E\u043B\u044E\u0442\u043D\u0443\u044E \u0441\u0441\u044B\u043B\u043A\u0443", "the text contains a reference-style image: use an absolute link");
2098
+ }
2099
+ for (const match of text2.matchAll(MD_IMAGE_LINK)) {
2100
+ const url = match[1];
2101
+ if (!host) {
2102
+ return tr("\u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u044B \u0442\u043E\u043B\u044C\u043A\u043E \u0441 \u0445\u043E\u0441\u0442\u0430 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u044B", "images are allowed only from the platform host");
2103
+ }
2104
+ if (url.startsWith("/uploads/")) continue;
2105
+ try {
2106
+ const parsed = new URL(url);
2107
+ if ((parsed.protocol === "https:" || parsed.protocol === "http:") && parsed.host === host) continue;
2108
+ } catch {
2109
+ }
2110
+ return tr(`\u0432\u043D\u0435\u0448\u043D\u044F\u044F \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 ${url}: \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u044B \u0442\u043E\u043B\u044C\u043A\u043E \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 \u0441 ${host}`, `external image ${url}: only uploads from ${host} are allowed`);
2111
+ }
2112
+ return null;
2113
+ }
2114
+ function normalizeMediaLinks(body, platform, host) {
2115
+ if (platform !== "gitlab" || !host) return body;
2116
+ const base = `https://${host}`;
2117
+ return body.replace(/(!?\[[^\]]*\]\(\s*)(\/?uploads\/[^)\s]+)/g, (_match, prefix, path9) => {
2118
+ return `${prefix}${path9.startsWith("/") ? base : `${base}/`}${path9}`;
2119
+ });
2120
+ }
2121
+
2122
+ // src/core/attachments.ts
2123
+ var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
2124
+ var MIME_BY_EXTENSION = {
2125
+ ".png": "image/png",
2126
+ ".jpg": "image/jpeg",
2127
+ ".jpeg": "image/jpeg",
2128
+ ".gif": "image/gif",
2129
+ ".webp": "image/webp",
2130
+ ".avif": "image/avif",
2131
+ ".mp4": "video/mp4",
2132
+ ".m4v": "video/mp4",
2133
+ ".webm": "video/webm",
2134
+ ".ogv": "video/ogg",
2135
+ ".ogg": "video/ogg",
2136
+ ".mov": "video/quicktime"
2137
+ };
2138
+ function extensionOf(name) {
2139
+ const index = name.lastIndexOf(".");
2140
+ return index >= 0 ? name.slice(index).toLowerCase() : "";
2141
+ }
2142
+ function attachmentSpec(name, contentType = "") {
2143
+ if (/[/\\]/.test(name)) return null;
2144
+ const base = name.replace(/[\u0000-\u001f\u007f]/g, "").trim();
2145
+ if (!base || base.length > 240) return null;
2146
+ const extension = extensionOf(base);
2147
+ const expected = MIME_BY_EXTENSION[extension];
2148
+ if (!expected) return null;
2149
+ const provided = normalizeContentType(String(contentType));
2150
+ if (provided && provided !== expected) return null;
2151
+ return {
2152
+ name: base,
2153
+ contentType: expected,
2154
+ extension,
2155
+ kind: expected.startsWith("image/") ? "image" : "video"
2156
+ };
2157
+ }
2158
+ var CONTENT_TYPE_ALIASES = {
2159
+ "image/jpg": "image/jpeg",
2160
+ "image/pjpeg": "image/jpeg",
2161
+ "video/x-m4v": "video/mp4",
2162
+ "video/x-ogg": "video/ogg"
2163
+ };
2164
+ function normalizeContentType(value) {
2165
+ const type = value.split(";")[0].trim().toLowerCase();
2166
+ return CONTENT_TYPE_ALIASES[type] ?? type;
2167
+ }
2168
+ function decodeAttachment(data) {
2169
+ const clean = String(data ?? "").replace(/\s+/g, "");
2170
+ if (!clean || clean.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(clean)) {
2171
+ throw new Error("\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435: \u043E\u0436\u0438\u0434\u0430\u043B\u0441\u044F base64");
2172
+ }
2173
+ const buffer = Buffer.from(clean, "base64");
2174
+ if (!buffer.length) throw new Error("\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u0443\u0441\u0442\u043E\u0435");
2175
+ if (buffer.length > MAX_ATTACHMENT_BYTES) {
2176
+ throw new Error(`\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${Math.floor(MAX_ATTACHMENT_BYTES / (1024 * 1024))} \u041C\u0411`);
2177
+ }
2178
+ return buffer;
2179
+ }
2180
+ function matchesSignature(buffer, contentType) {
2181
+ if (contentType === "image/png") return buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
2182
+ if (contentType === "image/jpeg") return buffer.length > 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255;
2183
+ if (contentType === "image/gif") return buffer.subarray(0, 4).toString("latin1") === "GIF8";
2184
+ if (contentType === "image/webp") return buffer.subarray(0, 4).toString("latin1") === "RIFF" && buffer.subarray(8, 12).toString("latin1") === "WEBP";
2185
+ if (contentType === "image/avif") return buffer.subarray(4, 8).toString("latin1") === "ftyp";
2186
+ if (contentType === "video/webm") return buffer.subarray(0, 4).equals(Buffer.from([26, 69, 223, 163]));
2187
+ if (contentType === "video/ogg") return buffer.subarray(0, 4).toString("latin1") === "OggS";
2188
+ if (contentType === "video/mp4" || contentType === "video/quicktime") return buffer.subarray(4, 8).toString("latin1") === "ftyp";
2189
+ return false;
2190
+ }
2027
2191
 
2028
2192
  // src/core/findings.ts
2193
+ init_types();
2029
2194
  import { createHash } from "node:crypto";
2030
2195
  init_locale();
2031
2196
  var LEVEL_ORDER = { blocker: 0, major: 1, minor: 2, question: 3 };
@@ -2159,6 +2324,7 @@ function normalize(value) {
2159
2324
 
2160
2325
  // src/daemon/service.ts
2161
2326
  init_locale();
2327
+ init_types();
2162
2328
 
2163
2329
  // src/core/git.ts
2164
2330
  init_exec();
@@ -2368,6 +2534,14 @@ var ReviewService = class {
2368
2534
  await this.refresh(id, meta.row.root, provider, loaded);
2369
2535
  return this.getReview(id);
2370
2536
  }
2537
+ async providerFor(id) {
2538
+ const stored = this.store.getReview(id);
2539
+ if (!stored) throw new Error(tr(`\u0440\u0435\u0432\u044C\u044E ${id} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E`, `review ${id} not found`));
2540
+ return this.providerFactory(
2541
+ { platform: stored.row.platform, host: stored.row.host, project: stored.row.project, number: stored.row.number },
2542
+ stored.row.root
2543
+ );
2544
+ }
2371
2545
  async refresh(id, root, provider, loaded) {
2372
2546
  const errors = [...loaded.warnings];
2373
2547
  const previous = this.store.getReview(id)?.snapshot;
@@ -2581,6 +2755,77 @@ ${errors.join("\n")}`));
2581
2755
  this.emit(id);
2582
2756
  return next;
2583
2757
  }
2758
+ // ---------------------------------------------------------------- reactions
2759
+ /** Реакции для набора комментариев; отсутствующие id пропускаются. */
2760
+ async reactions(id, noteIds) {
2761
+ const review = this.getReview(id);
2762
+ const known = new Set(review.threads.flatMap((thread) => thread.notes.map((note) => note.id)));
2763
+ const requested = [...new Set(noteIds.filter((noteId) => known.has(noteId)))].slice(0, 500);
2764
+ if (!requested.length) return {};
2765
+ const provider = await this.providerFor(id);
2766
+ const result = {};
2767
+ for (const noteId of requested) {
2768
+ try {
2769
+ result[noteId] = await provider.reactions(noteId);
2770
+ } catch {
2771
+ result[noteId] = [];
2772
+ }
2773
+ }
2774
+ return result;
2775
+ }
2776
+ async react(id, noteId, emoji) {
2777
+ if (!REACTIONS.includes(emoji)) {
2778
+ throw new Error(tr(`\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u0440\u0435\u0430\u043A\u0446\u0438\u044F ${emoji}: ${REACTIONS.join(", ")}`, `unknown reaction ${emoji}: ${REACTIONS.join(", ")}`));
2779
+ }
2780
+ const review = this.getReview(id);
2781
+ const note = review.threads.flatMap((thread) => thread.notes).find((item) => item.id === noteId);
2782
+ if (!note) throw new Error(tr(`\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u044F ${noteId} \u043D\u0435\u0442 \u0432 \u044D\u0442\u043E\u043C \u0440\u0435\u0432\u044C\u044E`, `comment ${noteId} is not in this review`));
2783
+ const provider = await this.providerFor(id);
2784
+ const result = await provider.react(noteId, emoji);
2785
+ let reactions = [];
2786
+ try {
2787
+ reactions = await provider.reactions(noteId);
2788
+ } catch {
2789
+ }
2790
+ this.store.audit(id, "reaction.toggle", { noteId, emoji, active: result.active });
2791
+ return { active: result.active, reactions };
2792
+ }
2793
+ async uploadAttachment(id, input) {
2794
+ const stored = this.store.getReview(id);
2795
+ if (!stored) throw new Error(tr(`\u0440\u0435\u0432\u044C\u044E ${id} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E`, `review ${id} not found`));
2796
+ const spec = attachmentSpec(String(input.name ?? ""), String(input.contentType ?? ""));
2797
+ if (!spec) {
2798
+ throw new Error(
2799
+ tr(
2800
+ "\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435: \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u044B PNG, JPEG, GIF, WebP, AVIF, MP4, WebM, OGV \u0438 MOV",
2801
+ "attachment: PNG, JPEG, GIF, WebP, AVIF, MP4, WebM, OGV and MOV are allowed"
2802
+ )
2803
+ );
2804
+ }
2805
+ const provider = await this.providerFor(id);
2806
+ if (!provider.capabilities.attachments) {
2807
+ throw new Error(
2808
+ tr(
2809
+ "\u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0430 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0443 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0438\u0437 revdeck: \u0432\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u0430\u0431\u0441\u043E\u043B\u044E\u0442\u043D\u0443\u044E \u0441\u0441\u044B\u043B\u043A\u0443 \u0432\u0440\u0443\u0447\u043D\u0443\u044E",
2810
+ "the platform does not support attachments from revdeck: paste an absolute URL manually"
2811
+ )
2812
+ );
2813
+ }
2814
+ const buffer = decodeAttachment(input.data);
2815
+ if (!matchesSignature(buffer, spec.contentType)) {
2816
+ throw new Error(tr("\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435: \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u0435\u0442 \u0441 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0435\u043C", "attachment: content does not match the extension"));
2817
+ }
2818
+ const dir = await mkdtemp2(path6.join(tmpdir4(), "revdeck-attachment-"));
2819
+ const file = path6.join(dir, spec.name);
2820
+ try {
2821
+ await writeFile2(file, buffer, { mode: 384, flag: "wx" });
2822
+ const uploaded = await provider.upload({ path: file, name: spec.name, contentType: spec.contentType });
2823
+ this.store.audit(id, "attachment.upload", { name: spec.name, contentType: spec.contentType, bytes: buffer.length, url: uploaded.url });
2824
+ return uploaded;
2825
+ } finally {
2826
+ await rm2(dir, { recursive: true, force: true });
2827
+ }
2828
+ }
2584
2829
  // ---------------------------------------------------------------- notes
2585
2830
  addNote(id, input) {
2586
2831
  const review = this.getReview(id);
@@ -2653,25 +2898,27 @@ ${errors.join("\n")}`));
2653
2898
  warnings.push(tr(`\u0442\u0440\u0435\u0434 ${itemId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D`, `thread ${itemId} not found`));
2654
2899
  continue;
2655
2900
  }
2656
- const body = draft.body.trim();
2657
- const alreadyPosted = body !== "" && draft.posted.trim() === body;
2901
+ const rawBody = draft.body.trim();
2902
+ const body2 = normalizeMediaLinks(rawBody, review.info.ref.platform, review.info.ref.host);
2903
+ const posted = normalizeMediaLinks(draft.posted.trim(), review.info.ref.platform, review.info.ref.host);
2904
+ const alreadyPosted = rawBody !== "" && posted === body2;
2658
2905
  const resolveChanged = draft.resolve !== thread.resolved;
2659
- if (!body && !resolveChanged) {
2906
+ if (!body2 && !resolveChanged) {
2660
2907
  warnings.push(tr(`\u0442\u0440\u0435\u0434 ${thread.id.substring(0, 8)}: \u043D\u0435\u0447\u0435\u0433\u043E \u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u0442\u044C`, `thread ${thread.id.substring(0, 8)}: nothing to publish`));
2661
2908
  continue;
2662
2909
  }
2663
- if (body && alreadyPosted && !resolveChanged) {
2910
+ if (body2 && alreadyPosted && !resolveChanged) {
2664
2911
  warnings.push(tr(`\u0442\u0440\u0435\u0434 ${thread.id.substring(0, 8)}: \u043E\u0442\u0432\u0435\u0442 \u0443\u0436\u0435 \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D`, `thread ${thread.id.substring(0, 8)}: reply already published`));
2665
2912
  continue;
2666
2913
  }
2667
- if (body) {
2668
- const check2 = sanitizeBody(body, review.info.ref.platform);
2914
+ if (body2) {
2915
+ const check2 = sanitizeBody(body2, review.info.ref.platform, { host: review.info.ref.host });
2669
2916
  if (!check2.ok) {
2670
2917
  warnings.push(tr(`\u0442\u0440\u0435\u0434 ${thread.id.substring(0, 8)}: ${check2.reason}`, `thread ${thread.id.substring(0, 8)}: ${check2.reason}`));
2671
2918
  continue;
2672
2919
  }
2673
2920
  }
2674
- items.push({ kind, id: thread.id, body, resolve: draft.resolve });
2921
+ items.push({ kind, id: thread.id, body: body2, resolve: draft.resolve });
2675
2922
  continue;
2676
2923
  }
2677
2924
  if (kind === "finding") {
@@ -2684,13 +2931,13 @@ ${errors.join("\n")}`));
2684
2931
  warnings.push(tr(`\u0437\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u0435 ${finding.id} \u0443\u0436\u0435 \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043E`, `finding ${finding.id} is already published`));
2685
2932
  continue;
2686
2933
  }
2687
- const body = finding.comment.trim();
2688
- const check2 = sanitizeBody(body, review.info.ref.platform);
2934
+ const body2 = normalizeMediaLinks(finding.comment.trim(), review.info.ref.platform, review.info.ref.host);
2935
+ const check2 = sanitizeBody(body2, review.info.ref.platform, { host: review.info.ref.host });
2689
2936
  if (!check2.ok) {
2690
2937
  warnings.push(tr(`\u0437\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u0435 ${finding.id}: ${check2.reason}`, `finding ${finding.id}: ${check2.reason}`));
2691
2938
  continue;
2692
2939
  }
2693
- const finalBody = finding.file && finding.line === null ? `\`${finding.file}\`: ${body}` : body;
2940
+ const finalBody = finding.file && finding.line === null ? `\`${finding.file}\`: ${body2}` : body2;
2694
2941
  items.push({
2695
2942
  kind,
2696
2943
  id: finding.id,
@@ -2710,12 +2957,13 @@ ${errors.join("\n")}`));
2710
2957
  warnings.push(tr(`\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 ${note.id} \u0443\u0436\u0435 \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D`, `comment ${note.id} is already published`));
2711
2958
  continue;
2712
2959
  }
2713
- const check = sanitizeBody(note.text, review.info.ref.platform);
2960
+ const body = normalizeMediaLinks(note.text, review.info.ref.platform, review.info.ref.host);
2961
+ const check = sanitizeBody(body, review.info.ref.platform, { host: review.info.ref.host });
2714
2962
  if (!check.ok) {
2715
2963
  warnings.push(tr(`\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 ${note.id}: ${check.reason}`, `comment ${note.id}: ${check.reason}`));
2716
2964
  continue;
2717
2965
  }
2718
- items.push({ kind, id: note.id, body: note.text, file: note.file, line: note.line, side: note.side });
2966
+ items.push({ kind, id: note.id, body, file: note.file, line: note.line, side: note.side });
2719
2967
  }
2720
2968
  return { plan: this.makePlan(id, kind, items), warnings };
2721
2969
  }
@@ -2778,7 +3026,7 @@ ${errors.join("\n")}`));
2778
3026
  if (!thread) throw new Error(tr("\u0442\u0440\u0435\u0434 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442", "the thread no longer exists"));
2779
3027
  const body = String(item.body ?? "").trim();
2780
3028
  if (body && draft.posted.trim() !== body) {
2781
- const check = sanitizeBody(body, platform);
3029
+ const check = sanitizeBody(body, platform, { host: review.info.ref.host });
2782
3030
  if (!check.ok) throw new Error(check.reason);
2783
3031
  await provider.reply(thread, body);
2784
3032
  draft.posted = body;
@@ -3045,7 +3293,7 @@ init_errors();
3045
3293
  import http from "node:http";
3046
3294
  import { createReadStream } from "node:fs";
3047
3295
  import { readFile as readFile2, stat } from "node:fs/promises";
3048
- import path6 from "node:path";
3296
+ import path7 from "node:path";
3049
3297
  import { timingSafeEqual } from "node:crypto";
3050
3298
 
3051
3299
  // src/daemon/rpc.ts
@@ -3053,7 +3301,7 @@ import net from "node:net";
3053
3301
  import { chmod, unlink as unlink2 } from "node:fs/promises";
3054
3302
  init_redact();
3055
3303
  init_errors();
3056
- var MAX_LINE = 4 * 1024 * 1024;
3304
+ var MAX_LINE = 40 * 1024 * 1024;
3057
3305
  async function dispatch(service, method, params) {
3058
3306
  switch (method) {
3059
3307
  case "ping":
@@ -3091,6 +3339,23 @@ async function dispatch(service, method, params) {
3091
3339
  const file = service.diffFile(String(params.review ?? ""), String(params.file ?? ""));
3092
3340
  return { file };
3093
3341
  }
3342
+ case "reactions.list":
3343
+ return {
3344
+ reactions: await service.reactions(
3345
+ String(params.review ?? ""),
3346
+ Array.isArray(params.noteIds) ? params.noteIds.map(String) : []
3347
+ )
3348
+ };
3349
+ case "reactions.toggle":
3350
+ return service.react(String(params.review ?? ""), String(params.noteId ?? ""), String(params.emoji ?? ""));
3351
+ case "attachments.upload":
3352
+ return {
3353
+ attachment: await service.uploadAttachment(String(params.review ?? ""), {
3354
+ name: String(params.name ?? ""),
3355
+ contentType: String(params.contentType ?? ""),
3356
+ data: String(params.data ?? "")
3357
+ })
3358
+ };
3094
3359
  case "drafts.put":
3095
3360
  return { draft: service.setDraft(String(params.review ?? ""), String(params.threadId ?? ""), params.patch ?? {}) };
3096
3361
  case "findings.put":
@@ -3223,7 +3488,7 @@ async function respond(service, request) {
3223
3488
  }
3224
3489
 
3225
3490
  // src/daemon/http.ts
3226
- var MAX_BODY = 4 * 1024 * 1024;
3491
+ var MAX_BODY = 40 * 1024 * 1024;
3227
3492
  var MIME = {
3228
3493
  ".html": "text/html; charset=utf-8",
3229
3494
  ".js": "text/javascript; charset=utf-8",
@@ -3317,15 +3582,15 @@ async function startHttpServer(options) {
3317
3582
  res.end('<meta charset="utf-8"><h1>\u041D\u0443\u0436\u043D\u0430 \u0441\u0441\u044B\u043B\u043A\u0430 \u0437\u0430\u043F\u0443\u0441\u043A\u0430</h1><p>\u041E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0438\u0437 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0430: \u043E\u043D \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 \u043E\u0434\u043D\u043E\u0440\u0430\u0437\u043E\u0432\u044B\u0439 \u0442\u043E\u043A\u0435\u043D.</p>');
3318
3583
  return;
3319
3584
  }
3320
- const html = await readFile2(path6.join(options.uiDir, "index.html"));
3585
+ const html = await readFile2(path7.join(options.uiDir, "index.html"));
3321
3586
  res.writeHead(200, { ...SECURITY_HEADERS, "content-type": MIME[".html"], "cache-control": "no-store" });
3322
3587
  res.end(html);
3323
3588
  return;
3324
3589
  }
3325
3590
  if (req.method === "GET" && (url.pathname.startsWith("/assets/") || url.pathname === "/favicon.svg")) {
3326
3591
  const relative = url.pathname === "/favicon.svg" ? "favicon.svg" : url.pathname.slice("/assets/".length);
3327
- const target = path6.resolve(options.uiDir, url.pathname === "/favicon.svg" ? "" : "assets", relative);
3328
- if (!target.startsWith(path6.resolve(options.uiDir) + path6.sep) || relative.includes("..")) {
3592
+ const target = path7.resolve(options.uiDir, url.pathname === "/favicon.svg" ? "" : "assets", relative);
3593
+ if (!target.startsWith(path7.resolve(options.uiDir) + path7.sep) || relative.includes("..")) {
3329
3594
  reply(res, 404, { ok: false, error: "\u043D\u0435\u0442 \u0444\u0430\u0439\u043B\u0430" });
3330
3595
  return;
3331
3596
  }
@@ -3336,7 +3601,7 @@ async function startHttpServer(options) {
3336
3601
  }
3337
3602
  res.writeHead(200, {
3338
3603
  ...SECURITY_HEADERS,
3339
- "content-type": MIME[path6.extname(target)] ?? "application/octet-stream",
3604
+ "content-type": MIME[path7.extname(target)] ?? "application/octet-stream",
3340
3605
  "cache-control": "public, max-age=31536000, immutable"
3341
3606
  });
3342
3607
  createReadStream(target).pipe(res);
@@ -3414,6 +3679,12 @@ data: ${JSON.stringify(event)}
3414
3679
  reply(res, 200, { ok: true, rules: await service.rules(review) });
3415
3680
  return;
3416
3681
  }
3682
+ if (req.method === "GET" && url.pathname === "/api/reactions") {
3683
+ const review = url.searchParams.get("review") ?? "";
3684
+ const noteIds = (url.searchParams.get("notes") ?? "").split(",").map((item) => item.trim()).filter(Boolean);
3685
+ reply(res, 200, { ok: true, reactions: await service.reactions(review, noteIds) });
3686
+ return;
3687
+ }
3417
3688
  if (req.method === "POST") {
3418
3689
  const body = await readBody(req);
3419
3690
  const review = String(body.review ?? "");
@@ -3436,6 +3707,20 @@ data: ${JSON.stringify(event)}
3436
3707
  reply(res, 200, { ok: true, review: service.getView(updated.id), reviews: service.listReviews() });
3437
3708
  return;
3438
3709
  }
3710
+ case "/api/attachments": {
3711
+ const attachment = await service.uploadAttachment(review, {
3712
+ name: String(body.name ?? ""),
3713
+ contentType: String(body.type ?? ""),
3714
+ data: String(body.data ?? "")
3715
+ });
3716
+ reply(res, 200, { ok: true, attachment });
3717
+ return;
3718
+ }
3719
+ case "/api/reactions": {
3720
+ const result = await service.react(review, String(body.noteId ?? ""), String(body.emoji ?? ""));
3721
+ reply(res, 200, { ok: true, ...result });
3722
+ return;
3723
+ }
3439
3724
  case "/api/drafts": {
3440
3725
  const draft = service.setDraft(review, String(body.threadId ?? ""), body.patch ?? {});
3441
3726
  reply(res, 200, { ok: true, draft });
@@ -3540,6 +3825,7 @@ async function main() {
3540
3825
  const { values } = parseArgs({
3541
3826
  options: {
3542
3827
  port: { type: "string" },
3828
+ host: { type: "string" },
3543
3829
  "state-dir": { type: "string" },
3544
3830
  "runtime-dir": { type: "string" },
3545
3831
  "ui-dir": { type: "string" },
@@ -3582,12 +3868,13 @@ async function main() {
3582
3868
  });
3583
3869
  const socket = socketPath();
3584
3870
  const rpc = await startRpcServer(service, socket);
3585
- const uiDir = values["ui-dir"] ? path7.resolve(values["ui-dir"]) : path7.join(packageRoot(), "dist", "ui");
3871
+ const uiDir = values["ui-dir"] ? path8.resolve(values["ui-dir"]) : path8.join(packageRoot(), "dist", "ui");
3586
3872
  const http2 = await startHttpServer({
3587
3873
  service,
3588
3874
  token,
3589
3875
  uiDir,
3590
3876
  ...values.port ? { port: Number(values.port) } : {},
3877
+ ...values.host ? { host: String(values.host) } : {},
3591
3878
  version: VERSION
3592
3879
  });
3593
3880
  const info = {
@@ -3625,7 +3912,7 @@ async function main() {
3625
3912
  service.close();
3626
3913
  await unlink3(daemonInfoPath()).catch(() => void 0);
3627
3914
  await lock.close().catch(() => void 0);
3628
- await unlink3(path7.join(stateDir(), "daemon.lock")).catch(() => void 0);
3915
+ await unlink3(path8.join(stateDir(), "daemon.lock")).catch(() => void 0);
3629
3916
  process.exit(0);
3630
3917
  }
3631
3918
  };
@@ -3637,9 +3924,9 @@ async function main() {
3637
3924
  }
3638
3925
  }
3639
3926
  function packageRoot() {
3640
- let dir = path7.dirname(fileURLToPath(import.meta.url));
3927
+ let dir = path8.dirname(fileURLToPath(import.meta.url));
3641
3928
  for (; ; ) {
3642
- const candidate = path7.join(dir, "package.json");
3929
+ const candidate = path8.join(dir, "package.json");
3643
3930
  if (existsSync4(candidate)) {
3644
3931
  try {
3645
3932
  const parsed = JSON.parse(readFileSync(candidate, "utf8"));
@@ -3647,7 +3934,7 @@ function packageRoot() {
3647
3934
  } catch {
3648
3935
  }
3649
3936
  }
3650
- const parent = path7.dirname(dir);
3937
+ const parent = path8.dirname(dir);
3651
3938
  if (parent === dir) return process.cwd();
3652
3939
  dir = parent;
3653
3940
  }
@@ -3686,7 +3973,7 @@ async function runningDaemon() {
3686
3973
  return reachable ? info : null;
3687
3974
  }
3688
3975
  async function acquireLock() {
3689
- const file = path7.join(stateDir(), "daemon.lock");
3976
+ const file = path8.join(stateDir(), "daemon.lock");
3690
3977
  for (let attempt = 0; attempt < 2; attempt++) {
3691
3978
  try {
3692
3979
  const handle = await open2(file, "wx", 384);