@claudiu-ceia/spatch 0.2.0 → 0.3.1

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 (64) hide show
  1. package/README.md +188 -112
  2. package/dist/app.d.ts +1 -0
  3. package/dist/app.js +28 -0
  4. package/dist/cli.js +3 -119
  5. package/dist/command/flags.d.ts +64 -0
  6. package/dist/command/flags.js +73 -0
  7. package/dist/command/interactive/path-resolution.d.ts +5 -0
  8. package/dist/command/interactive/path-resolution.js +55 -0
  9. package/dist/command/interactive/run.d.ts +3 -0
  10. package/dist/command/interactive/run.js +166 -0
  11. package/dist/command/interactive/terminal.d.ts +32 -0
  12. package/dist/command/interactive/terminal.js +79 -0
  13. package/dist/command/interactive/types.d.ts +13 -0
  14. package/dist/command/interactive/types.js +0 -0
  15. package/dist/command/interactive/validation.d.ts +2 -0
  16. package/dist/command/interactive/validation.js +19 -0
  17. package/dist/command/interactive.d.ts +1 -0
  18. package/dist/command/interactive.js +1 -0
  19. package/dist/command/output.d.ts +11 -0
  20. package/dist/command/output.js +82 -0
  21. package/dist/command.d.ts +26 -24
  22. package/dist/command.js +52 -322
  23. package/dist/file-write.d.ts +24 -0
  24. package/dist/file-write.js +50 -0
  25. package/dist/index.d.ts +3 -5
  26. package/dist/index.js +2 -3
  27. package/dist/internal/command.d.ts +1 -0
  28. package/dist/internal/command.js +1 -0
  29. package/dist/phases/output.d.ts +2 -1
  30. package/dist/phases/parse.d.ts +2 -2
  31. package/dist/phases/parse.js +6 -6
  32. package/dist/phases/patch-document.d.ts +6 -0
  33. package/dist/{patch-document.js → phases/patch-document.js} +6 -15
  34. package/dist/phases/rewrite.js +128 -33
  35. package/dist/replacement-spans.d.ts +7 -0
  36. package/dist/replacement-spans.js +26 -0
  37. package/dist/spatch.d.ts +0 -1
  38. package/dist/spatch.js +1 -2
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +19 -13
  41. package/src/app.ts +34 -0
  42. package/src/cli.ts +3 -143
  43. package/src/command/flags.ts +85 -0
  44. package/src/command/interactive/path-resolution.ts +72 -0
  45. package/src/command/interactive/run.ts +207 -0
  46. package/src/command/interactive/terminal.ts +134 -0
  47. package/src/command/interactive/types.ts +20 -0
  48. package/src/command/interactive/validation.ts +36 -0
  49. package/src/command/interactive.ts +1 -0
  50. package/src/command/output.ts +109 -0
  51. package/src/command.ts +99 -458
  52. package/src/file-write.ts +80 -0
  53. package/src/index.ts +3 -21
  54. package/src/internal/command.ts +1 -0
  55. package/src/phases/output.ts +1 -1
  56. package/src/phases/parse.ts +7 -7
  57. package/src/{patch-document.ts → phases/patch-document.ts} +16 -30
  58. package/src/phases/rewrite.ts +177 -53
  59. package/src/replacement-spans.ts +37 -0
  60. package/src/spatch.ts +1 -6
  61. package/dist/patch-document.d.ts +0 -9
  62. package/dist/template.d.ts +0 -2
  63. package/dist/template.js +0 -1
  64. package/src/template.ts +0 -2
package/README.md CHANGED
@@ -1,180 +1,256 @@
1
1
  # spatch
2
2
 
3
- `spatch` applies structural rewrites using a patch document.
3
+ Deterministic structural rewrites for TypeScript/JavaScript using a compact patch-document format.
4
4
 
5
- A patch document is a text block where:
6
- - `-` lines define what to match
7
- - `+` lines define what to insert
8
- - other lines are context shared by both sides
5
+ `spatch` lets you describe a code change once and apply it safely across a scoped project.
9
6
 
10
- You can pass the patch document either:
11
- - inline as a string
12
- - as a file path
7
+ ## Contents
13
8
 
14
- ## CLI
9
+ - [Install](#install)
10
+ - [Quickstart](#quickstart)
11
+ - [Patch document format](#patch-document-format)
12
+ - [Metavariables](#metavariables)
13
+ - [Matching and formatting behavior](#matching-and-formatting-behavior)
14
+ - [CLI](#cli)
15
+ - [Output modes](#output-modes)
16
+ - [Scope and safety model](#scope-and-safety-model)
17
+ - [API](#api)
18
+ - [Caveats](#caveats)
19
+ - [Development](#development)
20
+
21
+ ## Install
15
22
 
16
23
  ```bash
17
- spatch <patch-input> [scope] [--cwd <path>] [--dry-run] [--json] [--no-color] [--interactive]
18
- # or:
19
- astkit patch <patch-input> [scope] [--cwd <path>] [--dry-run] [--json] [--no-color] [--interactive]
24
+ npm install --save-dev @claudiu-ceia/spatch
20
25
  ```
21
26
 
22
- Examples:
27
+ Or run directly:
23
28
 
24
29
  ```bash
25
- # patch document from file
26
- spatch rules/const-to-let.spatch src --cwd /repo
30
+ npx @claudiu-ceia/spatch --help
31
+ ```
27
32
 
28
- # inline patch document
29
- spatch $'-const :[name] = :[value];\n+let :[name] = :[value];' src
33
+ In this monorepo during development:
30
34
 
31
- # preview only
32
- spatch rules/const-to-let.spatch src --dry-run
35
+ ```bash
36
+ bun run spatch -- --help
37
+ ```
33
38
 
34
- # structured JSON output
35
- spatch rules/const-to-let.spatch src --json
39
+ ## Quickstart
36
40
 
37
- # interactive apply mode
38
- spatch rules/const-to-let.spatch src --interactive
41
+ Create a patch file:
42
+
43
+ ```spatch
44
+ -const :[name] = :[value];
45
+ +let :[name] = :[value];
39
46
  ```
40
47
 
41
- ## API
48
+ Preview:
42
49
 
43
- ```ts
44
- import { patchProject } from "@claudiu-ceia/spatch";
50
+ ```bash
51
+ spatch rules/const-to-let.spatch src --dry-run
52
+ ```
45
53
 
46
- await patchProject(patchInput, {
47
- cwd: "/repo", // optional, default process.cwd()
48
- scope: "src", // file or directory, default "."
49
- dryRun: false, // default false
50
- encoding: "utf8", // default utf8
51
- });
54
+ Apply:
55
+
56
+ ```bash
57
+ spatch rules/const-to-let.spatch src
58
+ ```
59
+
60
+ CI guardrail:
61
+
62
+ ```bash
63
+ spatch rules/const-to-let.spatch src --check
52
64
  ```
53
65
 
54
- `patchInput` can be:
55
- - a patch document string
56
- - a path to a patch file (resolved from `cwd`)
66
+ ## Patch document format
67
+
68
+ A patch document is line-based:
69
+
70
+ - `-...` deletion line: belongs to the match pattern only
71
+ - `+...` addition line: belongs to the replacement only
72
+ - ` ...` context line: shared by both pattern and replacement
73
+ - `\-...` and `\+...` escaped markers: treated as literal context starting with `-` or `+`
57
74
 
58
- ## Patch Document Grammar
75
+ At least one `-` or `+` line is required.
59
76
 
60
- ### Line kinds
77
+ If the patch document ends with a trailing newline, generated pattern/replacement keep that trailing newline.
61
78
 
62
- - `-...`: deletion line (belongs to match pattern only)
63
- - `+...`: addition line (belongs to replacement only)
64
- - ` ...`: context line (belongs to both pattern and replacement)
65
- - `\-...` and `\+...`: escaped marker lines, treated as literal context starting with `-` or `+`
79
+ You can pass patch input as:
66
80
 
67
- ### Minimum change rule
81
+ - inline patch text
82
+ - a patch file path
83
+ - `-` to read from stdin
68
84
 
69
- A patch document must contain at least one `-` line or one `+` line.
85
+ Examples:
86
+
87
+ ```bash
88
+ # file input
89
+ spatch rules/rename.spatch src
70
90
 
71
- ### Newline behavior
91
+ # stdin input
92
+ cat rules/rename.spatch | spatch - src
72
93
 
73
- If the patch document ends with a trailing newline, both generated `pattern` and `replacement` preserve it.
94
+ # inline input (bash/zsh)
95
+ spatch $'-foo(:[x])\n+bar(:[x])' src
96
+ ```
74
97
 
75
98
  ## Metavariables
76
99
 
77
- Inside pattern/replacement text, holes use this syntax:
100
+ Supported placeholders:
78
101
 
79
- - `:[name]`
80
- - `:[_]` (anonymous hole, not captured)
81
- - `:[name~regex]` (capture must satisfy regex)
82
- - `...` (variadic wildcard; captured and reusable in replacement)
102
+ - `:[name]` named capture
103
+ - `:[_]` anonymous wildcard (not captured)
104
+ - `:[name~regex]` named capture constrained by regex
105
+ - `...` variadic wildcard, reusable in replacement
83
106
 
84
107
  Examples:
85
108
 
86
- ```text
87
- -const :[name] = :[value];
109
+ ```spatch
110
+ -const :[name~[a-zA-Z_$][\w$]*] = :[value];
88
111
  +let :[name] = :[value];
89
112
  ```
90
113
 
91
- ```text
92
- -const :[name~[a-z]+] = :[value~\d+];
93
- +let :[name] = Number(:[value]);
114
+ ```spatch
115
+ -transform(:[input], :[config], ...);
116
+ +normalize(:[input], :[config], ...);
94
117
  ```
95
118
 
96
- Repeated holes enforce equality:
119
+ Repeated names enforce equality:
97
120
 
98
- ```text
99
- -:[x] + :[x];
100
- +double(:[x]);
121
+ ```spatch
122
+ -:[x] + :[x]
123
+ +double(:[x])
101
124
  ```
102
125
 
103
- `foo + foo` matches, `foo + bar` does not.
126
+ Regex constraint safety limits:
104
127
 
105
- Variadic example:
128
+ - max regex constraint length: `256` characters
129
+ - disallowed in constraints: lookarounds, backreferences, nested quantified groups (for example `([a-z]+)+`)
130
+ - constrained captures longer than `2048` characters are rejected during matching
106
131
 
107
- ```text
108
- -foo(:[x], ...);
109
- +bar(:[x], ...);
110
- ```
132
+ ## Matching and formatting behavior
133
+
134
+ `spatch` matches structurally, not by raw text equality.
135
+
136
+ - matching is trivia-insensitive between lexemes (whitespace/comments can differ)
137
+ - captures are structurally balanced (parens/brackets/braces, strings, comments)
138
+ - `...` captures variadic middle segments
111
139
 
112
- Rewrites the callee and preserves remaining arguments.
140
+ Formatting behavior:
113
141
 
114
- ## How It Works
142
+ - if replacement has the same lexical shape, original trivia layout is preserved
143
+ - if replacement changes lexical shape, output follows replacement template layout
115
144
 
116
- ### 1) Parse phase
145
+ Example: a single-line pattern can match multi-line code without reformatting the whole block.
117
146
 
118
- `patchProject` resolves `patchInput` into a patch document and parses it into:
119
- - `pattern`
120
- - `replacement`
147
+ ```spatch
148
+ -transform(:[input], :[config], ...);
149
+ +normalize(:[input], :[config], ...);
150
+ ```
151
+
152
+ Example rewrite result:
153
+
154
+ ```ts
155
+ // source
156
+ const call = transform(source /* keep comment */, cfg, optA, optB);
121
157
 
122
- ### 2) Rewrite phase
158
+ // after spatch
159
+ const call = normalize(source /* keep comment */, cfg, optA, optB);
160
+ ```
123
161
 
124
- For each scoped file:
125
- - compile template tokens from `pattern`
126
- - find all structural matches
127
- - render `replacement` with captures
128
- - apply replacements
129
- - optionally write file (skipped in `dryRun`)
162
+ ## CLI
130
163
 
131
- ### 3) Output phase
164
+ ```bash
165
+ spatch [--concurrency n] [--verbose level] [--interactive] [--json] [--no-color] [--dry-run] [--check] [--cwd path] <patch> [scope]
166
+ spatch --help
167
+ ```
132
168
 
133
- Return an aggregate result:
134
- - files scanned/matched/changed
135
- - match and replacement counts
136
- - elapsed time
137
- - per-file occurrences with spans and captures
169
+ Flags:
138
170
 
139
- ## Structural Balancing
171
+ - `--dry-run`: preview changes without writing files
172
+ - `--check`: fail with non-zero exit if replacements would be made (implies dry-run)
173
+ - `--interactive`: confirm each change (`y/n/a/q`)
174
+ - `--json`: emit structured JSON result
175
+ - `--no-color`: disable colored output
176
+ - `--cwd <path>`: working directory used to resolve patch input and scope
177
+ - `--concurrency <n>`: max files processed in parallel (default `8`)
178
+ - `--verbose <level>`: perf tracing (`1` summary, `2` includes slow files)
140
179
 
141
- Hole captures are checked for structural balance to avoid malformed partial captures.
180
+ Notes:
142
181
 
143
- Balanced constructs supported in capture chunks:
144
- - parentheses `(...)`
145
- - brackets `[...]`
146
- - braces `{...}`
147
- - single/double/template strings
148
- - line and block comments
182
+ - `--interactive` cannot be combined with `--dry-run` or `--check`
183
+ - run `spatch --help` for the generated stricli help text
149
184
 
150
- ## End-to-End Example
185
+ ## Output modes
151
186
 
152
- Patch document:
187
+ Default output is compact diff-style text plus a summary.
153
188
 
154
189
  ```text
155
- function wrap() {
156
- - const value = :[value];
157
- + let value = :[value];
158
- return value;
159
- }
190
+ diff --git a/src/a.ts b/src/a.ts
191
+ --- a/src/a.ts
192
+ +++ b/src/a.ts
193
+ @@ -10,1 +10,1 @@
194
+ -foo(x)
195
+ +bar(x)
196
+ 1 file changed, 1 replacement, (dry-run)
160
197
  ```
161
198
 
162
- Call:
199
+ `--json` returns the full `SpatchResult` object for automation.
200
+
201
+ ## Scope and safety model
202
+
203
+ Scope boundary:
204
+
205
+ - if `cwd` is inside a git repository, scope must stay within the nearest repo root
206
+ - if no git repo root is found, scope must stay within `cwd`
207
+
208
+ Write safety:
209
+
210
+ - non-interactive apply uses stale-content checks and atomic temp-file rename writes
211
+ - interactive mode re-validates selected spans, then writes through the same stale-safe atomic path
212
+
213
+ This makes `--check` suitable for CI and agent workflows.
214
+
215
+ ## API
163
216
 
164
217
  ```ts
165
- await patchProject("rules/wrap.spatch", { cwd: "/repo", scope: "src" });
218
+ import { patchProject } from "@claudiu-ceia/spatch";
219
+
220
+ const result = await patchProject("rules/const-to-let.spatch", {
221
+ cwd: "/repo",
222
+ scope: "src",
223
+ dryRun: true,
224
+ encoding: "utf8",
225
+ concurrency: 8,
226
+ verbose: 1,
227
+ logger: console.error,
228
+ });
229
+
230
+ console.log(result.totalReplacements);
166
231
  ```
167
232
 
168
- ## Flow Diagram
233
+ `patchInput` can be patch text or a patch file path.
169
234
 
170
- ```text
171
- patchProject(patchInput, options)
172
- -> parsePatchInvocation
173
- -> resolve patch text (inline or file)
174
- -> parsePatchDocument (+/-/context)
175
- -> rewriteProject
176
- -> compileTemplate(pattern)
177
- -> collect files
178
- -> for each file: match -> render -> apply -> (write)
179
- -> buildSpatchResult
235
+ Exports:
236
+
237
+ - `patchProject`
238
+ - `DEFAULT_PATCHABLE_EXTENSIONS`, `DEFAULT_EXCLUDED_DIRECTORIES`
239
+
240
+ ## Caveats
241
+
242
+ - matching is syntactic/structural, not semantic/type-aware
243
+ - comments/whitespace are preserved by lexical slot; when reordering captures, inline comments follow slot position
244
+ - very broad patterns can have large blast radius, use `--dry-run` and optionally `--interactive` first
245
+
246
+ ## Development
247
+
248
+ From monorepo root:
249
+
250
+ ```bash
251
+ bun run spatch -- --help
252
+ bun run spatch -- '<patch-input>' [scope]
253
+ npm run test:spatch
254
+ npm run test:spatch:coverage
255
+ npm run typecheck
180
256
  ```
package/dist/app.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const app: import("@stricli/core").Application<import("@stricli/core").CommandContext>;
package/dist/app.js ADDED
@@ -0,0 +1,28 @@
1
+ import { buildApplication, text_en } from "@stricli/core";
2
+ import { patchCommand } from "./command.js";
3
+ function formatCommandException(exc) {
4
+ if (exc instanceof Error) {
5
+ return exc.message.length > 0 ? `Error: ${exc.message}` : "Error";
6
+ }
7
+ return String(exc);
8
+ }
9
+ const text = {
10
+ ...text_en,
11
+ exceptionWhileParsingArguments: (exc) => `Unable to parse arguments, ${formatCommandException(exc)}`,
12
+ exceptionWhileLoadingCommandFunction: (exc) => `Unable to load command function, ${formatCommandException(exc)}`,
13
+ exceptionWhileLoadingCommandContext: (exc) => `Unable to load command context, ${formatCommandException(exc)}`,
14
+ exceptionWhileRunningCommand: (exc) => `Command failed, ${formatCommandException(exc)}`,
15
+ };
16
+ export const app = buildApplication(patchCommand, {
17
+ name: "spatch",
18
+ scanner: {
19
+ caseStyle: "original",
20
+ },
21
+ documentation: {
22
+ caseStyle: "original",
23
+ },
24
+ localization: {
25
+ defaultLocale: "en",
26
+ loadText: () => text,
27
+ },
28
+ });
package/dist/cli.js CHANGED
@@ -1,120 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { formatPatchOutput, runPatchCommand } from "./command.js";
3
- function parsePositiveInteger(name, raw) {
4
- const value = Number(raw);
5
- if (!Number.isFinite(value) || value <= 0) {
6
- throw new Error(`${name} must be a positive number`);
7
- }
8
- return Math.floor(value);
9
- }
10
- function readFlagValue(argv, index) {
11
- const token = argv[index];
12
- if (!token) {
13
- throw new Error("Missing flag token");
14
- }
15
- const eqIndex = token.indexOf("=");
16
- if (eqIndex >= 0) {
17
- const value = token.slice(eqIndex + 1);
18
- if (value.length === 0) {
19
- throw new Error(`Missing value for ${token.slice(0, eqIndex)}`);
20
- }
21
- return { value, consumed: 1 };
22
- }
23
- const next = argv[index + 1];
24
- if (!next) {
25
- throw new Error(`Missing value for ${token}`);
26
- }
27
- return { value: next, consumed: 2 };
28
- }
29
- function printHelp() {
30
- process.stdout.write([
31
- "spatch - structural patch for TS/JS",
32
- "",
33
- "Usage:",
34
- " spatch [--dry-run] [--interactive] [--json] [--no-color] [--cwd <path>] <patch-input> [scope]",
35
- "",
36
- "Flags:",
37
- " --dry-run Preview changes without writing files",
38
- " --interactive Interactively select which matches to apply",
39
- " --json Output structured JSON",
40
- " --no-color Disable colored output",
41
- " --cwd <path> Working directory for resolving patch and scope",
42
- " --concurrency <n> Max files processed concurrently (default: 8)",
43
- " --verbose <level> Perf tracing to stderr (1=summary, 2=slow files)",
44
- "",
45
- ].join("\n"));
46
- }
47
- const argv = process.argv.slice(2);
48
- if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
49
- printHelp();
50
- process.exit(0);
51
- }
52
- const flags = {};
53
- const positional = [];
54
- for (let i = 0; i < argv.length; i += 1) {
55
- const token = argv[i];
56
- if (!token)
57
- continue;
58
- if (!token.startsWith("-")) {
59
- positional.push(token);
60
- continue;
61
- }
62
- if (token === "--dry-run") {
63
- flags["dry-run"] = true;
64
- continue;
65
- }
66
- if (token === "--interactive") {
67
- flags.interactive = true;
68
- continue;
69
- }
70
- if (token === "--json") {
71
- flags.json = true;
72
- continue;
73
- }
74
- if (token === "--no-color") {
75
- flags["no-color"] = true;
76
- continue;
77
- }
78
- if (token === "--cwd" || token.startsWith("--cwd=")) {
79
- const { value, consumed } = readFlagValue(argv, i);
80
- flags.cwd = value;
81
- i += consumed - 1;
82
- continue;
83
- }
84
- if (token === "--concurrency" || token.startsWith("--concurrency=")) {
85
- const { value, consumed } = readFlagValue(argv, i);
86
- flags.concurrency = parsePositiveInteger("--concurrency", value);
87
- i += consumed - 1;
88
- continue;
89
- }
90
- if (token === "--verbose" || token.startsWith("--verbose=")) {
91
- const { value, consumed } = readFlagValue(argv, i);
92
- const level = Number(value);
93
- if (!Number.isFinite(level) || level < 0) {
94
- throw new Error("--verbose must be a non-negative number");
95
- }
96
- flags.verbose = Math.floor(level);
97
- i += consumed - 1;
98
- continue;
99
- }
100
- throw new Error(`Unknown flag: ${token}`);
101
- }
102
- const patchInput = positional[0];
103
- const scope = positional[1];
104
- if (!patchInput) {
105
- printHelp();
106
- process.exit(1);
107
- }
108
- if (positional.length > 2) {
109
- throw new Error("Too many positional arguments.");
110
- }
111
- const result = await runPatchCommand(patchInput, scope, flags);
112
- if (flags.json ?? false) {
113
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
114
- }
115
- else {
116
- const output = formatPatchOutput(result, {
117
- color: Boolean(process.stdout.isTTY) && !(flags["no-color"] ?? false),
118
- });
119
- process.stdout.write(`${output}\n`);
120
- }
2
+ import { run } from "@stricli/core";
3
+ import { app } from "./app.js";
4
+ await run(app, process.argv.slice(2), { process });
@@ -0,0 +1,64 @@
1
+ export type PatchCommandFlags = {
2
+ "dry-run"?: boolean;
3
+ check?: boolean;
4
+ interactive?: boolean;
5
+ json?: boolean;
6
+ "no-color"?: boolean;
7
+ cwd?: string;
8
+ concurrency?: number;
9
+ verbose?: number;
10
+ };
11
+ export declare const patchCommandFlagParameters: {
12
+ readonly concurrency: {
13
+ readonly kind: "parsed";
14
+ readonly optional: true;
15
+ readonly brief: "Max files processed concurrently (default: 8)";
16
+ readonly placeholder: "n";
17
+ readonly parse: (input: string) => number;
18
+ };
19
+ readonly verbose: {
20
+ readonly kind: "parsed";
21
+ readonly optional: true;
22
+ readonly brief: "Print perf tracing (1=summary, 2=includes slow files)";
23
+ readonly placeholder: "level";
24
+ readonly parse: (input: string) => number;
25
+ };
26
+ readonly interactive: {
27
+ readonly kind: "boolean";
28
+ readonly optional: true;
29
+ readonly withNegated: false;
30
+ readonly brief: "Interactively select which matches to apply";
31
+ };
32
+ readonly json: {
33
+ readonly kind: "boolean";
34
+ readonly optional: true;
35
+ readonly withNegated: false;
36
+ readonly brief: "Output structured JSON instead of compact diff-style text";
37
+ };
38
+ readonly "no-color": {
39
+ readonly kind: "boolean";
40
+ readonly optional: true;
41
+ readonly withNegated: false;
42
+ readonly brief: "Disable colored output";
43
+ };
44
+ readonly "dry-run": {
45
+ readonly kind: "boolean";
46
+ readonly optional: true;
47
+ readonly withNegated: false;
48
+ readonly brief: "Preview changes without writing files";
49
+ };
50
+ readonly check: {
51
+ readonly kind: "boolean";
52
+ readonly optional: true;
53
+ readonly withNegated: false;
54
+ readonly brief: "Exit non-zero if changes would be made (implies --dry-run)";
55
+ };
56
+ readonly cwd: {
57
+ readonly kind: "parsed";
58
+ readonly optional: true;
59
+ readonly brief: "Working directory for resolving patch file and scope";
60
+ readonly placeholder: "path";
61
+ readonly parse: (input: string) => string;
62
+ };
63
+ };
64
+ export declare function validatePatchCommandFlags(flags: PatchCommandFlags): void;
@@ -0,0 +1,73 @@
1
+ export const patchCommandFlagParameters = {
2
+ concurrency: {
3
+ kind: "parsed",
4
+ optional: true,
5
+ brief: "Max files processed concurrently (default: 8)",
6
+ placeholder: "n",
7
+ parse: (input) => {
8
+ const value = Number(input);
9
+ if (!Number.isFinite(value) || value <= 0) {
10
+ throw new Error("--concurrency must be a positive number");
11
+ }
12
+ return Math.floor(value);
13
+ },
14
+ },
15
+ verbose: {
16
+ kind: "parsed",
17
+ optional: true,
18
+ brief: "Print perf tracing (1=summary, 2=includes slow files)",
19
+ placeholder: "level",
20
+ parse: (input) => {
21
+ const value = Number(input);
22
+ if (!Number.isFinite(value) || value < 0) {
23
+ throw new Error("--verbose must be a non-negative number");
24
+ }
25
+ return Math.floor(value);
26
+ },
27
+ },
28
+ interactive: {
29
+ kind: "boolean",
30
+ optional: true,
31
+ withNegated: false,
32
+ brief: "Interactively select which matches to apply",
33
+ },
34
+ json: {
35
+ kind: "boolean",
36
+ optional: true,
37
+ withNegated: false,
38
+ brief: "Output structured JSON instead of compact diff-style text",
39
+ },
40
+ "no-color": {
41
+ kind: "boolean",
42
+ optional: true,
43
+ withNegated: false,
44
+ brief: "Disable colored output",
45
+ },
46
+ "dry-run": {
47
+ kind: "boolean",
48
+ optional: true,
49
+ withNegated: false,
50
+ brief: "Preview changes without writing files",
51
+ },
52
+ check: {
53
+ kind: "boolean",
54
+ optional: true,
55
+ withNegated: false,
56
+ brief: "Exit non-zero if changes would be made (implies --dry-run)",
57
+ },
58
+ cwd: {
59
+ kind: "parsed",
60
+ optional: true,
61
+ brief: "Working directory for resolving patch file and scope",
62
+ placeholder: "path",
63
+ parse: (input) => input,
64
+ },
65
+ };
66
+ export function validatePatchCommandFlags(flags) {
67
+ if ((flags.interactive ?? false) && (flags["dry-run"] ?? false)) {
68
+ throw new Error("Cannot combine --interactive with --dry-run.");
69
+ }
70
+ if ((flags.interactive ?? false) && (flags.check ?? false)) {
71
+ throw new Error("Cannot combine --interactive with --check.");
72
+ }
73
+ }
@@ -0,0 +1,5 @@
1
+ export declare function resolveInteractiveFilePath(file: string, options: {
2
+ cwd: string;
3
+ scope: string;
4
+ scopeKind: "file" | "directory";
5
+ }): Promise<string>;