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,226 @@
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.collectGrepSources = collectGrepSources;
7
+ const child_process_1 = require("child_process");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const file_set_1 = require("../filesystem/file-set");
11
+ const path_2 = require("../filesystem/path");
12
+ async function collectGrepSources(plan) {
13
+ const warnings = [];
14
+ const errors = [];
15
+ const opts = plan.grepOptions;
16
+ if (!opts) {
17
+ errors.push("Missing grep options in execution plan.");
18
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "grep", warnings, errors };
19
+ }
20
+ // 1. Build ripgrep arguments
21
+ const args = ["--json"];
22
+ if (opts.mode === "regex") {
23
+ args.push("-e", opts.terms[0]);
24
+ }
25
+ else {
26
+ // query, any, all mode use fixed-strings
27
+ args.push("-F");
28
+ if (opts.mode === "all") {
29
+ // Search for the first term with ripgrep, we will filter the rest in JS
30
+ args.push("-e", opts.terms[0]);
31
+ }
32
+ else {
33
+ // query (one term) or any (multiple terms in OR search)
34
+ for (const term of opts.terms) {
35
+ args.push("-e", term);
36
+ }
37
+ }
38
+ }
39
+ // Add within path boundaries
40
+ for (const w of opts.within) {
41
+ const absPath = path_1.default.isAbsolute(w) ? w : path_1.default.resolve(plan.root, w);
42
+ const relPath = path_1.default.relative(plan.root, absPath).replace(/\\/g, "/");
43
+ args.push("-g", relPath);
44
+ }
45
+ // Add skip path boundaries
46
+ for (const s of opts.skip) {
47
+ const absPath = path_1.default.isAbsolute(s) ? s : path_1.default.resolve(plan.root, s);
48
+ const relPath = path_1.default.relative(plan.root, absPath).replace(/\\/g, "/");
49
+ args.push("-g", `!${relPath}`);
50
+ }
51
+ // Target the current directory explicitly to avoid ripgrep default behavior of reading stdin
52
+ args.push(".");
53
+ // 2. Execute ripgrep
54
+ const res = (0, child_process_1.spawnSync)("rg", args, {
55
+ cwd: plan.root,
56
+ encoding: "utf8",
57
+ maxBuffer: 10 * 1024 * 1024,
58
+ });
59
+ if (res.error) {
60
+ if (res.error.code === "ENOENT") {
61
+ errors.push('Command "grep" requires ripgrep (`rg`) to be installed.');
62
+ }
63
+ else {
64
+ errors.push(`Ripgrep execution error: ${res.error.message}`);
65
+ }
66
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "grep", warnings, errors };
67
+ }
68
+ const matchedFilesSet = new Set();
69
+ const matchCounts = new Map();
70
+ if (res.status === 0 && res.stdout) {
71
+ const lines = res.stdout.split("\n");
72
+ for (const line of lines) {
73
+ const trimmed = line.trim();
74
+ if (!trimmed)
75
+ continue;
76
+ try {
77
+ const msg = JSON.parse(trimmed);
78
+ if (msg.type === "match") {
79
+ const relPath = msg.data?.path?.text;
80
+ if (relPath) {
81
+ const absPath = (0, path_2.normalizePath)(path_1.default.resolve(plan.root, relPath));
82
+ matchedFilesSet.add(absPath);
83
+ matchCounts.set(absPath, (matchCounts.get(absPath) || 0) + 1);
84
+ }
85
+ }
86
+ }
87
+ catch {
88
+ // Ignore non-json or malformed output lines
89
+ }
90
+ }
91
+ }
92
+ else if (res.status !== 0 && res.status !== 1) {
93
+ errors.push(`Ripgrep error (exit code ${res.status}): ${res.stderr || "unknown error"}`);
94
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "grep", warnings, errors };
95
+ }
96
+ let matchedFiles = Array.from(matchedFilesSet);
97
+ // 3. Filter for AND search (mode === "all")
98
+ if (opts.mode === "all" && opts.terms.length > 1) {
99
+ const remainingTerms = opts.terms.slice(1);
100
+ const filteredMatchedFiles = [];
101
+ for (const file of matchedFiles) {
102
+ try {
103
+ if (fs_1.default.existsSync(file)) {
104
+ const content = fs_1.default.readFileSync(file, "utf8");
105
+ const matchesAll = remainingTerms.every((term) => content.includes(term));
106
+ if (matchesAll) {
107
+ filteredMatchedFiles.push(file);
108
+ }
109
+ else {
110
+ matchCounts.delete(file);
111
+ }
112
+ }
113
+ }
114
+ catch (err) {
115
+ warnings.push(`Failed to read file content for filtering: ${file}. Error: ${err.message}`);
116
+ }
117
+ }
118
+ matchedFiles = filteredMatchedFiles;
119
+ }
120
+ // 4. Apply --not content filter
121
+ if (opts.negativeTerms.length > 0) {
122
+ const filteredMatchedFiles = [];
123
+ for (const file of matchedFiles) {
124
+ try {
125
+ if (fs_1.default.existsSync(file)) {
126
+ const content = fs_1.default.readFileSync(file, "utf8");
127
+ const hasNegative = opts.negativeTerms.some((neg) => content.includes(neg));
128
+ if (!hasNegative) {
129
+ filteredMatchedFiles.push(file);
130
+ }
131
+ else {
132
+ matchCounts.delete(file);
133
+ }
134
+ }
135
+ }
136
+ catch (err) {
137
+ warnings.push(`Failed to read file content for negative filtering: ${file}. Error: ${err.message}`);
138
+ }
139
+ }
140
+ matchedFiles = filteredMatchedFiles;
141
+ }
142
+ // 5. Sort matched files deterministically by normalized root-relative path
143
+ matchedFiles.sort((a, b) => {
144
+ const relA = (0, path_2.normalizePath)(path_1.default.relative(plan.root, a));
145
+ const relB = (0, path_2.normalizePath)(path_1.default.relative(plan.root, b));
146
+ return relA.localeCompare(relB);
147
+ });
148
+ const matchedCountBeforeLimit = matchedFiles.length;
149
+ // 6. Apply --max-files limit to matched files
150
+ const resolvedMaxFiles = plan.maxFiles;
151
+ let maxFilesApplied = false;
152
+ if (resolvedMaxFiles !== undefined && resolvedMaxFiles !== null && matchedFiles.length > resolvedMaxFiles) {
153
+ matchedFiles = matchedFiles.slice(0, resolvedMaxFiles);
154
+ maxFilesApplied = true;
155
+ }
156
+ // 7. Add --include and apply --exclude through buildFinalFileSet
157
+ const finalFiles = await (0, file_set_1.buildFinalFileSet)({
158
+ root: plan.root,
159
+ sources: matchedFiles,
160
+ include: plan.include ?? [],
161
+ exclude: plan.exclude ?? [],
162
+ });
163
+ // 8. No-match behavior validation
164
+ const hasMatchedFiles = matchedCountBeforeLimit > 0;
165
+ const hasIncludes = plan.include && plan.include.length > 0;
166
+ if (!hasMatchedFiles && !hasIncludes) {
167
+ errors.push("No files matched grep search.");
168
+ return { files: [], entries: [], includes: plan.include ?? [], mode: "grep", warnings, errors };
169
+ }
170
+ if (!hasMatchedFiles && hasIncludes) {
171
+ warnings.push("No files matched grep search.");
172
+ }
173
+ // 9. Generate metadata sections
174
+ const sections = [];
175
+ const formatList = (list) => (list.length > 0 ? list.map((item) => `\`${item}\``).join(", ") : "(none)");
176
+ let queryTermsDesc = "";
177
+ if (opts.mode === "regex") {
178
+ queryTermsDesc = `\`${opts.terms[0]}\` (regex)`;
179
+ }
180
+ else if (opts.mode === "query") {
181
+ queryTermsDesc = `\`${opts.terms[0]}\``;
182
+ }
183
+ else {
184
+ queryTermsDesc = opts.terms.map((t) => `\`${t}\``).join(", ");
185
+ }
186
+ const summaryContent = [
187
+ `- **Search Mode**: ${opts.mode}`,
188
+ `- **Query/Terms/Pattern**: ${queryTermsDesc}`,
189
+ `- **Within Boundaries**: ${formatList(opts.within)}`,
190
+ `- **Skip Boundaries**: ${formatList(opts.skip)}`,
191
+ `- **Negative Filters**: ${formatList(opts.negativeTerms)}`,
192
+ `- **Matched File Count (before includes & limits)**: ${matchedCountBeforeLimit}`,
193
+ `- **Max Files Limit**: ${resolvedMaxFiles ?? "(none)"}${maxFilesApplied ? " (applied)" : ""}`,
194
+ `- **Final File Count (after includes/excludes)**: ${finalFiles.length}`,
195
+ ].join("\n");
196
+ sections.push({
197
+ id: "grep-summary",
198
+ title: "Grep Summary",
199
+ kind: "text",
200
+ content: summaryContent,
201
+ });
202
+ const finalFileSet = new Set(finalFiles);
203
+ const finalMatchedFiles = matchedFiles.filter((file) => finalFileSet.has(file));
204
+ const matchesContentList = [];
205
+ for (const file of finalMatchedFiles) {
206
+ const rel = path_1.default.relative(plan.root, file).replace(/\\/g, "/");
207
+ const count = matchCounts.get(file) || 0;
208
+ const countText = count === 1 ? "1 matching line" : `${count} matching lines`;
209
+ matchesContentList.push(`- ${rel} (${countText})`);
210
+ }
211
+ sections.push({
212
+ id: "grep-matches",
213
+ title: "Grep Matches",
214
+ kind: "text",
215
+ content: matchesContentList.length > 0 ? matchesContentList.join("\n") : "(no content matches)",
216
+ });
217
+ return {
218
+ files: finalFiles,
219
+ entries: [],
220
+ includes: plan.include ?? [],
221
+ mode: "grep",
222
+ warnings,
223
+ errors,
224
+ sections,
225
+ };
226
+ }
@@ -0,0 +1,12 @@
1
+ import type { ProdexConfigFile, ExecutionPlan, CommandIntent } from "../types";
2
+ export interface PlannerResult {
3
+ plans: ExecutionPlan[];
4
+ warnings: string[];
5
+ errors: string[];
6
+ listScopes?: string[];
7
+ }
8
+ export declare function createExecutionPlans(params: {
9
+ intent: CommandIntent;
10
+ userConfig: ProdexConfigFile;
11
+ root: string;
12
+ }): PlannerResult;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createExecutionPlans = createExecutionPlans;
4
+ const default_config_1 = require("../config/default-config");
5
+ const pack_plan_1 = require("./planners/pack-plan");
6
+ const trace_plan_1 = require("./planners/trace-plan");
7
+ const scope_plan_1 = require("./planners/scope-plan");
8
+ const git_plan_1 = require("./planners/git-plan");
9
+ const grep_plan_1 = require("./planners/grep-plan");
10
+ const attachment_options_1 = require("./planners/attachment-options");
11
+ function createExecutionPlans(params) {
12
+ const { intent, userConfig, root } = params;
13
+ const warnings = [];
14
+ const errors = [];
15
+ const aliases = userConfig.aliases ?? default_config_1.DEFAULT_PRODEX_CONFIG.aliases;
16
+ const depth = userConfig.depth ?? default_config_1.DEFAULT_PRODEX_CONFIG.depth;
17
+ const maxFiles = userConfig.maxFiles ?? default_config_1.DEFAULT_PRODEX_CONFIG.maxFiles;
18
+ const defaultOutput = {
19
+ dir: userConfig.output?.dir ?? default_config_1.DEFAULT_PRODEX_CONFIG.output.dir,
20
+ versioned: userConfig.output?.versioned ?? default_config_1.DEFAULT_PRODEX_CONFIG.output.versioned,
21
+ format: userConfig.output?.format ?? default_config_1.DEFAULT_PRODEX_CONFIG.output.format,
22
+ };
23
+ const flags = intent.flags ?? {};
24
+ const attachmentOptions = (0, attachment_options_1.parseCommandAttachmentOptions)(flags, errors);
25
+ if (intent.kind === "pack") {
26
+ const plans = (0, pack_plan_1.buildPackPlan)({
27
+ intent,
28
+ userConfig,
29
+ root,
30
+ aliases,
31
+ depth,
32
+ maxFiles,
33
+ defaultOutput,
34
+ attachmentOptions,
35
+ warnings,
36
+ errors,
37
+ });
38
+ return { plans, warnings, errors };
39
+ }
40
+ if (intent.kind === "trace") {
41
+ const plans = (0, trace_plan_1.buildTracePlan)({
42
+ intent,
43
+ userConfig,
44
+ root,
45
+ aliases,
46
+ depth,
47
+ maxFiles,
48
+ defaultOutput,
49
+ attachmentOptions,
50
+ warnings,
51
+ errors,
52
+ });
53
+ return { plans, warnings, errors };
54
+ }
55
+ if (intent.kind === "scope") {
56
+ const scopeResult = (0, scope_plan_1.buildScopePlan)({
57
+ intent,
58
+ userConfig,
59
+ root,
60
+ aliases,
61
+ depth,
62
+ maxFiles,
63
+ defaultOutput,
64
+ attachmentOptions,
65
+ warnings,
66
+ errors,
67
+ });
68
+ return {
69
+ plans: scopeResult.plans,
70
+ warnings,
71
+ errors,
72
+ listScopes: scopeResult.listScopes,
73
+ };
74
+ }
75
+ if (intent.kind === "git") {
76
+ const plans = (0, git_plan_1.buildGitPlan)({
77
+ intent,
78
+ userConfig,
79
+ root,
80
+ aliases,
81
+ depth,
82
+ maxFiles,
83
+ defaultOutput,
84
+ attachmentOptions,
85
+ warnings,
86
+ errors,
87
+ });
88
+ return { plans, warnings, errors };
89
+ }
90
+ if (intent.kind === "grep") {
91
+ const plans = (0, grep_plan_1.buildGrepPlan)({
92
+ intent,
93
+ userConfig,
94
+ root,
95
+ aliases,
96
+ depth,
97
+ maxFiles,
98
+ defaultOutput,
99
+ attachmentOptions,
100
+ warnings,
101
+ errors,
102
+ });
103
+ return { plans, warnings, errors };
104
+ }
105
+ return { plans: [], warnings, errors };
106
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProdexFlags, CommandAttachmentOptions } from "../../types";
2
+ export declare function parseCommandAttachmentOptions(flags: Partial<ProdexFlags>, errors: string[]): CommandAttachmentOptions | undefined;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseCommandAttachmentOptions = parseCommandAttachmentOptions;
4
+ function parseCommandAttachmentOptions(flags, errors) {
5
+ const commands = flags.cmd ?? [];
6
+ const hasCmd = commands.length > 0;
7
+ const hasTimeout = flags.cmdTimeout !== undefined && flags.cmdTimeout !== null;
8
+ const hasFailOnError = flags.failOnCmdError !== undefined;
9
+ // Reject command-specific flags when no command is provided
10
+ if ((hasTimeout || hasFailOnError) && !hasCmd) {
11
+ errors.push("Command attachment options (--cmd-timeout, --fail-on-cmd-error) require providing at least one command using --cmd.");
12
+ }
13
+ // Reject blank commands
14
+ for (const cmd of commands) {
15
+ if (!cmd.trim()) {
16
+ errors.push("Command string specified via --cmd cannot be blank.");
17
+ }
18
+ }
19
+ // Always validate timeout if specified
20
+ let timeoutSeconds = 180;
21
+ if (hasTimeout) {
22
+ const val = flags.cmdTimeout;
23
+ if (typeof val !== "number" || !Number.isFinite(val) || val <= 0) {
24
+ errors.push("Flag --cmd-timeout expects a positive finite number.");
25
+ }
26
+ else {
27
+ timeoutSeconds = val;
28
+ }
29
+ }
30
+ if (commands.length === 0) {
31
+ return undefined;
32
+ }
33
+ const failOnError = !!flags.failOnCmdError;
34
+ return {
35
+ commands,
36
+ timeoutSeconds,
37
+ failOnError,
38
+ };
39
+ }
@@ -0,0 +1,13 @@
1
+ import type { ExecutionPlan, CommandIntent, ProdexConfigFile, CommandAttachmentOptions } from "../../types";
2
+ export declare function buildGitPlan(params: {
3
+ intent: CommandIntent;
4
+ userConfig: ProdexConfigFile;
5
+ root: string;
6
+ aliases: Record<string, string>;
7
+ depth: number;
8
+ maxFiles: number;
9
+ defaultOutput: ExecutionPlan["output"];
10
+ attachmentOptions: CommandAttachmentOptions | undefined;
11
+ warnings: string[];
12
+ errors: string[];
13
+ }): ExecutionPlan[];
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildGitPlan = buildGitPlan;
4
+ const path_patterns_1 = require("../../filesystem/path-patterns");
5
+ const list_utils_1 = require("./list-utils");
6
+ function buildGitPlan(params) {
7
+ const { intent, userConfig, root, aliases, depth, maxFiles, defaultOutput, attachmentOptions, warnings, errors } = params;
8
+ const flags = intent.flags ?? {};
9
+ if (flags.entry !== undefined ||
10
+ flags.scope !== undefined ||
11
+ flags.key !== undefined ||
12
+ flags.all !== undefined ||
13
+ flags.list !== undefined ||
14
+ flags.depth !== undefined ||
15
+ flags.maxFiles !== undefined) {
16
+ errors.push('Command "git" does not accept "--entry", "--scope", "--key", "--all", "--list", "--depth", or "--max-files".');
17
+ return [];
18
+ }
19
+ const rootExclude = userConfig.exclude ?? [];
20
+ const mergedInclude = (flags.include ?? []).map((item) => (0, path_patterns_1.normalizePathOrGlob)(item, root, { role: "include" }));
21
+ const mergedExclude = [...rootExclude, ...(flags.exclude ?? [])].map((item) => (0, path_patterns_1.normalizePathOrGlob)(item, root, { role: "exclude" }));
22
+ const hasGroupB = flags.commit !== undefined || flags.range !== undefined || flags.against !== undefined;
23
+ const hasGroupA = flags.changed !== undefined || flags.staged !== undefined || flags.unstaged !== undefined || flags.untracked !== undefined;
24
+ // Check mutual exclusivity within Group B
25
+ const activeGroupBFlags = [];
26
+ if (flags.commit !== undefined)
27
+ activeGroupBFlags.push("--commit");
28
+ if (flags.range !== undefined)
29
+ activeGroupBFlags.push("--range");
30
+ if (flags.against !== undefined)
31
+ activeGroupBFlags.push("--against");
32
+ if (activeGroupBFlags.length > 1) {
33
+ errors.push("--commit, --range, and --against are mutually exclusive.");
34
+ return [];
35
+ }
36
+ // Check compatibility between Group A and Group B
37
+ if (hasGroupB && hasGroupA) {
38
+ errors.push("--commit/--range/--against cannot be combined with --changed, --staged, --unstaged, or --untracked.");
39
+ return [];
40
+ }
41
+ let gitOptions;
42
+ let defaultOutputName = "git-changes";
43
+ function sanitizeName(name) {
44
+ return name.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-");
45
+ }
46
+ if (flags.commit !== undefined) {
47
+ const rev = flags.commit.trim();
48
+ if (!rev) {
49
+ errors.push("--commit requires a non-empty revision.");
50
+ return [];
51
+ }
52
+ gitOptions = {
53
+ mode: "commit",
54
+ rev,
55
+ includeDiff: !!flags.includeDiff,
56
+ };
57
+ defaultOutputName = `git-commit-${sanitizeName(rev.slice(0, 8))}`;
58
+ }
59
+ else if (flags.range !== undefined) {
60
+ const spec = flags.range.trim();
61
+ const match = spec.match(/^(.+?)(\.{2,3})(.+)$/);
62
+ if (!match) {
63
+ errors.push('Invalid range format. Expected "base..head" or "base...head".');
64
+ return [];
65
+ }
66
+ const base = match[1];
67
+ const separator = match[2];
68
+ const head = match[3];
69
+ if (!base || !head) {
70
+ errors.push('Invalid range format. Expected "base..head" or "base...head".');
71
+ return [];
72
+ }
73
+ gitOptions = {
74
+ mode: "range",
75
+ spec,
76
+ base,
77
+ head,
78
+ includeDiff: !!flags.includeDiff,
79
+ };
80
+ defaultOutputName = "git-range";
81
+ }
82
+ else if (flags.against !== undefined) {
83
+ const base = flags.against.trim();
84
+ if (!base) {
85
+ errors.push("--against requires a non-empty base branch/commit.");
86
+ return [];
87
+ }
88
+ gitOptions = {
89
+ mode: "against",
90
+ base,
91
+ includeDiff: !!flags.includeDiff,
92
+ };
93
+ defaultOutputName = `git-against-${sanitizeName(base)}`;
94
+ }
95
+ else {
96
+ // Working-tree mode
97
+ const changed = !!flags.changed;
98
+ let staged = !!flags.staged;
99
+ let unstaged = !!flags.unstaged;
100
+ let untracked = !!flags.untracked;
101
+ if (!changed && !staged && !unstaged && !untracked) {
102
+ staged = true;
103
+ unstaged = true;
104
+ untracked = true;
105
+ }
106
+ else if (changed) {
107
+ staged = true;
108
+ unstaged = true;
109
+ untracked = true;
110
+ }
111
+ gitOptions = {
112
+ mode: "working-tree",
113
+ changed: changed || (!staged && !unstaged && !untracked),
114
+ staged,
115
+ unstaged,
116
+ untracked,
117
+ includeDiff: !!flags.includeDiff,
118
+ };
119
+ defaultOutputName = "git-changes";
120
+ }
121
+ const plan = {
122
+ root,
123
+ command: "git",
124
+ outputName: flags.name ?? defaultOutputName,
125
+ entry: [],
126
+ include: (0, list_utils_1.uniqueTrimmed)(mergedInclude),
127
+ exclude: (0, list_utils_1.uniqueTrimmed)(mergedExclude),
128
+ depth,
129
+ maxFiles,
130
+ aliases,
131
+ output: {
132
+ dir: defaultOutput.dir,
133
+ versioned: defaultOutput.versioned,
134
+ format: flags.format ?? defaultOutput.format,
135
+ },
136
+ dryRun: !!flags.dryRun,
137
+ attachmentOptions,
138
+ gitOptions,
139
+ };
140
+ return [plan];
141
+ }
@@ -0,0 +1,13 @@
1
+ import type { ExecutionPlan, CommandIntent, ProdexConfigFile, CommandAttachmentOptions } from "../../types";
2
+ export declare function buildGrepPlan(params: {
3
+ intent: CommandIntent;
4
+ userConfig: ProdexConfigFile;
5
+ root: string;
6
+ aliases: Record<string, string>;
7
+ depth: number;
8
+ maxFiles: number;
9
+ defaultOutput: ExecutionPlan["output"];
10
+ attachmentOptions: CommandAttachmentOptions | undefined;
11
+ warnings: string[];
12
+ errors: string[];
13
+ }): ExecutionPlan[];
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildGrepPlan = buildGrepPlan;
4
+ const path_patterns_1 = require("../../filesystem/path-patterns");
5
+ const list_utils_1 = require("./list-utils");
6
+ function buildGrepPlan(params) {
7
+ const { intent, userConfig, root, aliases, depth, maxFiles, defaultOutput, attachmentOptions, warnings, errors } = params;
8
+ const flags = intent.flags ?? {};
9
+ // Validate unsupported options
10
+ if (flags.entry !== undefined || flags.target !== undefined || flags.scope !== undefined || flags.key !== undefined || flags.all !== undefined || flags.list !== undefined) {
11
+ errors.push('Command "grep" does not accept "--entry", "--target", "--scope", "--key", "--all", or "--list".');
12
+ return [];
13
+ }
14
+ // Mode validation
15
+ const modesCount = (flags.query !== undefined ? 1 : 0) +
16
+ (flags.any !== undefined ? 1 : 0) +
17
+ (flags.grepAll !== undefined ? 1 : 0) +
18
+ (flags.regex !== undefined ? 1 : 0);
19
+ if (modesCount === 0) {
20
+ errors.push('Command "grep" requires one search mode: --query, --any, --all, or --regex.');
21
+ return [];
22
+ }
23
+ else if (modesCount > 1) {
24
+ errors.push('Command "grep" accepts only one positive search mode.');
25
+ return [];
26
+ }
27
+ let mode = "query";
28
+ let terms = [];
29
+ if (flags.query !== undefined) {
30
+ mode = "query";
31
+ if (!flags.query.trim()) {
32
+ errors.push("Search terms cannot be blank.");
33
+ }
34
+ else {
35
+ terms = [flags.query];
36
+ }
37
+ }
38
+ else if (flags.any !== undefined) {
39
+ mode = "any";
40
+ if (flags.any.length === 0 || flags.any.some(t => !t.trim())) {
41
+ errors.push("Search terms cannot be blank.");
42
+ }
43
+ else {
44
+ terms = flags.any;
45
+ }
46
+ }
47
+ else if (flags.grepAll !== undefined) {
48
+ mode = "all";
49
+ if (flags.grepAll.length === 0 || flags.grepAll.some(t => !t.trim())) {
50
+ errors.push("Search terms cannot be blank.");
51
+ }
52
+ else {
53
+ terms = flags.grepAll;
54
+ }
55
+ }
56
+ else if (flags.regex !== undefined) {
57
+ mode = "regex";
58
+ if (!flags.regex.trim()) {
59
+ errors.push("Search terms cannot be blank.");
60
+ }
61
+ else {
62
+ terms = [flags.regex];
63
+ }
64
+ }
65
+ let negativeTerms = [];
66
+ if (flags.not !== undefined) {
67
+ if (flags.not.length === 0 || flags.not.some(t => !t.trim())) {
68
+ errors.push("Search terms cannot be blank.");
69
+ }
70
+ else {
71
+ negativeTerms = flags.not;
72
+ }
73
+ }
74
+ // Invalid max files validation
75
+ const resolvedMaxFiles = flags.maxFiles !== undefined ? flags.maxFiles : maxFiles;
76
+ if (resolvedMaxFiles !== undefined && resolvedMaxFiles !== null) {
77
+ if (!Number.isInteger(resolvedMaxFiles) || resolvedMaxFiles <= 0) {
78
+ errors.push("--max-files must be an integer greater than 0.");
79
+ }
80
+ }
81
+ if (errors.length) {
82
+ return [];
83
+ }
84
+ const rootExclude = userConfig.exclude ?? [];
85
+ const mergedWithin = (flags.within ?? []).map((item) => (0, path_patterns_1.normalizePathOrGlob)(item, root, { role: "within" }));
86
+ const mergedSkip = (flags.skip ?? []).map((item) => (0, path_patterns_1.normalizePathOrGlob)(item, root, { role: "skip" }));
87
+ const mergedInclude = (flags.include ?? []).map((item) => (0, path_patterns_1.normalizePathOrGlob)(item, root, { role: "include" }));
88
+ const mergedExclude = [...rootExclude, ...(flags.exclude ?? [])].map((item) => (0, path_patterns_1.normalizePathOrGlob)(item, root, { role: "exclude" }));
89
+ const plan = {
90
+ root,
91
+ command: "grep",
92
+ outputName: flags.name ?? "grep-results",
93
+ entry: [],
94
+ include: (0, list_utils_1.uniqueTrimmed)(mergedInclude),
95
+ exclude: (0, list_utils_1.uniqueTrimmed)(mergedExclude),
96
+ depth: flags.depth ?? depth,
97
+ maxFiles: flags.maxFiles ?? maxFiles,
98
+ aliases,
99
+ output: {
100
+ dir: defaultOutput.dir,
101
+ versioned: defaultOutput.versioned,
102
+ format: flags.format ?? defaultOutput.format,
103
+ },
104
+ dryRun: !!flags.dryRun,
105
+ attachmentOptions,
106
+ grepOptions: {
107
+ mode,
108
+ terms,
109
+ negativeTerms: (0, list_utils_1.uniqueTrimmed)(negativeTerms),
110
+ within: (0, list_utils_1.uniqueTrimmed)(mergedWithin),
111
+ skip: (0, list_utils_1.uniqueTrimmed)(mergedSkip),
112
+ },
113
+ };
114
+ return [plan];
115
+ }
@@ -0,0 +1 @@
1
+ export declare function uniqueTrimmed(items: string[]): string[];
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uniqueTrimmed = uniqueTrimmed;
4
+ function uniqueTrimmed(items) {
5
+ return [...new Set(items.map((item) => item.trim()).filter(Boolean))];
6
+ }