@jpkme/diff-review 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +22 -6
  2. package/bin/diff-review.mjs +328 -18
  3. package/lib/autocomplete-es.mjs +143 -0
  4. package/lib/clipboard.mjs +29 -0
  5. package/lib/config.mjs +233 -0
  6. package/lib/config.test.mjs +73 -0
  7. package/lib/extract.mjs +111 -0
  8. package/lib/git.mjs +202 -0
  9. package/lib/git.test.mjs +29 -0
  10. package/lib/json-payload.mjs +31 -0
  11. package/lib/json-payload.test.mjs +22 -0
  12. package/lib/output-name.mjs +44 -0
  13. package/lib/output-name.test.mjs +37 -0
  14. package/lib/pick-folder.mjs +4 -0
  15. package/lib/prompt.mjs +160 -0
  16. package/lib/remote-repo.mjs +182 -0
  17. package/lib/review.mjs +228 -0
  18. package/lib/review.test.mjs +160 -0
  19. package/lib/validate.mjs +221 -0
  20. package/lib/validate.test.mjs +85 -0
  21. package/lib/wait-output.mjs +104 -0
  22. package/package.json +4 -2
  23. package/schema.json +12 -0
  24. package/web/build/client/_app/immutable/assets/2.Xnfq26gJ.css +1 -0
  25. package/web/build/client/_app/immutable/assets/2.Xnfq26gJ.css.br +0 -0
  26. package/web/build/client/_app/immutable/assets/2.Xnfq26gJ.css.gz +0 -0
  27. package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js +1 -0
  28. package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js.br +2 -0
  29. package/web/build/client/_app/immutable/chunks/CO0mUwMJ.js.gz +0 -0
  30. package/web/build/client/_app/immutable/chunks/{C5moC2DW.js → CdHZDAAo.js} +42 -32
  31. package/web/build/client/_app/immutable/chunks/CdHZDAAo.js.br +0 -0
  32. package/web/build/client/_app/immutable/chunks/CdHZDAAo.js.gz +0 -0
  33. package/web/build/client/_app/immutable/chunks/DzqTpz7_.js +2 -0
  34. package/web/build/client/_app/immutable/chunks/DzqTpz7_.js.br +0 -0
  35. package/web/build/client/_app/immutable/chunks/DzqTpz7_.js.gz +0 -0
  36. package/web/build/client/_app/immutable/entry/{app.Dei8qxo7.js → app.BRzz8J51.js} +2 -2
  37. package/web/build/client/_app/immutable/entry/app.BRzz8J51.js.br +0 -0
  38. package/web/build/client/_app/immutable/entry/app.BRzz8J51.js.gz +0 -0
  39. package/web/build/client/_app/immutable/entry/start.BSioRkqK.js +1 -0
  40. package/web/build/client/_app/immutable/entry/start.BSioRkqK.js.br +0 -0
  41. package/web/build/client/_app/immutable/entry/start.BSioRkqK.js.gz +0 -0
  42. package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js +1 -0
  43. package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js.br +1 -0
  44. package/web/build/client/_app/immutable/nodes/1.ws8Khan5.js.gz +0 -0
  45. package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js +66 -0
  46. package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js.br +0 -0
  47. package/web/build/client/_app/immutable/nodes/2.CsD5xqRB.js.gz +0 -0
  48. package/web/build/client/_app/immutable/nodes/3.ByTmpJQZ.js +170 -0
  49. package/web/build/client/_app/immutable/nodes/3.ByTmpJQZ.js.br +0 -0
  50. package/web/build/client/_app/immutable/nodes/3.ByTmpJQZ.js.gz +0 -0
  51. package/web/build/client/_app/version.json +1 -1
  52. package/web/build/client/_app/version.json.br +0 -0
  53. package/web/build/client/_app/version.json.gz +0 -0
  54. package/web/build/handler.js +3 -3
  55. package/web/build/index.js +3 -3
  56. package/web/build/server/chunks/1-qrn24yN5.js +7 -0
  57. package/web/build/server/chunks/1-qrn24yN5.js.map +1 -0
  58. package/web/build/server/chunks/2-C7_Loqyn.js +7 -0
  59. package/web/build/server/chunks/2-C7_Loqyn.js.map +1 -0
  60. package/web/build/server/chunks/{3-Zc5_qwgV.js → 3-DZ5PBhOc.js} +2 -2
  61. package/web/build/server/chunks/{3-Zc5_qwgV.js.map → 3-DZ5PBhOc.js.map} +1 -1
  62. package/web/build/server/chunks/{handler-BmuqxCCf.js → handler-DD4vP3bU.js} +3 -3
  63. package/web/build/server/chunks/{handler-BmuqxCCf.js.map → handler-DD4vP3bU.js.map} +1 -1
  64. package/web/build/server/chunks/{index.js-41EcOndB.js → index.js-IhFPHwmp.js} +150 -150
  65. package/web/build/server/chunks/index.js-IhFPHwmp.js.map +1 -0
  66. package/web/build/server/chunks/{manifest.js-KnmnjP9i.js → manifest.js-Dk3RuAA5.js} +6 -6
  67. package/web/build/server/chunks/{manifest.js-KnmnjP9i.js.map → manifest.js-Dk3RuAA5.js.map} +1 -1
  68. package/web/build/server/chunks/{remote-1g4ayag-BgUNMUS8.js → remote-1g4ayag-BRL-uaKE.js} +632 -146
  69. package/web/build/server/chunks/remote-1g4ayag-BRL-uaKE.js.map +1 -0
  70. package/web/build/client/_app/immutable/assets/2.Du8E-7-D.css +0 -1
  71. package/web/build/client/_app/immutable/assets/2.Du8E-7-D.css.br +0 -0
  72. package/web/build/client/_app/immutable/assets/2.Du8E-7-D.css.gz +0 -0
  73. package/web/build/client/_app/immutable/chunks/C5moC2DW.js.br +0 -0
  74. package/web/build/client/_app/immutable/chunks/C5moC2DW.js.gz +0 -0
  75. package/web/build/client/_app/immutable/chunks/WxPrACSs.js +0 -2
  76. package/web/build/client/_app/immutable/chunks/WxPrACSs.js.br +0 -0
  77. package/web/build/client/_app/immutable/chunks/WxPrACSs.js.gz +0 -0
  78. package/web/build/client/_app/immutable/entry/app.Dei8qxo7.js.br +0 -0
  79. package/web/build/client/_app/immutable/entry/app.Dei8qxo7.js.gz +0 -0
  80. package/web/build/client/_app/immutable/entry/start.Fzxrn2l4.js +0 -1
  81. package/web/build/client/_app/immutable/entry/start.Fzxrn2l4.js.br +0 -1
  82. package/web/build/client/_app/immutable/entry/start.Fzxrn2l4.js.gz +0 -0
  83. package/web/build/client/_app/immutable/nodes/1.Cn_iktkl.js +0 -1
  84. package/web/build/client/_app/immutable/nodes/1.Cn_iktkl.js.br +0 -0
  85. package/web/build/client/_app/immutable/nodes/1.Cn_iktkl.js.gz +0 -0
  86. package/web/build/client/_app/immutable/nodes/2.vvYFHTy-.js +0 -66
  87. package/web/build/client/_app/immutable/nodes/2.vvYFHTy-.js.br +0 -0
  88. package/web/build/client/_app/immutable/nodes/2.vvYFHTy-.js.gz +0 -0
  89. package/web/build/client/_app/immutable/nodes/3.CeJ7dEsh.js +0 -170
  90. package/web/build/client/_app/immutable/nodes/3.CeJ7dEsh.js.br +0 -0
  91. package/web/build/client/_app/immutable/nodes/3.CeJ7dEsh.js.gz +0 -0
  92. package/web/build/server/chunks/1-Bjgeu1Iz.js +0 -7
  93. package/web/build/server/chunks/1-Bjgeu1Iz.js.map +0 -1
  94. package/web/build/server/chunks/2-BvMZU9fP.js +0 -7
  95. package/web/build/server/chunks/2-BvMZU9fP.js.map +0 -1
  96. package/web/build/server/chunks/index.js-41EcOndB.js.map +0 -1
  97. 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-IhFPHwmp.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,224 @@ 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 branch/base. Legacy (con stamp o sin tips) sigue válido al importar.
1119
+ */
1120
+ /** Prefijo del formato actual. */
1121
+ var OUTPUT_PREFIX = "diff-rev";
1122
+ /** Nombre legacy fijo. */
1123
+ var OUTPUT_FILENAME_LEGACY = `diff-review-output.json`;
1124
+ /**
1125
+ * Válidos:
1126
+ * - diff-rev_{branchTip}_{baseTip}.json
1127
+ * - diff-report_{branchTip}-{baseTip}_YYYYMMDD-HHmmss.json (legacy)
1128
+ * - diff-review-output.json / diff-review-output-YYYYMMDD-HHmmss.json (legacy)
1129
+ */
1130
+ var OUTPUT_FILENAME_RE = /^(?:diff-rev_[0-9a-f]{7,12}_[0-9a-f]{7,12}|diff-report_[0-9a-f]{7,12}-[0-9a-f]{7,12}_\d{8}-\d{6}|diff-review-output(?:-\d{8}-\d{6})?)\.json$/i;
1131
+ /**
1132
+ * @param {{ branchSha?: string, baseSha?: string }} [input]
1133
+ * @returns {string}
1134
+ * con tips: diff-rev_aaaaaaaaaa_bbbbbbbbbb.json
1135
+ * sin tips: diff-review-output.json
1136
+ */
1137
+ function makeOutputFilename(input = {}) {
1138
+ const opts = input instanceof Date ? {} : input || {};
1139
+ const fp = fingerprint(opts.branchSha, opts.baseSha);
1140
+ if (fp) {
1141
+ const [branchTip, baseTip] = fp.split("-");
1142
+ if (branchTip && baseTip) return `${OUTPUT_PREFIX}_${branchTip}_${baseTip}.json`;
1143
+ }
1144
+ return OUTPUT_FILENAME_LEGACY;
1145
+ }
1146
+ function isOutputFilename(name) {
1147
+ return OUTPUT_FILENAME_RE.test(String(name || "").trim());
1148
+ }
1149
+ //#endregion
1150
+ //#region ../lib/json-payload.mjs
1151
+ function tryParseJson(s) {
1152
+ try {
1153
+ return JSON.parse(s);
1154
+ } catch {
1155
+ return null;
1156
+ }
1157
+ }
1158
+ /** Extrae un objeto JSON del texto del modelo (fences, prosa alrededor, envelope). */
1159
+ function decodePayload(text) {
1160
+ const raw = String(text || "").trim();
1161
+ if (!raw) throw new Error("Respuesta vacía.");
1162
+ const fence = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
1163
+ if (fence) {
1164
+ const parsed = tryParseJson(fence[1].trim());
1165
+ if (parsed && typeof parsed === "object") return parsed;
1166
+ }
1167
+ const direct = tryParseJson(raw);
1168
+ if (direct && typeof direct === "object") return direct;
1169
+ const start = raw.indexOf("{");
1170
+ const end = raw.lastIndexOf("}");
1171
+ if (start >= 0 && end > start) {
1172
+ const parsed = tryParseJson(raw.slice(start, end + 1));
1173
+ if (parsed && typeof parsed === "object") return parsed;
1174
+ }
1175
+ throw new Error("No se pudo interpretar la respuesta como JSON.");
1176
+ }
1177
+ //#endregion
960
1178
  //#region ../lib/config.mjs
961
1179
  var DEFAULT_IGNORE = [
962
1180
  "**/package-lock.json",
@@ -1112,150 +1330,14 @@ function capDiff(diff, max = STORE_DIFF_MAX) {
1112
1330
  if (text.length <= max) return {
1113
1331
  diff: text,
1114
1332
  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");
1333
+ truncated: false
1334
+ };
1231
1335
  return {
1232
- branches: unique,
1233
- current,
1234
- defaultBase: bases.includes("develop") ? "develop" : bases.includes("main") ? "main" : bases[0] || "develop"
1336
+ diff: `${text.slice(0, max)}\n\n… [diff recortado: se guardan ${max} de ${text.length} caracteres]`,
1337
+ diffChars: text.length,
1338
+ truncated: true
1235
1339
  };
1236
1340
  }
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
1341
  //#endregion
1260
1342
  //#region ../lib/extract.mjs
1261
1343
  function extractBranchDiff({ repo, branch, base = "develop" }) {
@@ -1786,6 +1868,228 @@ function buildReview({ skeleton, payload, notes = [] }) {
1786
1868
  };
1787
1869
  }
1788
1870
  //#endregion
1871
+ //#region src/lib/server/pick-file.ts
1872
+ /**
1873
+ * Diálogo nativo para elegir un JSON. Si `defaultDir` existe, abre ahí.
1874
+ */
1875
+ async function pickJsonFile(defaultDir) {
1876
+ const startDir = defaultDir && existsSync(defaultDir) ? defaultDir : "";
1877
+ if (platform() === "win32") try {
1878
+ return pickWindowsJson(startDir);
1879
+ } catch (err) {
1880
+ console.warn("[diff-review] picker JSON Windows falló, probando rfd/fallback:", err instanceof Error ? err.message : err);
1881
+ }
1882
+ try {
1883
+ return pickJsonWithRfd(startDir);
1884
+ } catch (err) {
1885
+ console.warn("[diff-review] @bindrs/rfd JSON no disponible, usando fallback:", err instanceof Error ? err.message : err);
1886
+ return pickJsonFallback(startDir);
1887
+ }
1888
+ }
1889
+ function resolveNodeBin$1() {
1890
+ if (process$1.execPath && /node(\.exe)?$/i.test(process$1.execPath)) return process$1.execPath;
1891
+ return "node";
1892
+ }
1893
+ function writeUtf8Bom$1(path, content) {
1894
+ writeFileSync(path, `\uFEFF${content}`, "utf8");
1895
+ }
1896
+ function readPicked(path) {
1897
+ if (!path || !existsSync(path)) return null;
1898
+ return {
1899
+ path,
1900
+ text: readFileSync(path, "utf8")
1901
+ };
1902
+ }
1903
+ /** OpenFileDialog con InitialDirectory (DPI: mejor esfuerzo). */
1904
+ function pickWindowsJson(startDir) {
1905
+ const dir = mkdtempSync(join(tmpdir(), "diff-review-pick-json-"));
1906
+ const script = join(dir, "pick.ps1");
1907
+ const outFile = join(dir, "path.txt");
1908
+ try {
1909
+ writeUtf8Bom$1(script, `
1910
+ $ErrorActionPreference = 'Stop'
1911
+ Add-Type -AssemblyName System.Windows.Forms
1912
+ try {
1913
+ Add-Type @'
1914
+ using System;
1915
+ using System.Runtime.InteropServices;
1916
+ public static class DiffReviewDpi {
1917
+ [DllImport("user32.dll")] public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
1918
+ public static readonly IntPtr PerMonitorV2 = unchecked((IntPtr)(-4));
1919
+ public static void Enable() {
1920
+ try { SetThreadDpiAwarenessContext(PerMonitorV2); } catch { }
1921
+ }
1922
+ }
1923
+ '@
1924
+ [DiffReviewDpi]::Enable()
1925
+ } catch {}
1926
+
1927
+ $dlg = New-Object System.Windows.Forms.OpenFileDialog
1928
+ $dlg.Title = 'Elegi el JSON de review'
1929
+ $dlg.Filter = 'JSON (*.json)|*.json|Todos (*.*)|*.*'
1930
+ $dlg.FilterIndex = 1
1931
+ $dlg.CheckFileExists = $true
1932
+ $dlg.Multiselect = $false
1933
+ $dir = ${JSON.stringify(startDir)}
1934
+ if ($dir -and (Test-Path -LiteralPath $dir)) { $dlg.InitialDirectory = $dir }
1935
+ if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { exit 2 }
1936
+ [System.IO.File]::WriteAllText(${JSON.stringify(outFile)}, $dlg.FileName)
1937
+ exit 0
1938
+ `.trim() + "\r\n");
1939
+ const result = spawnSync("powershell.exe", [
1940
+ "-STA",
1941
+ "-NoProfile",
1942
+ "-ExecutionPolicy",
1943
+ "Bypass",
1944
+ "-File",
1945
+ script
1946
+ ], {
1947
+ encoding: "utf8",
1948
+ windowsHide: false,
1949
+ stdio: [
1950
+ "ignore",
1951
+ "pipe",
1952
+ "pipe"
1953
+ ],
1954
+ timeout: 600 * 1e3
1955
+ });
1956
+ if (result.status === 2) return null;
1957
+ if (result.status !== 0) {
1958
+ const err = (result.stderr || result.stdout || result.error?.message || `exit ${result.status}`).toString().trim();
1959
+ throw new Error(err || "powershell json picker fallo");
1960
+ }
1961
+ if (!existsSync(outFile)) throw new Error("powershell json picker no escribio la ruta");
1962
+ const picked = readPicked(readFileSync(outFile, "utf8").replace(/^\uFEFF/, "").trim());
1963
+ if (!picked) throw new Error("powershell json picker devolvio ruta vacia");
1964
+ return picked;
1965
+ } finally {
1966
+ try {
1967
+ rmSync(dir, {
1968
+ recursive: true,
1969
+ force: true
1970
+ });
1971
+ } catch {}
1972
+ }
1973
+ }
1974
+ function pickJsonWithRfd(startDir) {
1975
+ const dir = mkdtempSync(join(tmpdir(), "diff-review-rfd-json-"));
1976
+ const worker = join(dir, "pick.mjs");
1977
+ const outFile = join(dir, "path.txt");
1978
+ try {
1979
+ writeFileSync(worker, `import { createRequire } from 'node:module';
1980
+ import { existsSync, writeFileSync } from 'node:fs';
1981
+ import { dirname, join } from 'node:path';
1982
+
1983
+ const require = createRequire(import.meta.url);
1984
+ const outFile = ${JSON.stringify(outFile)};
1985
+ const startDir = ${JSON.stringify(startDir)};
1986
+
1987
+ function findRfdIndex() {
1988
+ const seen = new Set();
1989
+ let dir = process.cwd();
1990
+ for (let i = 0; i < 16; i++) {
1991
+ if (seen.has(dir)) break;
1992
+ seen.add(dir);
1993
+ const indexJs = join(dir, 'node_modules', '@bindrs', 'rfd', 'index.js');
1994
+ if (existsSync(indexJs)) return indexJs;
1995
+ const parent = dirname(dir);
1996
+ if (parent === dir) break;
1997
+ dir = parent;
1998
+ }
1999
+ throw new Error('No se encontro @bindrs/rfd en node_modules');
2000
+ }
2001
+
2002
+ const { AsyncFileDialog } = require(findRfdIndex());
2003
+ let dlg = new AsyncFileDialog()
2004
+ .setTitle('Elegi el JSON de review')
2005
+ .addFilter('JSON', ['json']);
2006
+ if (startDir) dlg = dlg.setDirectory(startDir);
2007
+ const handle = await dlg.pickFile();
2008
+ if (!handle) process.exit(2);
2009
+ writeFileSync(outFile, handle.path(), 'utf8');
2010
+ `, "utf8");
2011
+ const result = spawnSync(resolveNodeBin$1(), [worker], {
2012
+ encoding: "utf8",
2013
+ windowsHide: false,
2014
+ stdio: [
2015
+ "ignore",
2016
+ "pipe",
2017
+ "pipe"
2018
+ ],
2019
+ timeout: 600 * 1e3,
2020
+ cwd: process$1.cwd(),
2021
+ env: process$1.env
2022
+ });
2023
+ if (result.status === 2 || result.status === 1) return null;
2024
+ if (result.status !== 0) {
2025
+ const err = (result.stderr || result.stdout || result.error?.message || `exit ${result.status}`).toString().trim();
2026
+ throw new Error(err || "worker rfd json fallo");
2027
+ }
2028
+ if (!existsSync(outFile)) return null;
2029
+ return readPicked(readFileSync(outFile, "utf8").replace(/^\uFEFF/, "").trim());
2030
+ } finally {
2031
+ try {
2032
+ rmSync(dir, {
2033
+ recursive: true,
2034
+ force: true
2035
+ });
2036
+ } catch {}
2037
+ }
2038
+ }
2039
+ function pickJsonFallback(startDir) {
2040
+ const os = platform();
2041
+ if (os === "darwin") return pickMacJson(startDir);
2042
+ if (os === "linux") return pickLinuxJson(startDir);
2043
+ return null;
2044
+ }
2045
+ function pickMacJson(startDir) {
2046
+ try {
2047
+ 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\"})"], {
2048
+ encoding: "utf8",
2049
+ timeout: 600 * 1e3
2050
+ });
2051
+ return readPicked(String(out || "").trim().replace(/\/$/, ""));
2052
+ } catch (err) {
2053
+ if (err.status === 1 || /User canceled|-128/i.test(String(err))) return null;
2054
+ throw new Error("No se pudo abrir el dialogo de archivo: " + (err instanceof Error ? err.message : String(err)));
2055
+ }
2056
+ }
2057
+ function pickLinuxJson(startDir) {
2058
+ try {
2059
+ const args = [
2060
+ "--file-selection",
2061
+ "--title=Elegi el JSON de review",
2062
+ "--file-filter=*.json"
2063
+ ];
2064
+ if (startDir) args.push(`--filename=${join(startDir, "/")}`);
2065
+ const out = execFileSync("zenity", args, {
2066
+ encoding: "utf8",
2067
+ timeout: 600 * 1e3
2068
+ });
2069
+ return readPicked(String(out || "").trim());
2070
+ } catch (zenityErr) {
2071
+ if (zenityErr.status === 1) return null;
2072
+ try {
2073
+ const out = execFileSync("kdialog", startDir ? [
2074
+ "--getopenfilename",
2075
+ startDir,
2076
+ "*.json"
2077
+ ] : [
2078
+ "--getopenfilename",
2079
+ ".",
2080
+ "*.json"
2081
+ ], {
2082
+ encoding: "utf8",
2083
+ timeout: 600 * 1e3
2084
+ });
2085
+ return readPicked(String(out || "").trim());
2086
+ } catch (kErr) {
2087
+ if (kErr.status === 1) return null;
2088
+ throw new Error("No hay dialogo de archivos disponible. Instala zenity o kdialog, o arrastra el JSON.");
2089
+ }
2090
+ }
2091
+ }
2092
+ //#endregion
1789
2093
  //#region src/lib/server/pick-folder.ts
1790
2094
  /**
1791
2095
  * Diálogo nativo de carpetas.
@@ -2164,9 +2468,135 @@ function loadLaunchRepoInfo() {
2164
2468
  return null;
2165
2469
  }
2166
2470
  }
2471
+ /**
2472
+ * JSON dejado por el CLI (env DIFF_REVIEW_IMPORT_FILE). Se consume una sola vez.
2473
+ */
2474
+ function takePendingImport() {
2475
+ const raw = String(process.env.DIFF_REVIEW_IMPORT_FILE || "").trim();
2476
+ if (!raw) return null;
2477
+ delete process.env.DIFF_REVIEW_IMPORT_FILE;
2478
+ const filePath = resolve(raw);
2479
+ const name = basename(filePath);
2480
+ if (!isOutputFilename(name)) return null;
2481
+ if (!existsSync(filePath)) return null;
2482
+ try {
2483
+ return {
2484
+ filename: name,
2485
+ payload: decodePayload(readFileSync(filePath, "utf8"))
2486
+ };
2487
+ } catch {
2488
+ return null;
2489
+ }
2490
+ }
2491
+ /**
2492
+ * Busca un JSON de review en la raíz del repo local.
2493
+ * Prefiere `preferredName` si existe; si no, el más reciente que matchee el patrón.
2494
+ */
2495
+ function detectReviewOutput(repo, preferredName) {
2496
+ const raw = String(repo || "").trim();
2497
+ if (!raw || looksLikeGitUrl(raw)) return null;
2498
+ let root;
2499
+ try {
2500
+ root = assertGitRepo(raw);
2501
+ } catch {
2502
+ return null;
2503
+ }
2504
+ const preferred = String(preferredName || "").trim();
2505
+ if (preferred && isOutputFilename(preferred)) {
2506
+ const preferredPath = join(root, preferred);
2507
+ if (existsSync(preferredPath)) try {
2508
+ return {
2509
+ filename: preferred,
2510
+ path: preferredPath,
2511
+ mtimeMs: statSync(preferredPath).mtimeMs
2512
+ };
2513
+ } catch {}
2514
+ }
2515
+ let best = null;
2516
+ try {
2517
+ for (const name of readdirSync(root)) {
2518
+ if (!isOutputFilename(name)) continue;
2519
+ const full = join(root, name);
2520
+ try {
2521
+ const st = statSync(full);
2522
+ if (!st.isFile()) continue;
2523
+ if (!best || st.mtimeMs > best.mtimeMs) best = {
2524
+ filename: name,
2525
+ path: full,
2526
+ mtimeMs: st.mtimeMs
2527
+ };
2528
+ } catch {}
2529
+ }
2530
+ } catch {
2531
+ return null;
2532
+ }
2533
+ return best;
2534
+ }
2535
+ /** Lee y parsea un JSON de review en la raíz del repo (solo nombres válidos). */
2536
+ function readReviewOutput(repo, filename) {
2537
+ const raw = String(repo || "").trim();
2538
+ const name = basename(String(filename || "").trim());
2539
+ if (!raw || !name || !isOutputFilename(name) || looksLikeGitUrl(raw)) return null;
2540
+ let root;
2541
+ try {
2542
+ root = assertGitRepo(raw);
2543
+ } catch {
2544
+ return null;
2545
+ }
2546
+ const filePath = join(root, name);
2547
+ if (!existsSync(filePath)) return null;
2548
+ try {
2549
+ return {
2550
+ filename: name,
2551
+ payload: decodePayload(readFileSync(filePath, "utf8"))
2552
+ };
2553
+ } catch {
2554
+ return null;
2555
+ }
2556
+ }
2167
2557
  async function pickLocalFolder$1() {
2168
2558
  return pickFolder();
2169
2559
  }
2560
+ /** Nombre de salida con tips cortos del branch y la base (repo local). */
2561
+ function resolveOutputName(repo, branch, base) {
2562
+ const raw = String(repo || "").trim();
2563
+ const b = String(branch || "").trim();
2564
+ const baseRef = String(base || "").trim() || "develop";
2565
+ if (!raw || looksLikeGitUrl(raw) || !b) return {
2566
+ filename: makeOutputFilename({}),
2567
+ fingerprint: null
2568
+ };
2569
+ try {
2570
+ const tips = branchTips(assertGitRepo(raw), b, baseRef);
2571
+ const fp = fingerprint(tips.branchSha, tips.baseSha);
2572
+ return {
2573
+ filename: makeOutputFilename({
2574
+ branchSha: tips.branchSha,
2575
+ baseSha: tips.baseSha
2576
+ }),
2577
+ fingerprint: fp,
2578
+ branchSha: tips.branchSha,
2579
+ baseSha: tips.baseSha
2580
+ };
2581
+ } catch {
2582
+ return {
2583
+ filename: makeOutputFilename({}),
2584
+ fingerprint: null
2585
+ };
2586
+ }
2587
+ }
2588
+ /** Diálogo nativo de JSON; abre en la raíz del repo si es local. */
2589
+ async function pickReviewJsonFile(defaultDir) {
2590
+ const dir = String(defaultDir || "").trim();
2591
+ let start = dir;
2592
+ if (dir && !looksLikeGitUrl(dir)) try {
2593
+ start = assertGitRepo(dir);
2594
+ } catch {
2595
+ start = dir;
2596
+ }
2597
+ else start = "";
2598
+ return pickJsonFile(start || void 0);
2599
+ }
2170
2600
  function resolveWorktree(source, repo, branch, base) {
2171
2601
  if (source === "url") return ensureRemoteWorktree(repo, branch, base);
2172
2602
  return assertGitRepo(repo);
@@ -2221,11 +2651,16 @@ function hydrateReviewPayload(input) {
2221
2651
  //#endregion
2222
2652
  //#region src/lib/report/git.remote.ts
2223
2653
  var git_remote_exports = /* @__PURE__ */ __exportAll({
2654
+ detectOutput: () => detectOutput,
2224
2655
  fileLines: () => fileLines,
2225
2656
  hydrateFromGit: () => hydrateFromGit,
2226
2657
  launchContext: () => launchContext,
2658
+ loadDetectedOutput: () => loadDetectedOutput,
2227
2659
  loadRepo: () => loadRepo,
2228
- pickLocalFolder: () => pickLocalFolder
2660
+ outputName: () => outputName,
2661
+ pendingImport: () => pendingImport,
2662
+ pickLocalFolder: () => pickLocalFolder,
2663
+ pickReviewFile: () => pickReviewFile
2229
2664
  });
2230
2665
  function fail(err) {
2231
2666
  const message = describeGitError(err) || "No se pudo hablar con git.";
@@ -2248,6 +2683,44 @@ var launchContext = /* @__PURE__ */ query(async () => {
2248
2683
  return null;
2249
2684
  }
2250
2685
  });
2686
+ /** JSON pendiente del CLI (DIFF_REVIEW_IMPORT_FILE); se consume al leer. */
2687
+ var pendingImport = /* @__PURE__ */ query(async () => {
2688
+ try {
2689
+ return takePendingImport();
2690
+ } catch (err) {
2691
+ console.error("[diff-review git] pending import", describeGitError(err), err);
2692
+ return null;
2693
+ }
2694
+ });
2695
+ /** ¿Hay un JSON de review en la raíz del repo local? */
2696
+ var detectOutput = /* @__PURE__ */ query("unchecked", async (input) => {
2697
+ try {
2698
+ return detectReviewOutput(String(input?.repo || "").trim(), input?.preferredName);
2699
+ } catch (err) {
2700
+ console.error("[diff-review git] detect output", describeGitError(err), err);
2701
+ return null;
2702
+ }
2703
+ });
2704
+ /** Nombre sugerido con tips cortos (branch/base). */
2705
+ var outputName = /* @__PURE__ */ query("unchecked", async (input) => {
2706
+ try {
2707
+ return resolveOutputName(String(input?.repo || "").trim(), String(input?.branch || "").trim(), String(input?.base || "develop").trim());
2708
+ } catch (err) {
2709
+ console.error("[diff-review git] output name", describeGitError(err), err);
2710
+ return {
2711
+ filename: makeOutputFilename({}),
2712
+ fingerprint: null
2713
+ };
2714
+ }
2715
+ });
2716
+ /** Lee un JSON detectado en el repo (sin diálogo). */
2717
+ var loadDetectedOutput = /* @__PURE__ */ command("unchecked", async (input) => {
2718
+ try {
2719
+ return readReviewOutput(String(input?.repo || "").trim(), String(input?.filename || "").trim());
2720
+ } catch (err) {
2721
+ fail(err);
2722
+ }
2723
+ });
2251
2724
  var pickLocalFolder = /* @__PURE__ */ command("unchecked", async (_input) => {
2252
2725
  try {
2253
2726
  return await pickLocalFolder$1();
@@ -2255,6 +2728,19 @@ var pickLocalFolder = /* @__PURE__ */ command("unchecked", async (_input) => {
2255
2728
  fail(err);
2256
2729
  }
2257
2730
  });
2731
+ /** Diálogo nativo de JSON; `directory` = raíz del repo local. */
2732
+ var pickReviewFile = /* @__PURE__ */ command("unchecked", async (input) => {
2733
+ try {
2734
+ const picked = await pickReviewJsonFile(input?.directory);
2735
+ if (!picked) return null;
2736
+ return {
2737
+ filename: basename(picked.path),
2738
+ payload: decodePayload(picked.text)
2739
+ };
2740
+ } catch (err) {
2741
+ fail(err);
2742
+ }
2743
+ });
2258
2744
  /** Líneas del archivo en el branch revisado, para expandir el contexto oculto de un diff. */
2259
2745
  var fileLines = /* @__PURE__ */ query("unchecked", async (input) => loadFileLines({
2260
2746
  repo: String(input?.repo || "").trim(),
@@ -2286,4 +2772,4 @@ for (const [name, fn] of Object.entries(git_remote_exports)) {
2286
2772
  var _sveltekit_remote_1g4ayag_default = git_remote_exports;
2287
2773
 
2288
2774
  export { _sveltekit_remote_1g4ayag_default as default };
2289
- //# sourceMappingURL=remote-1g4ayag-BgUNMUS8.js.map
2775
+ //# sourceMappingURL=remote-1g4ayag-BRL-uaKE.js.map