@jpkme/diff-review 0.1.3 → 0.1.4

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 (95) hide show
  1. package/README.md +22 -6
  2. package/bin/diff-review.mjs +187 -19
  3. package/lib/clipboard.mjs +29 -0
  4. package/lib/config.mjs +233 -0
  5. package/lib/config.test.mjs +73 -0
  6. package/lib/extract.mjs +111 -0
  7. package/lib/git.mjs +202 -0
  8. package/lib/git.test.mjs +29 -0
  9. package/lib/json-payload.mjs +31 -0
  10. package/lib/json-payload.test.mjs +22 -0
  11. package/lib/output-name.mjs +56 -0
  12. package/lib/output-name.test.mjs +54 -0
  13. package/lib/pick-folder.mjs +4 -0
  14. package/lib/prompt.mjs +154 -0
  15. package/lib/remote-repo.mjs +182 -0
  16. package/lib/review.mjs +228 -0
  17. package/lib/review.test.mjs +160 -0
  18. package/lib/validate.mjs +221 -0
  19. package/lib/validate.test.mjs +85 -0
  20. package/lib/wait-output.mjs +104 -0
  21. package/package.json +2 -1
  22. package/web/build/client/_app/immutable/assets/2.Xnfq26gJ.css +1 -0
  23. package/web/build/client/_app/immutable/assets/2.Xnfq26gJ.css.br +0 -0
  24. package/web/build/client/_app/immutable/assets/2.Xnfq26gJ.css.gz +0 -0
  25. package/web/build/client/_app/immutable/chunks/{C5moC2DW.js → BaYDuTXu.js} +36 -29
  26. package/web/build/client/_app/immutable/chunks/BaYDuTXu.js.br +0 -0
  27. package/web/build/client/_app/immutable/chunks/BaYDuTXu.js.gz +0 -0
  28. package/web/build/client/_app/immutable/chunks/CAnYe5db.js +1 -0
  29. package/web/build/client/_app/immutable/chunks/CAnYe5db.js.br +1 -0
  30. package/web/build/client/_app/immutable/chunks/CAnYe5db.js.gz +0 -0
  31. package/web/build/client/_app/immutable/chunks/Gya4IU1I.js +2 -0
  32. package/web/build/client/_app/immutable/chunks/Gya4IU1I.js.br +0 -0
  33. package/web/build/client/_app/immutable/chunks/Gya4IU1I.js.gz +0 -0
  34. package/web/build/client/_app/immutable/entry/{app.Dei8qxo7.js → app.e96uPVtl.js} +2 -2
  35. package/web/build/client/_app/immutable/entry/app.e96uPVtl.js.br +0 -0
  36. package/web/build/client/_app/immutable/entry/app.e96uPVtl.js.gz +0 -0
  37. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js +1 -0
  38. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js.br +0 -0
  39. package/web/build/client/_app/immutable/entry/start.D-D-7QPy.js.gz +0 -0
  40. package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js +1 -0
  41. package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js.br +0 -0
  42. package/web/build/client/_app/immutable/nodes/1.V-g5-sfL.js.gz +0 -0
  43. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js +66 -0
  44. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js.br +0 -0
  45. package/web/build/client/_app/immutable/nodes/2.lcRePHZ5.js.gz +0 -0
  46. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js +170 -0
  47. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js.br +0 -0
  48. package/web/build/client/_app/immutable/nodes/3.GZPWeD9I.js.gz +0 -0
  49. package/web/build/client/_app/version.json +1 -1
  50. package/web/build/client/_app/version.json.br +0 -0
  51. package/web/build/client/_app/version.json.gz +0 -0
  52. package/web/build/handler.js +3 -3
  53. package/web/build/index.js +3 -3
  54. package/web/build/server/chunks/1-DCKScL3A.js +7 -0
  55. package/web/build/server/chunks/1-DCKScL3A.js.map +1 -0
  56. package/web/build/server/chunks/2-BpBQQ02B.js +7 -0
  57. package/web/build/server/chunks/2-BpBQQ02B.js.map +1 -0
  58. package/web/build/server/chunks/{3-Zc5_qwgV.js → 3-rGkcNCVz.js} +2 -2
  59. package/web/build/server/chunks/{3-Zc5_qwgV.js.map → 3-rGkcNCVz.js.map} +1 -1
  60. package/web/build/server/chunks/{handler-BmuqxCCf.js → handler-BaT-QavL.js} +3 -3
  61. package/web/build/server/chunks/{handler-BmuqxCCf.js.map → handler-BaT-QavL.js.map} +1 -1
  62. package/web/build/server/chunks/{index.js-41EcOndB.js → index.js-C-K0YDBJ.js} +150 -150
  63. package/web/build/server/chunks/index.js-C-K0YDBJ.js.map +1 -0
  64. package/web/build/server/chunks/{manifest.js-KnmnjP9i.js → manifest.js-CDz-Fem4.js} +6 -6
  65. package/web/build/server/chunks/{manifest.js-KnmnjP9i.js.map → manifest.js-CDz-Fem4.js.map} +1 -1
  66. package/web/build/server/chunks/{remote-1g4ayag-BgUNMUS8.js → remote-1g4ayag-DXdFe1BG.js} +645 -146
  67. package/web/build/server/chunks/remote-1g4ayag-DXdFe1BG.js.map +1 -0
  68. package/web/build/client/_app/immutable/assets/2.Du8E-7-D.css +0 -1
  69. package/web/build/client/_app/immutable/assets/2.Du8E-7-D.css.br +0 -0
  70. package/web/build/client/_app/immutable/assets/2.Du8E-7-D.css.gz +0 -0
  71. package/web/build/client/_app/immutable/chunks/C5moC2DW.js.br +0 -0
  72. package/web/build/client/_app/immutable/chunks/C5moC2DW.js.gz +0 -0
  73. package/web/build/client/_app/immutable/chunks/WxPrACSs.js +0 -2
  74. package/web/build/client/_app/immutable/chunks/WxPrACSs.js.br +0 -0
  75. package/web/build/client/_app/immutable/chunks/WxPrACSs.js.gz +0 -0
  76. package/web/build/client/_app/immutable/entry/app.Dei8qxo7.js.br +0 -0
  77. package/web/build/client/_app/immutable/entry/app.Dei8qxo7.js.gz +0 -0
  78. package/web/build/client/_app/immutable/entry/start.Fzxrn2l4.js +0 -1
  79. package/web/build/client/_app/immutable/entry/start.Fzxrn2l4.js.br +0 -1
  80. package/web/build/client/_app/immutable/entry/start.Fzxrn2l4.js.gz +0 -0
  81. package/web/build/client/_app/immutable/nodes/1.Cn_iktkl.js +0 -1
  82. package/web/build/client/_app/immutable/nodes/1.Cn_iktkl.js.br +0 -0
  83. package/web/build/client/_app/immutable/nodes/1.Cn_iktkl.js.gz +0 -0
  84. package/web/build/client/_app/immutable/nodes/2.vvYFHTy-.js +0 -66
  85. package/web/build/client/_app/immutable/nodes/2.vvYFHTy-.js.br +0 -0
  86. package/web/build/client/_app/immutable/nodes/2.vvYFHTy-.js.gz +0 -0
  87. package/web/build/client/_app/immutable/nodes/3.CeJ7dEsh.js +0 -170
  88. package/web/build/client/_app/immutable/nodes/3.CeJ7dEsh.js.br +0 -0
  89. package/web/build/client/_app/immutable/nodes/3.CeJ7dEsh.js.gz +0 -0
  90. package/web/build/server/chunks/1-Bjgeu1Iz.js +0 -7
  91. package/web/build/server/chunks/1-Bjgeu1Iz.js.map +0 -1
  92. package/web/build/server/chunks/2-BvMZU9fP.js +0 -7
  93. package/web/build/server/chunks/2-BvMZU9fP.js.map +0 -1
  94. package/web/build/server/chunks/index.js-41EcOndB.js.map +0 -1
  95. package/web/build/server/chunks/remote-1g4ayag-BgUNMUS8.js.map +0 -1
@@ -1,7 +1,7 @@
1
- import { e as error, g as get_request_store, b as stringify_remote_arg, M as MUTATIVE_METHODS, w as with_request_store, c as create_remote_key, n as noop, H as HttpError, S as SvelteKitError, h as handle_error_and_jsonify, _ as __exportAll, i as init_remote_functions } from './index.js-41EcOndB.js';
2
- import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
3
- import { join, resolve } from 'node:path';
1
+ import { e as error, g as get_request_store, b as stringify_remote_arg, M as MUTATIVE_METHODS, w as with_request_store, c as create_remote_key, n as noop, H as HttpError, S as SvelteKitError, h as handle_error_and_jsonify, _ as __exportAll, i as init_remote_functions } from './index.js-C-K0YDBJ.js';
4
2
  import { execFileSync, spawnSync } from 'node:child_process';
3
+ import { existsSync, readFileSync, statSync, readdirSync, mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
4
+ import { join, basename, resolve } from 'node:path';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { tmpdir, platform } from 'node:os';
7
7
  import process$1 from 'node:process';
@@ -957,6 +957,230 @@ Object.defineProperty(query, "live", {
957
957
  enumerable: true
958
958
  });
959
959
 
960
+ //#region ../lib/git.mjs
961
+ function assertGitRepo(repo) {
962
+ const raw = String(repo || "").trim();
963
+ if (!raw) throw new Error("Indicá la ruta absoluta de un repositorio git.");
964
+ if (!/[\\/]/.test(raw) && !/^[A-Za-z]:/.test(raw)) throw new Error(`"${raw}" no es una ruta. Pegá la ruta absoluta del repo (ej. C:\\tienda-argenta\\tienda-argenta-2).`);
965
+ const resolved = resolve(raw);
966
+ if (!resolved || resolved === ".") throw new Error("Indicá la ruta absoluta de un repositorio git.");
967
+ if (!existsSync(resolved)) throw new Error(`No existe la carpeta: ${resolved}`);
968
+ if (!existsSync(join(resolved, ".git"))) throw new Error(`No es un repositorio git (falta .git): ${resolved}`);
969
+ return resolved;
970
+ }
971
+ /**
972
+ * Si `cwd` está dentro de un repo git, devuelve la raíz absoluta; si no, null.
973
+ * Acepta subcarpetas (usa `git rev-parse --show-toplevel`).
974
+ */
975
+ function tryResolveGitRoot(cwd) {
976
+ const raw = String(cwd || "").trim();
977
+ if (!raw) return null;
978
+ const dir = resolve(raw);
979
+ if (!existsSync(dir)) return null;
980
+ try {
981
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
982
+ cwd: dir,
983
+ encoding: "utf8",
984
+ windowsHide: true,
985
+ stdio: [
986
+ "ignore",
987
+ "pipe",
988
+ "ignore"
989
+ ]
990
+ }).trim();
991
+ return root ? resolve(root) : null;
992
+ } catch {
993
+ return null;
994
+ }
995
+ }
996
+ function git(repo, args, { maxBuffer = 64 * 1024 * 1024 } = {}) {
997
+ return execFileSync("git", args, {
998
+ cwd: repo,
999
+ encoding: "utf8",
1000
+ maxBuffer,
1001
+ windowsHide: true
1002
+ });
1003
+ }
1004
+ function resolveRev(repo, ref) {
1005
+ return git(repo, ["rev-parse", ref]).trim();
1006
+ }
1007
+ function branchTips(repo, branch, base) {
1008
+ return {
1009
+ branchSha: resolveRev(repo, branch),
1010
+ baseSha: resolveRev(repo, base)
1011
+ };
1012
+ }
1013
+ function fingerprint(branchSha, baseSha) {
1014
+ const a = shortSha(branchSha);
1015
+ const b = shortSha(baseSha);
1016
+ if (!a || !b) return null;
1017
+ return `${a}-${b}`;
1018
+ }
1019
+ function shortSha(sha, n = 10) {
1020
+ const hex = String(sha || "").toLowerCase().replace(/[^a-f0-9]/g, "");
1021
+ return hex.length >= 7 ? hex.slice(0, n) : "";
1022
+ }
1023
+ function commitLog(repo, mergeBase, branch, limit = 25) {
1024
+ try {
1025
+ return git(repo, [
1026
+ "log",
1027
+ "--oneline",
1028
+ "--no-merges",
1029
+ "-n",
1030
+ String(limit),
1031
+ `${mergeBase}..${branch}`
1032
+ ]).trim().split("\n").filter(Boolean);
1033
+ } catch {
1034
+ return [];
1035
+ }
1036
+ }
1037
+ var FILE_LINE_CACHE = /* @__PURE__ */ new Map();
1038
+ var FILE_LINE_CACHE_MAX = 48;
1039
+ function rememberFileLines(key, lines) {
1040
+ if (FILE_LINE_CACHE.has(key)) FILE_LINE_CACHE.delete(key);
1041
+ FILE_LINE_CACHE.set(key, lines);
1042
+ while (FILE_LINE_CACHE.size > FILE_LINE_CACHE_MAX) {
1043
+ const oldest = FILE_LINE_CACHE.keys().next().value;
1044
+ FILE_LINE_CACHE.delete(oldest);
1045
+ }
1046
+ }
1047
+ function showFileLines(repo, rev, path) {
1048
+ const resolved = assertGitRepo(repo);
1049
+ const safeRev = String(rev || "").trim();
1050
+ const safePath = String(path || "").replace(/\\/g, "/").trim();
1051
+ if (!safeRev || !/^[A-Za-z0-9._/\-]+$/.test(safeRev)) throw new Error("Rev inválido.");
1052
+ if (!safePath || safePath.startsWith("/") || safePath.includes("..") || safePath.includes("\0")) throw new Error("Ruta inválida.");
1053
+ const key = `${resolved}\0${safeRev}\0${safePath}`;
1054
+ if (FILE_LINE_CACHE.has(key)) return FILE_LINE_CACHE.get(key);
1055
+ let text;
1056
+ try {
1057
+ text = git(resolved, ["show", `${safeRev}:${safePath}`]);
1058
+ } catch {
1059
+ rememberFileLines(key, null);
1060
+ return null;
1061
+ }
1062
+ if (text.includes("\0")) {
1063
+ rememberFileLines(key, null);
1064
+ return null;
1065
+ }
1066
+ const raw = text.replace(/\r\n/g, "\n");
1067
+ const lines = raw.endsWith("\n") ? raw.slice(0, -1).split("\n") : raw.split("\n");
1068
+ rememberFileLines(key, lines);
1069
+ return lines;
1070
+ }
1071
+ function listBranches(repo) {
1072
+ const names = git(repo, [
1073
+ "for-each-ref",
1074
+ "--sort=-committerdate",
1075
+ "--format=%(refname:short)",
1076
+ "refs/heads",
1077
+ "refs/remotes"
1078
+ ]).split("\n").map((s) => s.trim()).filter(Boolean).map((name) => name.replace(/^origin\//, "")).filter((name) => name !== "HEAD" && !name.endsWith("/HEAD"));
1079
+ const unique = [...new Set(names)];
1080
+ const current = git(repo, [
1081
+ "rev-parse",
1082
+ "--abbrev-ref",
1083
+ "HEAD"
1084
+ ]).trim();
1085
+ const bases = unique.filter((n) => n === "develop" || n === "main" || n === "master");
1086
+ return {
1087
+ branches: unique,
1088
+ current,
1089
+ defaultBase: bases.includes("develop") ? "develop" : bases.includes("main") ? "main" : bases[0] || "develop"
1090
+ };
1091
+ }
1092
+ /** URL del remote (origin, o el primero disponible). Vacío si no hay. */
1093
+ function getRemoteUrl(repo, preferred = "origin") {
1094
+ try {
1095
+ const url = git(repo, [
1096
+ "remote",
1097
+ "get-url",
1098
+ preferred
1099
+ ]).trim();
1100
+ if (url) return url;
1101
+ } catch {}
1102
+ try {
1103
+ const remotes = git(repo, ["remote"]).split("\n").map((s) => s.trim()).filter(Boolean);
1104
+ if (!remotes.length) return "";
1105
+ return git(repo, [
1106
+ "remote",
1107
+ "get-url",
1108
+ remotes[0]
1109
+ ]).trim();
1110
+ } catch {
1111
+ return "";
1112
+ }
1113
+ }
1114
+ //#endregion
1115
+ //#region ../lib/output-name.mjs
1116
+ /**
1117
+ * Nombre del JSON que escribe el agente.
1118
+ * Preferido: tips cortos + stamp. Legacy sin tips sigue siendo válido al importar.
1119
+ */
1120
+ /** Prefijo del formato actual (con tips). */
1121
+ var OUTPUT_PREFIX = "diff-report";
1122
+ /** Prefijo legacy (sin tips). */
1123
+ var OUTPUT_PREFIX_LEGACY = "diff-review-output";
1124
+ /**
1125
+ * Válidos:
1126
+ * - diff-review-output.json
1127
+ * - diff-review-output-YYYYMMDD-HHmmss.json
1128
+ * - diff-report_{branchTip}-{baseTip}_YYYYMMDD-HHmmss.json
1129
+ */
1130
+ var OUTPUT_FILENAME_RE = /^(?:diff-review-output(?:-\d{8}-\d{6})?|diff-report_[0-9a-f]{7,12}-[0-9a-f]{7,12}_\d{8}-\d{6})\.json$/i;
1131
+ /**
1132
+ * @param {Date} [when]
1133
+ * @returns {string} ej. 20260915-143045
1134
+ */
1135
+ function formatOutputStamp(when = /* @__PURE__ */ new Date()) {
1136
+ const pad = (n) => String(n).padStart(2, "0");
1137
+ return `${when.getFullYear()}${pad(when.getMonth() + 1)}${pad(when.getDate())}-${pad(when.getHours())}${pad(when.getMinutes())}${pad(when.getSeconds())}`;
1138
+ }
1139
+ /**
1140
+ * @param {{ branchSha?: string, baseSha?: string, stamp?: string, when?: Date } | Date} [input]
1141
+ * @returns {string}
1142
+ * con tips: diff-report_aaaaaaaaaa-bbbbbbbbbb_20260915-143045.json
1143
+ * sin tips: diff-review-output-20260915-143045.json
1144
+ */
1145
+ function makeOutputFilename(input = {}) {
1146
+ const opts = input instanceof Date ? { when: input } : input || {};
1147
+ const stamp = opts.stamp || formatOutputStamp(opts.when ?? /* @__PURE__ */ new Date());
1148
+ const fp = fingerprint(opts.branchSha, opts.baseSha);
1149
+ if (fp) return `${OUTPUT_PREFIX}_${fp}_${stamp}.json`;
1150
+ return `${OUTPUT_PREFIX_LEGACY}-${stamp}.json`;
1151
+ }
1152
+ function isOutputFilename(name) {
1153
+ return OUTPUT_FILENAME_RE.test(String(name || "").trim());
1154
+ }
1155
+ //#endregion
1156
+ //#region ../lib/json-payload.mjs
1157
+ function tryParseJson(s) {
1158
+ try {
1159
+ return JSON.parse(s);
1160
+ } catch {
1161
+ return null;
1162
+ }
1163
+ }
1164
+ /** Extrae un objeto JSON del texto del modelo (fences, prosa alrededor, envelope). */
1165
+ function decodePayload(text) {
1166
+ const raw = String(text || "").trim();
1167
+ if (!raw) throw new Error("Respuesta vacía.");
1168
+ const fence = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
1169
+ if (fence) {
1170
+ const parsed = tryParseJson(fence[1].trim());
1171
+ if (parsed && typeof parsed === "object") return parsed;
1172
+ }
1173
+ const direct = tryParseJson(raw);
1174
+ if (direct && typeof direct === "object") return direct;
1175
+ const start = raw.indexOf("{");
1176
+ const end = raw.lastIndexOf("}");
1177
+ if (start >= 0 && end > start) {
1178
+ const parsed = tryParseJson(raw.slice(start, end + 1));
1179
+ if (parsed && typeof parsed === "object") return parsed;
1180
+ }
1181
+ throw new Error("No se pudo interpretar la respuesta como JSON.");
1182
+ }
1183
+ //#endregion
960
1184
  //#region ../lib/config.mjs
961
1185
  var DEFAULT_IGNORE = [
962
1186
  "**/package-lock.json",
@@ -1112,150 +1336,14 @@ function capDiff(diff, max = STORE_DIFF_MAX) {
1112
1336
  if (text.length <= max) return {
1113
1337
  diff: text,
1114
1338
  diffChars: text.length,
1115
- truncated: false
1116
- };
1117
- return {
1118
- diff: `${text.slice(0, max)}\n\n… [diff recortado: se guardan ${max} de ${text.length} caracteres]`,
1119
- diffChars: text.length,
1120
- truncated: true
1121
- };
1122
- }
1123
- //#endregion
1124
- //#region ../lib/git.mjs
1125
- function assertGitRepo(repo) {
1126
- const raw = String(repo || "").trim();
1127
- if (!raw) throw new Error("Indicá la ruta absoluta de un repositorio git.");
1128
- if (!/[\\/]/.test(raw) && !/^[A-Za-z]:/.test(raw)) throw new Error(`"${raw}" no es una ruta. Pegá la ruta absoluta del repo (ej. C:\\tienda-argenta\\tienda-argenta-2).`);
1129
- const resolved = resolve(raw);
1130
- if (!resolved || resolved === ".") throw new Error("Indicá la ruta absoluta de un repositorio git.");
1131
- if (!existsSync(resolved)) throw new Error(`No existe la carpeta: ${resolved}`);
1132
- if (!existsSync(join(resolved, ".git"))) throw new Error(`No es un repositorio git (falta .git): ${resolved}`);
1133
- return resolved;
1134
- }
1135
- /**
1136
- * Si `cwd` está dentro de un repo git, devuelve la raíz absoluta; si no, null.
1137
- * Acepta subcarpetas (usa `git rev-parse --show-toplevel`).
1138
- */
1139
- function tryResolveGitRoot(cwd) {
1140
- const raw = String(cwd || "").trim();
1141
- if (!raw) return null;
1142
- const dir = resolve(raw);
1143
- if (!existsSync(dir)) return null;
1144
- try {
1145
- const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
1146
- cwd: dir,
1147
- encoding: "utf8",
1148
- windowsHide: true,
1149
- stdio: [
1150
- "ignore",
1151
- "pipe",
1152
- "ignore"
1153
- ]
1154
- }).trim();
1155
- return root ? resolve(root) : null;
1156
- } catch {
1157
- return null;
1158
- }
1159
- }
1160
- function git(repo, args, { maxBuffer = 64 * 1024 * 1024 } = {}) {
1161
- return execFileSync("git", args, {
1162
- cwd: repo,
1163
- encoding: "utf8",
1164
- maxBuffer,
1165
- windowsHide: true
1166
- });
1167
- }
1168
- function commitLog(repo, mergeBase, branch, limit = 25) {
1169
- try {
1170
- return git(repo, [
1171
- "log",
1172
- "--oneline",
1173
- "--no-merges",
1174
- "-n",
1175
- String(limit),
1176
- `${mergeBase}..${branch}`
1177
- ]).trim().split("\n").filter(Boolean);
1178
- } catch {
1179
- return [];
1180
- }
1181
- }
1182
- var FILE_LINE_CACHE = /* @__PURE__ */ new Map();
1183
- var FILE_LINE_CACHE_MAX = 48;
1184
- function rememberFileLines(key, lines) {
1185
- if (FILE_LINE_CACHE.has(key)) FILE_LINE_CACHE.delete(key);
1186
- FILE_LINE_CACHE.set(key, lines);
1187
- while (FILE_LINE_CACHE.size > FILE_LINE_CACHE_MAX) {
1188
- const oldest = FILE_LINE_CACHE.keys().next().value;
1189
- FILE_LINE_CACHE.delete(oldest);
1190
- }
1191
- }
1192
- function showFileLines(repo, rev, path) {
1193
- const resolved = assertGitRepo(repo);
1194
- const safeRev = String(rev || "").trim();
1195
- const safePath = String(path || "").replace(/\\/g, "/").trim();
1196
- if (!safeRev || !/^[A-Za-z0-9._/\-]+$/.test(safeRev)) throw new Error("Rev inválido.");
1197
- if (!safePath || safePath.startsWith("/") || safePath.includes("..") || safePath.includes("\0")) throw new Error("Ruta inválida.");
1198
- const key = `${resolved}\0${safeRev}\0${safePath}`;
1199
- if (FILE_LINE_CACHE.has(key)) return FILE_LINE_CACHE.get(key);
1200
- let text;
1201
- try {
1202
- text = git(resolved, ["show", `${safeRev}:${safePath}`]);
1203
- } catch {
1204
- rememberFileLines(key, null);
1205
- return null;
1206
- }
1207
- if (text.includes("\0")) {
1208
- rememberFileLines(key, null);
1209
- return null;
1210
- }
1211
- const raw = text.replace(/\r\n/g, "\n");
1212
- const lines = raw.endsWith("\n") ? raw.slice(0, -1).split("\n") : raw.split("\n");
1213
- rememberFileLines(key, lines);
1214
- return lines;
1215
- }
1216
- function listBranches(repo) {
1217
- const names = git(repo, [
1218
- "for-each-ref",
1219
- "--sort=-committerdate",
1220
- "--format=%(refname:short)",
1221
- "refs/heads",
1222
- "refs/remotes"
1223
- ]).split("\n").map((s) => s.trim()).filter(Boolean).map((name) => name.replace(/^origin\//, "")).filter((name) => name !== "HEAD" && !name.endsWith("/HEAD"));
1224
- const unique = [...new Set(names)];
1225
- const current = git(repo, [
1226
- "rev-parse",
1227
- "--abbrev-ref",
1228
- "HEAD"
1229
- ]).trim();
1230
- const bases = unique.filter((n) => n === "develop" || n === "main" || n === "master");
1339
+ truncated: false
1340
+ };
1231
1341
  return {
1232
- branches: unique,
1233
- current,
1234
- defaultBase: bases.includes("develop") ? "develop" : bases.includes("main") ? "main" : bases[0] || "develop"
1342
+ diff: `${text.slice(0, max)}\n\n… [diff recortado: se guardan ${max} de ${text.length} caracteres]`,
1343
+ diffChars: text.length,
1344
+ truncated: true
1235
1345
  };
1236
1346
  }
1237
- /** URL del remote (origin, o el primero disponible). Vacío si no hay. */
1238
- function getRemoteUrl(repo, preferred = "origin") {
1239
- try {
1240
- const url = git(repo, [
1241
- "remote",
1242
- "get-url",
1243
- preferred
1244
- ]).trim();
1245
- if (url) return url;
1246
- } catch {}
1247
- try {
1248
- const remotes = git(repo, ["remote"]).split("\n").map((s) => s.trim()).filter(Boolean);
1249
- if (!remotes.length) return "";
1250
- return git(repo, [
1251
- "remote",
1252
- "get-url",
1253
- remotes[0]
1254
- ]).trim();
1255
- } catch {
1256
- return "";
1257
- }
1258
- }
1259
1347
  //#endregion
1260
1348
  //#region ../lib/extract.mjs
1261
1349
  function extractBranchDiff({ repo, branch, base = "develop" }) {
@@ -1786,6 +1874,228 @@ function buildReview({ skeleton, payload, notes = [] }) {
1786
1874
  };
1787
1875
  }
1788
1876
  //#endregion
1877
+ //#region src/lib/server/pick-file.ts
1878
+ /**
1879
+ * Diálogo nativo para elegir un JSON. Si `defaultDir` existe, abre ahí.
1880
+ */
1881
+ async function pickJsonFile(defaultDir) {
1882
+ const startDir = defaultDir && existsSync(defaultDir) ? defaultDir : "";
1883
+ if (platform() === "win32") try {
1884
+ return pickWindowsJson(startDir);
1885
+ } catch (err) {
1886
+ console.warn("[diff-review] picker JSON Windows falló, probando rfd/fallback:", err instanceof Error ? err.message : err);
1887
+ }
1888
+ try {
1889
+ return pickJsonWithRfd(startDir);
1890
+ } catch (err) {
1891
+ console.warn("[diff-review] @bindrs/rfd JSON no disponible, usando fallback:", err instanceof Error ? err.message : err);
1892
+ return pickJsonFallback(startDir);
1893
+ }
1894
+ }
1895
+ function resolveNodeBin$1() {
1896
+ if (process$1.execPath && /node(\.exe)?$/i.test(process$1.execPath)) return process$1.execPath;
1897
+ return "node";
1898
+ }
1899
+ function writeUtf8Bom$1(path, content) {
1900
+ writeFileSync(path, `\uFEFF${content}`, "utf8");
1901
+ }
1902
+ function readPicked(path) {
1903
+ if (!path || !existsSync(path)) return null;
1904
+ return {
1905
+ path,
1906
+ text: readFileSync(path, "utf8")
1907
+ };
1908
+ }
1909
+ /** OpenFileDialog con InitialDirectory (DPI: mejor esfuerzo). */
1910
+ function pickWindowsJson(startDir) {
1911
+ const dir = mkdtempSync(join(tmpdir(), "diff-review-pick-json-"));
1912
+ const script = join(dir, "pick.ps1");
1913
+ const outFile = join(dir, "path.txt");
1914
+ try {
1915
+ writeUtf8Bom$1(script, `
1916
+ $ErrorActionPreference = 'Stop'
1917
+ Add-Type -AssemblyName System.Windows.Forms
1918
+ try {
1919
+ Add-Type @'
1920
+ using System;
1921
+ using System.Runtime.InteropServices;
1922
+ public static class DiffReviewDpi {
1923
+ [DllImport("user32.dll")] public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
1924
+ public static readonly IntPtr PerMonitorV2 = unchecked((IntPtr)(-4));
1925
+ public static void Enable() {
1926
+ try { SetThreadDpiAwarenessContext(PerMonitorV2); } catch { }
1927
+ }
1928
+ }
1929
+ '@
1930
+ [DiffReviewDpi]::Enable()
1931
+ } catch {}
1932
+
1933
+ $dlg = New-Object System.Windows.Forms.OpenFileDialog
1934
+ $dlg.Title = 'Elegi el JSON de review'
1935
+ $dlg.Filter = 'JSON (*.json)|*.json|Todos (*.*)|*.*'
1936
+ $dlg.FilterIndex = 1
1937
+ $dlg.CheckFileExists = $true
1938
+ $dlg.Multiselect = $false
1939
+ $dir = ${JSON.stringify(startDir)}
1940
+ if ($dir -and (Test-Path -LiteralPath $dir)) { $dlg.InitialDirectory = $dir }
1941
+ if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { exit 2 }
1942
+ [System.IO.File]::WriteAllText(${JSON.stringify(outFile)}, $dlg.FileName)
1943
+ exit 0
1944
+ `.trim() + "\r\n");
1945
+ const result = spawnSync("powershell.exe", [
1946
+ "-STA",
1947
+ "-NoProfile",
1948
+ "-ExecutionPolicy",
1949
+ "Bypass",
1950
+ "-File",
1951
+ script
1952
+ ], {
1953
+ encoding: "utf8",
1954
+ windowsHide: false,
1955
+ stdio: [
1956
+ "ignore",
1957
+ "pipe",
1958
+ "pipe"
1959
+ ],
1960
+ timeout: 600 * 1e3
1961
+ });
1962
+ if (result.status === 2) return null;
1963
+ if (result.status !== 0) {
1964
+ const err = (result.stderr || result.stdout || result.error?.message || `exit ${result.status}`).toString().trim();
1965
+ throw new Error(err || "powershell json picker fallo");
1966
+ }
1967
+ if (!existsSync(outFile)) throw new Error("powershell json picker no escribio la ruta");
1968
+ const picked = readPicked(readFileSync(outFile, "utf8").replace(/^\uFEFF/, "").trim());
1969
+ if (!picked) throw new Error("powershell json picker devolvio ruta vacia");
1970
+ return picked;
1971
+ } finally {
1972
+ try {
1973
+ rmSync(dir, {
1974
+ recursive: true,
1975
+ force: true
1976
+ });
1977
+ } catch {}
1978
+ }
1979
+ }
1980
+ function pickJsonWithRfd(startDir) {
1981
+ const dir = mkdtempSync(join(tmpdir(), "diff-review-rfd-json-"));
1982
+ const worker = join(dir, "pick.mjs");
1983
+ const outFile = join(dir, "path.txt");
1984
+ try {
1985
+ writeFileSync(worker, `import { createRequire } from 'node:module';
1986
+ import { existsSync, writeFileSync } from 'node:fs';
1987
+ import { dirname, join } from 'node:path';
1988
+
1989
+ const require = createRequire(import.meta.url);
1990
+ const outFile = ${JSON.stringify(outFile)};
1991
+ const startDir = ${JSON.stringify(startDir)};
1992
+
1993
+ function findRfdIndex() {
1994
+ const seen = new Set();
1995
+ let dir = process.cwd();
1996
+ for (let i = 0; i < 16; i++) {
1997
+ if (seen.has(dir)) break;
1998
+ seen.add(dir);
1999
+ const indexJs = join(dir, 'node_modules', '@bindrs', 'rfd', 'index.js');
2000
+ if (existsSync(indexJs)) return indexJs;
2001
+ const parent = dirname(dir);
2002
+ if (parent === dir) break;
2003
+ dir = parent;
2004
+ }
2005
+ throw new Error('No se encontro @bindrs/rfd en node_modules');
2006
+ }
2007
+
2008
+ const { AsyncFileDialog } = require(findRfdIndex());
2009
+ let dlg = new AsyncFileDialog()
2010
+ .setTitle('Elegi el JSON de review')
2011
+ .addFilter('JSON', ['json']);
2012
+ if (startDir) dlg = dlg.setDirectory(startDir);
2013
+ const handle = await dlg.pickFile();
2014
+ if (!handle) process.exit(2);
2015
+ writeFileSync(outFile, handle.path(), 'utf8');
2016
+ `, "utf8");
2017
+ const result = spawnSync(resolveNodeBin$1(), [worker], {
2018
+ encoding: "utf8",
2019
+ windowsHide: false,
2020
+ stdio: [
2021
+ "ignore",
2022
+ "pipe",
2023
+ "pipe"
2024
+ ],
2025
+ timeout: 600 * 1e3,
2026
+ cwd: process$1.cwd(),
2027
+ env: process$1.env
2028
+ });
2029
+ if (result.status === 2 || result.status === 1) return null;
2030
+ if (result.status !== 0) {
2031
+ const err = (result.stderr || result.stdout || result.error?.message || `exit ${result.status}`).toString().trim();
2032
+ throw new Error(err || "worker rfd json fallo");
2033
+ }
2034
+ if (!existsSync(outFile)) return null;
2035
+ return readPicked(readFileSync(outFile, "utf8").replace(/^\uFEFF/, "").trim());
2036
+ } finally {
2037
+ try {
2038
+ rmSync(dir, {
2039
+ recursive: true,
2040
+ force: true
2041
+ });
2042
+ } catch {}
2043
+ }
2044
+ }
2045
+ function pickJsonFallback(startDir) {
2046
+ const os = platform();
2047
+ if (os === "darwin") return pickMacJson(startDir);
2048
+ if (os === "linux") return pickLinuxJson(startDir);
2049
+ return null;
2050
+ }
2051
+ function pickMacJson(startDir) {
2052
+ try {
2053
+ const out = execFileSync("osascript", startDir ? ["-e", `POSIX path of (choose file with prompt "Elegi el JSON de review" of type {"public.json","json"} default location POSIX file ${JSON.stringify(startDir + "/")})`] : ["-e", "POSIX path of (choose file with prompt \"Elegi el JSON de review\" of type {\"public.json\",\"json\"})"], {
2054
+ encoding: "utf8",
2055
+ timeout: 600 * 1e3
2056
+ });
2057
+ return readPicked(String(out || "").trim().replace(/\/$/, ""));
2058
+ } catch (err) {
2059
+ if (err.status === 1 || /User canceled|-128/i.test(String(err))) return null;
2060
+ throw new Error("No se pudo abrir el dialogo de archivo: " + (err instanceof Error ? err.message : String(err)));
2061
+ }
2062
+ }
2063
+ function pickLinuxJson(startDir) {
2064
+ try {
2065
+ const args = [
2066
+ "--file-selection",
2067
+ "--title=Elegi el JSON de review",
2068
+ "--file-filter=*.json"
2069
+ ];
2070
+ if (startDir) args.push(`--filename=${join(startDir, "/")}`);
2071
+ const out = execFileSync("zenity", args, {
2072
+ encoding: "utf8",
2073
+ timeout: 600 * 1e3
2074
+ });
2075
+ return readPicked(String(out || "").trim());
2076
+ } catch (zenityErr) {
2077
+ if (zenityErr.status === 1) return null;
2078
+ try {
2079
+ const out = execFileSync("kdialog", startDir ? [
2080
+ "--getopenfilename",
2081
+ startDir,
2082
+ "*.json"
2083
+ ] : [
2084
+ "--getopenfilename",
2085
+ ".",
2086
+ "*.json"
2087
+ ], {
2088
+ encoding: "utf8",
2089
+ timeout: 600 * 1e3
2090
+ });
2091
+ return readPicked(String(out || "").trim());
2092
+ } catch (kErr) {
2093
+ if (kErr.status === 1) return null;
2094
+ throw new Error("No hay dialogo de archivos disponible. Instala zenity o kdialog, o arrastra el JSON.");
2095
+ }
2096
+ }
2097
+ }
2098
+ //#endregion
1789
2099
  //#region src/lib/server/pick-folder.ts
1790
2100
  /**
1791
2101
  * Diálogo nativo de carpetas.
@@ -2164,9 +2474,140 @@ function loadLaunchRepoInfo() {
2164
2474
  return null;
2165
2475
  }
2166
2476
  }
2477
+ /**
2478
+ * JSON dejado por el CLI (env DIFF_REVIEW_IMPORT_FILE). Se consume una sola vez.
2479
+ */
2480
+ function takePendingImport() {
2481
+ const raw = String(process.env.DIFF_REVIEW_IMPORT_FILE || "").trim();
2482
+ if (!raw) return null;
2483
+ delete process.env.DIFF_REVIEW_IMPORT_FILE;
2484
+ const filePath = resolve(raw);
2485
+ const name = basename(filePath);
2486
+ if (!isOutputFilename(name)) return null;
2487
+ if (!existsSync(filePath)) return null;
2488
+ try {
2489
+ return {
2490
+ filename: name,
2491
+ payload: decodePayload(readFileSync(filePath, "utf8"))
2492
+ };
2493
+ } catch {
2494
+ return null;
2495
+ }
2496
+ }
2497
+ /**
2498
+ * Busca un JSON de review en la raíz del repo local.
2499
+ * Prefiere `preferredName` si existe; si no, el más reciente que matchee el patrón.
2500
+ */
2501
+ function detectReviewOutput(repo, preferredName) {
2502
+ const raw = String(repo || "").trim();
2503
+ if (!raw || looksLikeGitUrl(raw)) return null;
2504
+ let root;
2505
+ try {
2506
+ root = assertGitRepo(raw);
2507
+ } catch {
2508
+ return null;
2509
+ }
2510
+ const preferred = String(preferredName || "").trim();
2511
+ if (preferred && isOutputFilename(preferred)) {
2512
+ const preferredPath = join(root, preferred);
2513
+ if (existsSync(preferredPath)) try {
2514
+ return {
2515
+ filename: preferred,
2516
+ path: preferredPath,
2517
+ mtimeMs: statSync(preferredPath).mtimeMs
2518
+ };
2519
+ } catch {}
2520
+ }
2521
+ let best = null;
2522
+ try {
2523
+ for (const name of readdirSync(root)) {
2524
+ if (!isOutputFilename(name)) continue;
2525
+ const full = join(root, name);
2526
+ try {
2527
+ const st = statSync(full);
2528
+ if (!st.isFile()) continue;
2529
+ if (!best || st.mtimeMs > best.mtimeMs) best = {
2530
+ filename: name,
2531
+ path: full,
2532
+ mtimeMs: st.mtimeMs
2533
+ };
2534
+ } catch {}
2535
+ }
2536
+ } catch {
2537
+ return null;
2538
+ }
2539
+ return best;
2540
+ }
2541
+ /** Lee y parsea un JSON de review en la raíz del repo (solo nombres válidos). */
2542
+ function readReviewOutput(repo, filename) {
2543
+ const raw = String(repo || "").trim();
2544
+ const name = basename(String(filename || "").trim());
2545
+ if (!raw || !name || !isOutputFilename(name) || looksLikeGitUrl(raw)) return null;
2546
+ let root;
2547
+ try {
2548
+ root = assertGitRepo(raw);
2549
+ } catch {
2550
+ return null;
2551
+ }
2552
+ const filePath = join(root, name);
2553
+ if (!existsSync(filePath)) return null;
2554
+ try {
2555
+ return {
2556
+ filename: name,
2557
+ payload: decodePayload(readFileSync(filePath, "utf8"))
2558
+ };
2559
+ } catch {
2560
+ return null;
2561
+ }
2562
+ }
2167
2563
  async function pickLocalFolder$1() {
2168
2564
  return pickFolder();
2169
2565
  }
2566
+ /** Nombre de salida con tips cortos del branch y la base (repo local). */
2567
+ function resolveOutputName(repo, branch, base, stamp) {
2568
+ const outStamp = String(stamp || "").trim() || formatOutputStamp();
2569
+ const raw = String(repo || "").trim();
2570
+ const b = String(branch || "").trim();
2571
+ const baseRef = String(base || "").trim() || "develop";
2572
+ if (!raw || looksLikeGitUrl(raw) || !b) return {
2573
+ filename: makeOutputFilename({ stamp: outStamp }),
2574
+ stamp: outStamp,
2575
+ fingerprint: null
2576
+ };
2577
+ try {
2578
+ const tips = branchTips(assertGitRepo(raw), b, baseRef);
2579
+ const fp = fingerprint(tips.branchSha, tips.baseSha);
2580
+ return {
2581
+ filename: makeOutputFilename({
2582
+ branchSha: tips.branchSha,
2583
+ baseSha: tips.baseSha,
2584
+ stamp: outStamp
2585
+ }),
2586
+ stamp: outStamp,
2587
+ fingerprint: fp,
2588
+ branchSha: tips.branchSha,
2589
+ baseSha: tips.baseSha
2590
+ };
2591
+ } catch {
2592
+ return {
2593
+ filename: makeOutputFilename({ stamp: outStamp }),
2594
+ stamp: outStamp,
2595
+ fingerprint: null
2596
+ };
2597
+ }
2598
+ }
2599
+ /** Diálogo nativo de JSON; abre en la raíz del repo si es local. */
2600
+ async function pickReviewJsonFile(defaultDir) {
2601
+ const dir = String(defaultDir || "").trim();
2602
+ let start = dir;
2603
+ if (dir && !looksLikeGitUrl(dir)) try {
2604
+ start = assertGitRepo(dir);
2605
+ } catch {
2606
+ start = dir;
2607
+ }
2608
+ else start = "";
2609
+ return pickJsonFile(start || void 0);
2610
+ }
2170
2611
  function resolveWorktree(source, repo, branch, base) {
2171
2612
  if (source === "url") return ensureRemoteWorktree(repo, branch, base);
2172
2613
  return assertGitRepo(repo);
@@ -2221,11 +2662,16 @@ function hydrateReviewPayload(input) {
2221
2662
  //#endregion
2222
2663
  //#region src/lib/report/git.remote.ts
2223
2664
  var git_remote_exports = /* @__PURE__ */ __exportAll({
2665
+ detectOutput: () => detectOutput,
2224
2666
  fileLines: () => fileLines,
2225
2667
  hydrateFromGit: () => hydrateFromGit,
2226
2668
  launchContext: () => launchContext,
2669
+ loadDetectedOutput: () => loadDetectedOutput,
2227
2670
  loadRepo: () => loadRepo,
2228
- pickLocalFolder: () => pickLocalFolder
2671
+ outputName: () => outputName,
2672
+ pendingImport: () => pendingImport,
2673
+ pickLocalFolder: () => pickLocalFolder,
2674
+ pickReviewFile: () => pickReviewFile
2229
2675
  });
2230
2676
  function fail(err) {
2231
2677
  const message = describeGitError(err) || "No se pudo hablar con git.";
@@ -2248,6 +2694,46 @@ var launchContext = /* @__PURE__ */ query(async () => {
2248
2694
  return null;
2249
2695
  }
2250
2696
  });
2697
+ /** JSON pendiente del CLI (DIFF_REVIEW_IMPORT_FILE); se consume al leer. */
2698
+ var pendingImport = /* @__PURE__ */ query(async () => {
2699
+ try {
2700
+ return takePendingImport();
2701
+ } catch (err) {
2702
+ console.error("[diff-review git] pending import", describeGitError(err), err);
2703
+ return null;
2704
+ }
2705
+ });
2706
+ /** ¿Hay un JSON de review en la raíz del repo local? */
2707
+ var detectOutput = /* @__PURE__ */ query("unchecked", async (input) => {
2708
+ try {
2709
+ return detectReviewOutput(String(input?.repo || "").trim(), input?.preferredName);
2710
+ } catch (err) {
2711
+ console.error("[diff-review git] detect output", describeGitError(err), err);
2712
+ return null;
2713
+ }
2714
+ });
2715
+ /** Nombre sugerido con tips cortos (branch/base) + stamp de sesión. */
2716
+ var outputName = /* @__PURE__ */ query("unchecked", async (input) => {
2717
+ try {
2718
+ return resolveOutputName(String(input?.repo || "").trim(), String(input?.branch || "").trim(), String(input?.base || "develop").trim(), input?.stamp);
2719
+ } catch (err) {
2720
+ console.error("[diff-review git] output name", describeGitError(err), err);
2721
+ const stamp = String(input?.stamp || "").trim() || formatOutputStamp();
2722
+ return {
2723
+ filename: makeOutputFilename({ stamp }),
2724
+ stamp,
2725
+ fingerprint: null
2726
+ };
2727
+ }
2728
+ });
2729
+ /** Lee un JSON detectado en el repo (sin diálogo). */
2730
+ var loadDetectedOutput = /* @__PURE__ */ command("unchecked", async (input) => {
2731
+ try {
2732
+ return readReviewOutput(String(input?.repo || "").trim(), String(input?.filename || "").trim());
2733
+ } catch (err) {
2734
+ fail(err);
2735
+ }
2736
+ });
2251
2737
  var pickLocalFolder = /* @__PURE__ */ command("unchecked", async (_input) => {
2252
2738
  try {
2253
2739
  return await pickLocalFolder$1();
@@ -2255,6 +2741,19 @@ var pickLocalFolder = /* @__PURE__ */ command("unchecked", async (_input) => {
2255
2741
  fail(err);
2256
2742
  }
2257
2743
  });
2744
+ /** Diálogo nativo de JSON; `directory` = raíz del repo local. */
2745
+ var pickReviewFile = /* @__PURE__ */ command("unchecked", async (input) => {
2746
+ try {
2747
+ const picked = await pickReviewJsonFile(input?.directory);
2748
+ if (!picked) return null;
2749
+ return {
2750
+ filename: basename(picked.path),
2751
+ payload: decodePayload(picked.text)
2752
+ };
2753
+ } catch (err) {
2754
+ fail(err);
2755
+ }
2756
+ });
2258
2757
  /** Líneas del archivo en el branch revisado, para expandir el contexto oculto de un diff. */
2259
2758
  var fileLines = /* @__PURE__ */ query("unchecked", async (input) => loadFileLines({
2260
2759
  repo: String(input?.repo || "").trim(),
@@ -2286,4 +2785,4 @@ for (const [name, fn] of Object.entries(git_remote_exports)) {
2286
2785
  var _sveltekit_remote_1g4ayag_default = git_remote_exports;
2287
2786
 
2288
2787
  export { _sveltekit_remote_1g4ayag_default as default };
2289
- //# sourceMappingURL=remote-1g4ayag-BgUNMUS8.js.map
2788
+ //# sourceMappingURL=remote-1g4ayag-DXdFe1BG.js.map