prodex 2.0.1 → 2.1.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.
Files changed (119) hide show
  1. package/README.md +367 -146
  2. package/dist/app/dependency-source-provider.d.ts +2 -0
  3. package/dist/app/dependency-source-provider.js +66 -0
  4. package/dist/app/entry-resolver.d.ts +7 -0
  5. package/dist/app/entry-resolver.js +26 -0
  6. package/dist/app/execute-run.d.ts +2 -2
  7. package/dist/app/execute-run.js +138 -37
  8. package/dist/app/git-source-provider.d.ts +4 -0
  9. package/dist/app/git-source-provider.js +551 -0
  10. package/dist/app/grep-source-provider.d.ts +2 -0
  11. package/dist/app/grep-source-provider.js +226 -0
  12. package/dist/app/planner.d.ts +12 -0
  13. package/dist/app/planner.js +106 -0
  14. package/dist/app/planners/attachment-options.d.ts +2 -0
  15. package/dist/app/planners/attachment-options.js +39 -0
  16. package/dist/app/planners/git-plan.d.ts +13 -0
  17. package/dist/app/planners/git-plan.js +141 -0
  18. package/dist/app/planners/grep-plan.d.ts +13 -0
  19. package/dist/app/planners/grep-plan.js +115 -0
  20. package/dist/app/planners/list-utils.d.ts +1 -0
  21. package/dist/app/planners/list-utils.js +6 -0
  22. package/dist/app/planners/pack-plan.d.ts +13 -0
  23. package/dist/app/planners/pack-plan.js +65 -0
  24. package/dist/app/planners/scope-plan.d.ts +17 -0
  25. package/dist/app/planners/scope-plan.js +143 -0
  26. package/dist/app/planners/trace-plan.d.ts +13 -0
  27. package/dist/app/planners/trace-plan.js +77 -0
  28. package/dist/app/source-collector.d.ts +5 -0
  29. package/dist/app/source-collector.js +33 -0
  30. package/dist/app/target-resolver.d.ts +7 -0
  31. package/dist/app/target-resolver.js +110 -0
  32. package/dist/cli/cli-input.js +70 -19
  33. package/dist/cli/flag-specs.d.ts +7 -3
  34. package/dist/cli/flag-specs.js +58 -8
  35. package/dist/cli/help-specs.d.ts +205 -0
  36. package/dist/cli/help-specs.js +136 -0
  37. package/dist/cli/help.js +107 -28
  38. package/dist/cli/report-command.js +35 -23
  39. package/dist/commands/{run-command.d.ts → git-command.d.ts} +3 -3
  40. package/dist/commands/git-command.js +7 -0
  41. package/dist/commands/grep-command.d.ts +11 -0
  42. package/dist/commands/grep-command.js +7 -0
  43. package/dist/commands/pack-command.d.ts +11 -0
  44. package/dist/commands/pack-command.js +7 -0
  45. package/dist/commands/scope-command.d.ts +12 -0
  46. package/dist/commands/scope-command.js +7 -0
  47. package/dist/commands/shared-runner.d.ts +12 -0
  48. package/dist/commands/shared-runner.js +30 -0
  49. package/dist/commands/trace-command.d.ts +11 -0
  50. package/dist/commands/trace-command.js +7 -0
  51. package/dist/config/default-config.js +7 -11
  52. package/dist/config/load.d.ts +1 -0
  53. package/dist/config/load.js +269 -4
  54. package/dist/config/migration/detect.d.ts +2 -1
  55. package/dist/config/migration/detect.js +10 -2
  56. package/dist/config/migration/messages.js +12 -11
  57. package/dist/config/migration/transform.js +77 -32
  58. package/dist/filesystem/binary.d.ts +2 -0
  59. package/dist/filesystem/binary.js +30 -0
  60. package/dist/filesystem/entry-discovery.d.ts +14 -0
  61. package/dist/filesystem/entry-discovery.js +78 -0
  62. package/dist/filesystem/file-set.d.ts +7 -0
  63. package/dist/filesystem/file-set.js +57 -0
  64. package/dist/filesystem/glob-scan.d.ts +6 -2
  65. package/dist/filesystem/glob-scan.js +12 -7
  66. package/dist/filesystem/path-patterns.d.ts +20 -0
  67. package/dist/filesystem/path-patterns.js +70 -0
  68. package/dist/index.js +51 -29
  69. package/dist/output/markdown-sections.d.ts +5 -0
  70. package/dist/output/markdown-sections.js +103 -0
  71. package/dist/output/markdown-toc.d.ts +31 -0
  72. package/dist/output/markdown-toc.js +287 -0
  73. package/dist/output/markdown.d.ts +5 -8
  74. package/dist/output/markdown.js +80 -127
  75. package/dist/output/produce-output.d.ts +4 -1
  76. package/dist/output/produce-output.js +6 -7
  77. package/dist/output/render-helpers.d.ts +4 -0
  78. package/dist/output/render-helpers.js +14 -0
  79. package/dist/output/text.d.ts +2 -2
  80. package/dist/output/text.js +47 -8
  81. package/dist/resolvers/js/resolve-alias.js +10 -41
  82. package/dist/resolvers/php/bindings.js +6 -6
  83. package/dist/resolvers/php/extract-imports.d.ts +19 -7
  84. package/dist/resolvers/php/extract-imports.js +146 -26
  85. package/dist/resolvers/php/php-resolver.js +55 -24
  86. package/dist/resolvers/php/psr4.d.ts +1 -1
  87. package/dist/resolvers/php/psr4.js +7 -1
  88. package/dist/runtime/shell-command-runner.d.ts +5 -0
  89. package/dist/runtime/shell-command-runner.js +118 -0
  90. package/dist/tracing/collect-trace.d.ts +10 -0
  91. package/dist/tracing/collect-trace.js +23 -0
  92. package/dist/tracing/exclude.d.ts +0 -5
  93. package/dist/tracing/exclude.js +16 -12
  94. package/dist/tracing/follow-chain.js +2 -2
  95. package/dist/types/app.types.d.ts +37 -8
  96. package/dist/types/artifact.types.d.ts +50 -0
  97. package/dist/types/artifact.types.js +2 -0
  98. package/dist/types/cli.types.d.ts +52 -4
  99. package/dist/types/config.types.d.ts +37 -20
  100. package/dist/types/index.d.ts +2 -0
  101. package/dist/types/index.js +2 -0
  102. package/dist/types/output.types.d.ts +12 -3
  103. package/dist/types/planner.types.d.ts +76 -0
  104. package/dist/types/planner.types.js +2 -0
  105. package/dist/types/resolver.types.d.ts +1 -1
  106. package/dist/types/tracing.types.d.ts +0 -11
  107. package/package.json +2 -2
  108. package/schema/prodex.schema.json +59 -41
  109. package/dist/app/run-plans.d.ts +0 -11
  110. package/dist/app/run-plans.js +0 -61
  111. package/dist/commands/profiles-command.d.ts +0 -6
  112. package/dist/commands/profiles-command.js +0 -16
  113. package/dist/commands/run-command.js +0 -17
  114. package/dist/config/build-config.d.ts +0 -13
  115. package/dist/config/build-config.js +0 -127
  116. package/dist/tracing/include-files.d.ts +0 -2
  117. package/dist/tracing/include-files.js +0 -36
  118. package/dist/tracing/trace-run.d.ts +0 -2
  119. package/dist/tracing/trace-run.js +0 -21
@@ -0,0 +1,551 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runGit = runGit;
7
+ exports.runGitBuffer = runGitBuffer;
8
+ exports.collectGitSources = collectGitSources;
9
+ const child_process_1 = require("child_process");
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const binary_1 = require("../filesystem/binary");
13
+ const file_set_1 = require("../filesystem/file-set");
14
+ const path_2 = require("../filesystem/path");
15
+ const exclude_1 = require("../tracing/exclude");
16
+ function runGit(args, cwd, warnings, errors, maxBuffer = 10 * 1024 * 1024) {
17
+ try {
18
+ const res = (0, child_process_1.spawnSync)("git", args, {
19
+ cwd,
20
+ shell: false,
21
+ encoding: "utf8",
22
+ maxBuffer,
23
+ });
24
+ if (res.error) {
25
+ const err = res.error;
26
+ if (err.code === "ENOBUFS") {
27
+ warnings.push("Git output exceeded limit and was truncated.");
28
+ return (res.stdout || "").slice(0, maxBuffer);
29
+ }
30
+ if (err.code === "ENOENT") {
31
+ errors.push("Git executable not found in PATH.");
32
+ }
33
+ else {
34
+ errors.push(`Git command error: ${err.message || String(err)}`);
35
+ }
36
+ return "";
37
+ }
38
+ if (res.status !== 0) {
39
+ errors.push(`Git command failed (exit code ${res.status}): ${res.stderr || "unknown error"}`);
40
+ return "";
41
+ }
42
+ if (res.stderr) {
43
+ warnings.push(`Git command stderr: ${res.stderr}`);
44
+ }
45
+ return res.stdout || "";
46
+ }
47
+ catch (err) {
48
+ errors.push(`Git execution error: ${err.message || String(err)}`);
49
+ return "";
50
+ }
51
+ }
52
+ function runGitBuffer(args, cwd, warnings, errors, maxBuffer = 10 * 1024 * 1024) {
53
+ try {
54
+ const res = (0, child_process_1.spawnSync)("git", args, {
55
+ cwd,
56
+ shell: false,
57
+ encoding: "buffer",
58
+ maxBuffer,
59
+ });
60
+ if (res.error) {
61
+ const err = res.error;
62
+ if (err.code === "ENOBUFS") {
63
+ warnings.push("Git output exceeded limit and was truncated.");
64
+ return (res.stdout || Buffer.alloc(0)).subarray(0, maxBuffer);
65
+ }
66
+ if (err.code === "ENOENT") {
67
+ errors.push("Git executable not found in PATH.");
68
+ }
69
+ else {
70
+ errors.push(`Git command error: ${err.message || String(err)}`);
71
+ }
72
+ return null;
73
+ }
74
+ if (res.status !== 0) {
75
+ errors.push(`Git command failed (exit code ${res.status}): ${res.stderr ? res.stderr.toString("utf8") : "unknown error"}`);
76
+ return null;
77
+ }
78
+ return res.stdout || Buffer.alloc(0);
79
+ }
80
+ catch (err) {
81
+ errors.push(`Git execution error: ${err.message || String(err)}`);
82
+ return null;
83
+ }
84
+ }
85
+ function readHistoricalFile(rev, gitRelPath, gitRoot, warnings, errors) {
86
+ const initialErrorCount = errors.length;
87
+ const buf = runGitBuffer(["show", `${rev}:${gitRelPath}`], gitRoot, warnings, errors);
88
+ if (errors.length > initialErrorCount) {
89
+ const lastError = errors[errors.length - 1];
90
+ if (lastError.includes("does not exist in") ||
91
+ lastError.includes("exists on disk but not in") ||
92
+ lastError.includes("Path '") ||
93
+ lastError.includes("exists on disk, but not in")) {
94
+ errors.splice(initialErrorCount); // clear new errors
95
+ return { content: null, isBinary: false, isDeleted: true };
96
+ }
97
+ return { content: null, isBinary: false, isDeleted: false };
98
+ }
99
+ if (!buf) {
100
+ return { content: null, isBinary: false, isDeleted: false };
101
+ }
102
+ const isBinary = (0, binary_1.isBinaryBuffer)(buf);
103
+ if (isBinary) {
104
+ return { content: null, isBinary: true, isDeleted: false };
105
+ }
106
+ return { content: buf.toString("utf8"), isBinary: false, isDeleted: false };
107
+ }
108
+ function buildHistoricalSections(gitOptions, gitRoot, changesSummaryList, warnings, errors) {
109
+ const sections = [];
110
+ // 1. Git Mode section
111
+ let modeContent = "";
112
+ if (gitOptions.mode === "commit") {
113
+ const commitInfo = runGit(["show", "-s", "--format=Revision: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s", gitOptions.rev], gitRoot, warnings, errors).trim();
114
+ modeContent = `Mode: commit\n${commitInfo || `Revision: ${gitOptions.rev}`}`;
115
+ }
116
+ else if (gitOptions.mode === "range") {
117
+ const baseSha = runGit(["rev-parse", gitOptions.base], gitRoot, warnings, errors).trim();
118
+ const headSha = runGit(["rev-parse", gitOptions.head], gitRoot, warnings, errors).trim();
119
+ modeContent = `Mode: range\nSpec: ${gitOptions.spec}\nBase SHA: ${baseSha}\nHead SHA: ${headSha}`;
120
+ }
121
+ else if (gitOptions.mode === "against") {
122
+ const baseSha = runGit(["rev-parse", gitOptions.base], gitRoot, warnings, errors).trim();
123
+ const headSha = runGit(["rev-parse", "HEAD"], gitRoot, warnings, errors).trim();
124
+ modeContent = `Mode: against\nAgainst: ${gitOptions.base} (${baseSha})\nMerge Base: ${gitOptions.mergeBase}\nHEAD SHA: ${headSha}`;
125
+ }
126
+ sections.push({
127
+ id: "git-mode",
128
+ title: "Git Mode",
129
+ kind: "code",
130
+ language: "txt",
131
+ content: modeContent || "(unknown mode)",
132
+ });
133
+ // 2. Diff Stat section
134
+ let statTitle = "Git Diff Stat";
135
+ let statContent = "";
136
+ if (gitOptions.mode === "commit") {
137
+ statTitle = "Commit Diff Stat";
138
+ statContent = runGit(["show", "--stat", "--oneline", gitOptions.rev], gitRoot, warnings, errors);
139
+ }
140
+ else if (gitOptions.mode === "range") {
141
+ statTitle = "Range Diff Stat";
142
+ statContent = runGit(["diff", "--stat", gitOptions.spec], gitRoot, warnings, errors);
143
+ }
144
+ else if (gitOptions.mode === "against") {
145
+ statTitle = "Branch Diff Stat";
146
+ statContent = runGit(["diff", "--stat", gitOptions.mergeBase, "HEAD"], gitRoot, warnings, errors);
147
+ }
148
+ sections.push({
149
+ id: "git-diff-stat",
150
+ title: statTitle,
151
+ kind: "code",
152
+ language: "txt",
153
+ content: statContent || "(no diff stat)",
154
+ });
155
+ // 3. File Notes section
156
+ const filteredSummary = [...changesSummaryList];
157
+ if (filteredSummary.length === 0) {
158
+ filteredSummary.push("(none)");
159
+ }
160
+ sections.push({
161
+ id: "file-notes",
162
+ title: "File Notes",
163
+ kind: "text",
164
+ content: filteredSummary.join("\n"),
165
+ });
166
+ // 4. Full Diff (when includeDiff is passed)
167
+ if (gitOptions.includeDiff) {
168
+ let fullDiffTitle = "Full Diff";
169
+ let fullDiffContent = "";
170
+ if (gitOptions.mode === "commit") {
171
+ fullDiffTitle = "Commit Diff";
172
+ fullDiffContent = runGit(["show", gitOptions.rev], gitRoot, warnings, errors, 5 * 1024 * 1024);
173
+ }
174
+ else if (gitOptions.mode === "range") {
175
+ fullDiffTitle = "Range Diff";
176
+ fullDiffContent = runGit(["diff", gitOptions.spec], gitRoot, warnings, errors, 5 * 1024 * 1024);
177
+ }
178
+ else if (gitOptions.mode === "against") {
179
+ fullDiffTitle = "Branch Diff";
180
+ fullDiffContent = runGit(["diff", gitOptions.mergeBase, "HEAD"], gitRoot, warnings, errors, 5 * 1024 * 1024);
181
+ }
182
+ sections.push({
183
+ id: "full-diff",
184
+ title: fullDiffTitle,
185
+ kind: "code",
186
+ language: "diff",
187
+ content: fullDiffContent || "(no changes)",
188
+ });
189
+ }
190
+ return sections;
191
+ }
192
+ async function collectHistoricalSources(plan, gitOptions, gitRoot, warnings, errors) {
193
+ // 1. Resolve snapshot revision and file listing parameters
194
+ let snapshotRev = "";
195
+ let diffOut = "";
196
+ if (gitOptions.mode === "commit") {
197
+ // Verify commit exists
198
+ const revParse = runGit(["rev-parse", "--verify", gitOptions.rev], gitRoot, warnings, errors).trim();
199
+ if (errors.length || !revParse) {
200
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
201
+ }
202
+ snapshotRev = gitOptions.rev;
203
+ // Run git diff-tree -M --root --no-commit-id -r --name-status -z <rev>
204
+ diffOut = runGit(["diff-tree", "-M", "--root", "--no-commit-id", "-r", "--name-status", "-z", snapshotRev], gitRoot, warnings, errors);
205
+ }
206
+ else if (gitOptions.mode === "range") {
207
+ // Warn if using three-dot range
208
+ if (gitOptions.spec.includes("...")) {
209
+ warnings.push(`Using three-dot range: comparing merge-base(${gitOptions.base}, ${gitOptions.head}) to ${gitOptions.head}.`);
210
+ }
211
+ // Verify endpoints
212
+ const baseParse = runGit(["rev-parse", "--verify", gitOptions.base], gitRoot, warnings, errors).trim();
213
+ const headParse = runGit(["rev-parse", "--verify", gitOptions.head], gitRoot, warnings, errors).trim();
214
+ if (errors.length || !baseParse || !headParse) {
215
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
216
+ }
217
+ snapshotRev = gitOptions.head;
218
+ // Run git diff -M --name-status -z <spec>
219
+ diffOut = runGit(["diff", "-M", "--name-status", "-z", gitOptions.spec], gitRoot, warnings, errors);
220
+ }
221
+ else if (gitOptions.mode === "against") {
222
+ // Resolve merge-base
223
+ const mergeBase = runGit(["merge-base", gitOptions.base, "HEAD"], gitRoot, warnings, errors).trim();
224
+ if (errors.length || !mergeBase) {
225
+ errors.push(`Failed to find merge-base between ${gitOptions.base} and HEAD.`);
226
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
227
+ }
228
+ gitOptions.mergeBase = mergeBase;
229
+ snapshotRev = "HEAD";
230
+ // Run git diff -M --name-status -z <mergeBase> HEAD
231
+ diffOut = runGit(["diff", "-M", "--name-status", "-z", mergeBase, "HEAD"], gitRoot, warnings, errors);
232
+ }
233
+ if (errors.length) {
234
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
235
+ }
236
+ // 2. Parse NUL-separated name-status output
237
+ const tokens = diffOut.split("\0");
238
+ const parsedFiles = [];
239
+ let i = 0;
240
+ while (i < tokens.length) {
241
+ const status = tokens[i];
242
+ if (!status) {
243
+ i++;
244
+ continue;
245
+ }
246
+ if (status.startsWith("R") || status.startsWith("C")) {
247
+ const oldPath = tokens[i + 1];
248
+ const newPath = tokens[i + 2];
249
+ if (oldPath && newPath) {
250
+ parsedFiles.push({ status, path: newPath, oldPath });
251
+ }
252
+ i += 3;
253
+ }
254
+ else {
255
+ const filePath = tokens[i + 1];
256
+ if (filePath) {
257
+ parsedFiles.push({ status, path: filePath });
258
+ }
259
+ i += 2;
260
+ }
261
+ }
262
+ // 3. Apply excludes to notes too! Consistent exclude filtering
263
+ const filteredParsedFiles = parsedFiles.filter((file) => {
264
+ const absPath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, file.path));
265
+ return !(0, exclude_1.isExcluded)(absPath, plan.exclude, plan.root);
266
+ });
267
+ // 4. Resolve file listing and snapshots
268
+ const gitSelectedPaths = [];
269
+ const snapshots = [];
270
+ const changesSummaryList = [];
271
+ const relPath = (p) => path_1.default.relative(plan.root, p).replaceAll("\\", "/");
272
+ for (const file of filteredParsedFiles) {
273
+ const absPath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, file.path));
274
+ const isDeleted = file.status.startsWith("D");
275
+ if (isDeleted) {
276
+ changesSummaryList.push(`- Deleted: ${relPath(absPath)}`);
277
+ gitSelectedPaths.push(absPath);
278
+ continue;
279
+ }
280
+ if (file.status.startsWith("R")) {
281
+ const oldAbsPath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, file.oldPath));
282
+ changesSummaryList.push(`- Renamed: ${relPath(oldAbsPath)} -> ${relPath(absPath)}`);
283
+ }
284
+ else if (file.status.startsWith("C")) {
285
+ const oldAbsPath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, file.oldPath));
286
+ changesSummaryList.push(`- Copied: ${relPath(oldAbsPath)} -> ${relPath(absPath)}`);
287
+ }
288
+ // Read content from git history
289
+ const res = readHistoricalFile(snapshotRev, file.path, gitRoot, warnings, errors);
290
+ if (res.isDeleted) {
291
+ changesSummaryList.push(`- Deleted: ${relPath(absPath)}`);
292
+ gitSelectedPaths.push(absPath);
293
+ continue;
294
+ }
295
+ if (res.isBinary) {
296
+ changesSummaryList.push(`- Binary: ${relPath(absPath)}`);
297
+ gitSelectedPaths.push(absPath);
298
+ continue;
299
+ }
300
+ if (res.content !== null) {
301
+ gitSelectedPaths.push(absPath);
302
+ snapshots.push({
303
+ path: absPath,
304
+ content: res.content,
305
+ });
306
+ }
307
+ else {
308
+ // readError
309
+ gitSelectedPaths.push(absPath);
310
+ snapshots.push({
311
+ path: absPath,
312
+ content: "",
313
+ readError: "Failed to read file from Git history.",
314
+ });
315
+ }
316
+ }
317
+ // 5. Merge, resolve includes, apply excludes
318
+ const filteredPaths = await (0, file_set_1.buildFinalFileSet)({
319
+ root: plan.root,
320
+ sources: gitSelectedPaths,
321
+ include: plan.include ?? [],
322
+ exclude: plan.exclude,
323
+ });
324
+ // 6. Classify final path set
325
+ const finalFiles = [];
326
+ for (const p of filteredPaths) {
327
+ const isHistorical = gitSelectedPaths.includes(p);
328
+ if (isHistorical) {
329
+ const hasSnapshot = snapshots.some((s) => s.path === p);
330
+ if (hasSnapshot) {
331
+ finalFiles.push(p);
332
+ }
333
+ continue;
334
+ }
335
+ // Otherwise, it came from includes, so read from disk
336
+ const isDeleted = !fs_1.default.existsSync(p);
337
+ if (isDeleted) {
338
+ changesSummaryList.push(`- Deleted: ${relPath(p)}`);
339
+ continue;
340
+ }
341
+ if ((0, binary_1.isBinaryFile)(p)) {
342
+ changesSummaryList.push(`- Binary: ${relPath(p)}`);
343
+ continue;
344
+ }
345
+ finalFiles.push(p);
346
+ }
347
+ // 7. Validation error for empty results
348
+ const hasGitMatches = gitSelectedPaths.length > 0;
349
+ const hasIncludes = plan.include && plan.include.length > 0;
350
+ if (!hasGitMatches && !hasIncludes) {
351
+ errors.push("No files matched the Git revision/range options and no include patterns were provided.");
352
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
353
+ }
354
+ // 8. Generate Git metadata sections (Commit 3)
355
+ const sections = buildHistoricalSections(gitOptions, gitRoot, changesSummaryList, warnings, errors);
356
+ return {
357
+ files: finalFiles,
358
+ entries: [],
359
+ includes: plan.include ?? [],
360
+ mode: "git",
361
+ warnings,
362
+ errors,
363
+ sections,
364
+ snapshots,
365
+ };
366
+ }
367
+ async function collectGitSources(plan) {
368
+ const warnings = [];
369
+ const errors = [];
370
+ // 1. Verify if we are inside a Git repository and get the top level directory
371
+ const isInside = runGit(["rev-parse", "--is-inside-work-tree"], plan.root, warnings, errors);
372
+ if (errors.length) {
373
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
374
+ }
375
+ if (isInside.trim() !== "true") {
376
+ errors.push("Not a git repository (or any of the parent directories).");
377
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
378
+ }
379
+ const gitRoot = runGit(["rev-parse", "--show-toplevel"], plan.root, warnings, errors).trim();
380
+ if (errors.length || !gitRoot) {
381
+ errors.push("Failed to resolve git top-level directory.");
382
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
383
+ }
384
+ const gitOptions = plan.gitOptions || { mode: "working-tree", changed: true, staged: true, unstaged: true, untracked: true, includeDiff: false };
385
+ if (gitOptions.mode !== "working-tree" && gitOptions.mode !== undefined) {
386
+ return collectHistoricalSources(plan, gitOptions, gitRoot, warnings, errors);
387
+ }
388
+ // 2. Query git status --porcelain -z -uall relative to Git root
389
+ const statusOut = runGit(["status", "--porcelain", "-z", "-uall"], gitRoot, warnings, errors);
390
+ if (errors.length) {
391
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
392
+ }
393
+ // Determine active source options (already parsed above as working-tree)
394
+ // 3. Parse porcelain entries
395
+ const tokens = statusOut.split("\0");
396
+ const gitSelectedPaths = [];
397
+ const deletedList = [];
398
+ const renamedList = [];
399
+ let i = 0;
400
+ while (i < tokens.length) {
401
+ const token = tokens[i];
402
+ if (!token) {
403
+ i++;
404
+ continue;
405
+ }
406
+ const xy = token.slice(0, 2);
407
+ const path1 = token.slice(3);
408
+ const isRename = xy[0] === "R" || xy[0] === "C" || xy[1] === "R" || xy[1] === "C";
409
+ let fileGitRel = "";
410
+ let oldFileGitRel = "";
411
+ if (isRename) {
412
+ fileGitRel = path1; // new path / destination
413
+ oldFileGitRel = tokens[i + 1] || ""; // old path / source
414
+ i += 2;
415
+ }
416
+ else {
417
+ fileGitRel = path1;
418
+ i++;
419
+ }
420
+ if (!fileGitRel)
421
+ continue;
422
+ // Classify entry using X and Y status codes
423
+ const X = xy[0];
424
+ const Y = xy[1];
425
+ const isUntracked = (X === "?" && Y === "?");
426
+ const isStaged = !isUntracked && X !== " " && X !== "!" && X !== "?";
427
+ const isUnstaged = !isUntracked && Y !== " " && Y !== "!" && Y !== "?";
428
+ let isSelected = false;
429
+ if (gitOptions.staged && isStaged)
430
+ isSelected = true;
431
+ if (gitOptions.unstaged && isUnstaged)
432
+ isSelected = true;
433
+ if (gitOptions.untracked && isUntracked)
434
+ isSelected = true;
435
+ if (isSelected) {
436
+ const absolutePath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, fileGitRel));
437
+ gitSelectedPaths.push(absolutePath);
438
+ if (X === "D" || Y === "D") {
439
+ deletedList.push(absolutePath);
440
+ }
441
+ if (isRename) {
442
+ const oldAbsolutePath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, oldFileGitRel));
443
+ renamedList.push({ oldPath: oldAbsolutePath, newPath: absolutePath });
444
+ }
445
+ }
446
+ }
447
+ // 4. Merge, resolve includes, apply excludes, and sort deterministically
448
+ const filteredPaths = await (0, file_set_1.buildFinalFileSet)({
449
+ root: plan.root,
450
+ sources: gitSelectedPaths,
451
+ include: plan.include ?? [],
452
+ exclude: plan.exclude,
453
+ });
454
+ // 7. Classify final path set
455
+ const finalFiles = [];
456
+ const changesSummaryList = [];
457
+ // Map paths back to plan-root-relative or just project relative for summary display
458
+ const relPath = (p) => path_1.default.relative(plan.root, p).replaceAll("\\", "/");
459
+ for (const p of filteredPaths) {
460
+ const isDeleted = deletedList.includes(p) || !fs_1.default.existsSync(p);
461
+ if (isDeleted) {
462
+ changesSummaryList.push(`- Deleted: ${relPath(p)}`);
463
+ continue;
464
+ }
465
+ const renameMatch = renamedList.find(r => r.newPath === p);
466
+ if (renameMatch) {
467
+ changesSummaryList.push(`- Renamed: ${relPath(renameMatch.oldPath)} -> ${relPath(renameMatch.newPath)}`);
468
+ }
469
+ if ((0, binary_1.isBinaryFile)(p)) {
470
+ changesSummaryList.push(`- Binary: ${relPath(p)}`);
471
+ continue;
472
+ }
473
+ // It is a normal readable file or a readable file that might fail on snapshot
474
+ finalFiles.push(p);
475
+ }
476
+ // Double check validation:
477
+ // If no git selected files and no includes, fail clearly.
478
+ const hasGitMatches = gitSelectedPaths.length > 0;
479
+ const hasIncludes = plan.include && plan.include.length > 0;
480
+ if (!hasGitMatches && !hasIncludes) {
481
+ errors.push("No Git working-state files matched the active source flags and no include patterns were provided.");
482
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
483
+ }
484
+ // 8. Generate Git metadata sections
485
+ const sections = [];
486
+ // Git Status short
487
+ const statusShort = runGit(["status", "--short"], plan.root, warnings, errors);
488
+ sections.push({
489
+ id: "git-status",
490
+ title: "Git Status",
491
+ kind: "code",
492
+ language: "txt",
493
+ content: statusShort || "(empty status)",
494
+ });
495
+ // Git Diff Stat
496
+ const diffStat = runGit(["diff", "--stat"], plan.root, warnings, errors);
497
+ sections.push({
498
+ id: "git-diff-stat",
499
+ title: "Git Diff Stat",
500
+ kind: "code",
501
+ language: "txt",
502
+ content: diffStat || "(no diff stat)",
503
+ });
504
+ // Git Cached Diff Stat
505
+ const diffCachedStat = runGit(["diff", "--cached", "--stat"], plan.root, warnings, errors);
506
+ sections.push({
507
+ id: "git-diff-cached-stat",
508
+ title: "Git Cached Diff Stat",
509
+ kind: "code",
510
+ language: "txt",
511
+ content: diffCachedStat || "(no cached diff stat)",
512
+ });
513
+ // File Notes
514
+ if (changesSummaryList.length === 0) {
515
+ changesSummaryList.push("(none)");
516
+ }
517
+ sections.push({
518
+ id: "file-notes",
519
+ title: "File Notes",
520
+ kind: "text",
521
+ content: changesSummaryList.join("\n"),
522
+ });
523
+ // Optional full diff sections
524
+ if (gitOptions.includeDiff) {
525
+ const diffText = runGit(["diff"], plan.root, warnings, errors, 5 * 1024 * 1024);
526
+ sections.push({
527
+ id: "full-diff",
528
+ title: "Full Diff",
529
+ kind: "code",
530
+ language: "diff",
531
+ content: diffText || "(no changes)",
532
+ });
533
+ const diffCachedText = runGit(["diff", "--cached"], plan.root, warnings, errors, 5 * 1024 * 1024);
534
+ sections.push({
535
+ id: "cached-full-diff",
536
+ title: "Cached Full Diff",
537
+ kind: "code",
538
+ language: "diff",
539
+ content: diffCachedText || "(no changes)",
540
+ });
541
+ }
542
+ return {
543
+ files: finalFiles,
544
+ entries: [],
545
+ includes: plan.include ?? [],
546
+ mode: "git",
547
+ warnings,
548
+ errors,
549
+ sections,
550
+ };
551
+ }
@@ -0,0 +1,2 @@
1
+ import type { ExecutionPlan, SourceCollectionResult } from "../types";
2
+ export declare function collectGrepSources(plan: ExecutionPlan): Promise<SourceCollectionResult>;