@claudiu-ceia/spatch 0.3.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 +187 -114
  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 +22 -25
  22. package/dist/command.js +36 -334
  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 +82 -484
  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,183 +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
- # patch document from stdin
29
- cat rules/const-to-let.spatch | spatch - src
33
+ In this monorepo during development:
30
34
 
31
- # inline patch document
32
- spatch $'-const :[name] = :[value];\n+let :[name] = :[value];' src
35
+ ```bash
36
+ bun run spatch -- --help
37
+ ```
33
38
 
34
- # preview only
35
- spatch rules/const-to-let.spatch src --dry-run
39
+ ## Quickstart
36
40
 
37
- # structured JSON output
38
- spatch rules/const-to-let.spatch src --json
41
+ Create a patch file:
39
42
 
40
- # interactive apply mode
41
- spatch rules/const-to-let.spatch src --interactive
43
+ ```spatch
44
+ -const :[name] = :[value];
45
+ +let :[name] = :[value];
42
46
  ```
43
47
 
44
- ## API
48
+ Preview:
45
49
 
46
- ```ts
47
- import { patchProject } from "@claudiu-ceia/spatch";
50
+ ```bash
51
+ spatch rules/const-to-let.spatch src --dry-run
52
+ ```
48
53
 
49
- await patchProject(patchInput, {
50
- cwd: "/repo", // optional, default process.cwd()
51
- scope: "src", // file or directory, default "."
52
- dryRun: false, // default false
53
- encoding: "utf8", // default utf8
54
- });
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
55
64
  ```
56
65
 
57
- `patchInput` can be:
58
- - a patch document string
59
- - 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 `+`
60
74
 
61
- ## Patch Document Grammar
75
+ At least one `-` or `+` line is required.
62
76
 
63
- ### Line kinds
77
+ If the patch document ends with a trailing newline, generated pattern/replacement keep that trailing newline.
64
78
 
65
- - `-...`: deletion line (belongs to match pattern only)
66
- - `+...`: addition line (belongs to replacement only)
67
- - ` ...`: context line (belongs to both pattern and replacement)
68
- - `\-...` and `\+...`: escaped marker lines, treated as literal context starting with `-` or `+`
79
+ You can pass patch input as:
69
80
 
70
- ### Minimum change rule
81
+ - inline patch text
82
+ - a patch file path
83
+ - `-` to read from stdin
71
84
 
72
- 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
73
90
 
74
- ### Newline behavior
91
+ # stdin input
92
+ cat rules/rename.spatch | spatch - src
75
93
 
76
- 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
+ ```
77
97
 
78
98
  ## Metavariables
79
99
 
80
- Inside pattern/replacement text, holes use this syntax:
100
+ Supported placeholders:
81
101
 
82
- - `:[name]`
83
- - `:[_]` (anonymous hole, not captured)
84
- - `:[name~regex]` (capture must satisfy regex)
85
- - `...` (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
86
106
 
87
107
  Examples:
88
108
 
89
- ```text
90
- -const :[name] = :[value];
109
+ ```spatch
110
+ -const :[name~[a-zA-Z_$][\w$]*] = :[value];
91
111
  +let :[name] = :[value];
92
112
  ```
93
113
 
94
- ```text
95
- -const :[name~[a-z]+] = :[value~\d+];
96
- +let :[name] = Number(:[value]);
114
+ ```spatch
115
+ -transform(:[input], :[config], ...);
116
+ +normalize(:[input], :[config], ...);
97
117
  ```
98
118
 
99
- Repeated holes enforce equality:
119
+ Repeated names enforce equality:
100
120
 
101
- ```text
102
- -:[x] + :[x];
103
- +double(:[x]);
121
+ ```spatch
122
+ -:[x] + :[x]
123
+ +double(:[x])
104
124
  ```
105
125
 
106
- `foo + foo` matches, `foo + bar` does not.
126
+ Regex constraint safety limits:
107
127
 
108
- 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
109
131
 
110
- ```text
111
- -foo(:[x], ...);
112
- +bar(:[x], ...);
113
- ```
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
114
139
 
115
- Rewrites the callee and preserves remaining arguments.
140
+ Formatting behavior:
116
141
 
117
- ## 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
118
144
 
119
- ### 1) Parse phase
145
+ Example: a single-line pattern can match multi-line code without reformatting the whole block.
120
146
 
121
- `patchProject` resolves `patchInput` into a patch document and parses it into:
122
- - `pattern`
123
- - `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);
124
157
 
125
- ### 2) Rewrite phase
158
+ // after spatch
159
+ const call = normalize(source /* keep comment */, cfg, optA, optB);
160
+ ```
126
161
 
127
- For each scoped file:
128
- - compile template tokens from `pattern`
129
- - find all structural matches
130
- - render `replacement` with captures
131
- - apply replacements
132
- - optionally write file (skipped in `dryRun`)
162
+ ## CLI
133
163
 
134
- ### 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
+ ```
135
168
 
136
- Return an aggregate result:
137
- - files scanned/matched/changed
138
- - match and replacement counts
139
- - elapsed time
140
- - per-file occurrences with spans and captures
169
+ Flags:
141
170
 
142
- ## 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)
143
179
 
144
- Hole captures are checked for structural balance to avoid malformed partial captures.
180
+ Notes:
145
181
 
146
- Balanced constructs supported in capture chunks:
147
- - parentheses `(...)`
148
- - brackets `[...]`
149
- - braces `{...}`
150
- - single/double/template strings
151
- - line and block comments
182
+ - `--interactive` cannot be combined with `--dry-run` or `--check`
183
+ - run `spatch --help` for the generated stricli help text
152
184
 
153
- ## End-to-End Example
185
+ ## Output modes
154
186
 
155
- Patch document:
187
+ Default output is compact diff-style text plus a summary.
156
188
 
157
189
  ```text
158
- function wrap() {
159
- - const value = :[value];
160
- + let value = :[value];
161
- return value;
162
- }
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)
163
197
  ```
164
198
 
165
- 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
166
216
 
167
217
  ```ts
168
- 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);
169
231
  ```
170
232
 
171
- ## Flow Diagram
233
+ `patchInput` can be patch text or a patch file path.
172
234
 
173
- ```text
174
- patchProject(patchInput, options)
175
- -> parsePatchInvocation
176
- -> resolve patch text (inline or file)
177
- -> parsePatchDocument (+/-/context)
178
- -> rewriteProject
179
- -> compileTemplate(pattern)
180
- -> collect files
181
- -> for each file: match -> render -> apply -> (write)
182
- -> 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
183
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>;