@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.
- package/README.md +188 -112
- package/dist/app.d.ts +1 -0
- package/dist/app.js +28 -0
- package/dist/cli.js +3 -119
- package/dist/command/flags.d.ts +64 -0
- package/dist/command/flags.js +73 -0
- package/dist/command/interactive/path-resolution.d.ts +5 -0
- package/dist/command/interactive/path-resolution.js +55 -0
- package/dist/command/interactive/run.d.ts +3 -0
- package/dist/command/interactive/run.js +166 -0
- package/dist/command/interactive/terminal.d.ts +32 -0
- package/dist/command/interactive/terminal.js +79 -0
- package/dist/command/interactive/types.d.ts +13 -0
- package/dist/command/interactive/types.js +0 -0
- package/dist/command/interactive/validation.d.ts +2 -0
- package/dist/command/interactive/validation.js +19 -0
- package/dist/command/interactive.d.ts +1 -0
- package/dist/command/interactive.js +1 -0
- package/dist/command/output.d.ts +11 -0
- package/dist/command/output.js +82 -0
- package/dist/command.d.ts +26 -24
- package/dist/command.js +52 -322
- package/dist/file-write.d.ts +24 -0
- package/dist/file-write.js +50 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.js +2 -3
- package/dist/internal/command.d.ts +1 -0
- package/dist/internal/command.js +1 -0
- package/dist/phases/output.d.ts +2 -1
- package/dist/phases/parse.d.ts +2 -2
- package/dist/phases/parse.js +6 -6
- package/dist/phases/patch-document.d.ts +6 -0
- package/dist/{patch-document.js → phases/patch-document.js} +6 -15
- package/dist/phases/rewrite.js +128 -33
- package/dist/replacement-spans.d.ts +7 -0
- package/dist/replacement-spans.js +26 -0
- package/dist/spatch.d.ts +0 -1
- package/dist/spatch.js +1 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -13
- package/src/app.ts +34 -0
- package/src/cli.ts +3 -143
- package/src/command/flags.ts +85 -0
- package/src/command/interactive/path-resolution.ts +72 -0
- package/src/command/interactive/run.ts +207 -0
- package/src/command/interactive/terminal.ts +134 -0
- package/src/command/interactive/types.ts +20 -0
- package/src/command/interactive/validation.ts +36 -0
- package/src/command/interactive.ts +1 -0
- package/src/command/output.ts +109 -0
- package/src/command.ts +99 -458
- package/src/file-write.ts +80 -0
- package/src/index.ts +3 -21
- package/src/internal/command.ts +1 -0
- package/src/phases/output.ts +1 -1
- package/src/phases/parse.ts +7 -7
- package/src/{patch-document.ts → phases/patch-document.ts} +16 -30
- package/src/phases/rewrite.ts +177 -53
- package/src/replacement-spans.ts +37 -0
- package/src/spatch.ts +1 -6
- package/dist/patch-document.d.ts +0 -9
- package/dist/template.d.ts +0 -2
- package/dist/template.js +0 -1
- package/src/template.ts +0 -2
package/README.md
CHANGED
|
@@ -1,180 +1,256 @@
|
|
|
1
1
|
# spatch
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deterministic structural rewrites for TypeScript/JavaScript using a compact patch-document format.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
11
|
-
- inline as a string
|
|
12
|
-
- as a file path
|
|
7
|
+
## Contents
|
|
13
8
|
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
+
Or run directly:
|
|
23
28
|
|
|
24
29
|
```bash
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
npx @claudiu-ceia/spatch --help
|
|
31
|
+
```
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
spatch $'-const :[name] = :[value];\n+let :[name] = :[value];' src
|
|
33
|
+
In this monorepo during development:
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
```bash
|
|
36
|
+
bun run spatch -- --help
|
|
37
|
+
```
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
spatch rules/const-to-let.spatch src --json
|
|
39
|
+
## Quickstart
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
Create a patch file:
|
|
42
|
+
|
|
43
|
+
```spatch
|
|
44
|
+
-const :[name] = :[value];
|
|
45
|
+
+let :[name] = :[value];
|
|
39
46
|
```
|
|
40
47
|
|
|
41
|
-
|
|
48
|
+
Preview:
|
|
42
49
|
|
|
43
|
-
```
|
|
44
|
-
|
|
50
|
+
```bash
|
|
51
|
+
spatch rules/const-to-let.spatch src --dry-run
|
|
52
|
+
```
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
75
|
+
At least one `-` or `+` line is required.
|
|
59
76
|
|
|
60
|
-
|
|
77
|
+
If the patch document ends with a trailing newline, generated pattern/replacement keep that trailing newline.
|
|
61
78
|
|
|
62
|
-
|
|
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
|
-
|
|
81
|
+
- inline patch text
|
|
82
|
+
- a patch file path
|
|
83
|
+
- `-` to read from stdin
|
|
68
84
|
|
|
69
|
-
|
|
85
|
+
Examples:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# file input
|
|
89
|
+
spatch rules/rename.spatch src
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
# stdin input
|
|
92
|
+
cat rules/rename.spatch | spatch - src
|
|
72
93
|
|
|
73
|
-
|
|
94
|
+
# inline input (bash/zsh)
|
|
95
|
+
spatch $'-foo(:[x])\n+bar(:[x])' src
|
|
96
|
+
```
|
|
74
97
|
|
|
75
98
|
## Metavariables
|
|
76
99
|
|
|
77
|
-
|
|
100
|
+
Supported placeholders:
|
|
78
101
|
|
|
79
|
-
- `:[name]`
|
|
80
|
-
- `:[_]`
|
|
81
|
-
- `:[name~regex]`
|
|
82
|
-
- `...`
|
|
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
|
-
```
|
|
87
|
-
-const :[name] = :[value];
|
|
109
|
+
```spatch
|
|
110
|
+
-const :[name~[a-zA-Z_$][\w$]*] = :[value];
|
|
88
111
|
+let :[name] = :[value];
|
|
89
112
|
```
|
|
90
113
|
|
|
91
|
-
```
|
|
92
|
-
-
|
|
93
|
-
+
|
|
114
|
+
```spatch
|
|
115
|
+
-transform(:[input], :[config], ...);
|
|
116
|
+
+normalize(:[input], :[config], ...);
|
|
94
117
|
```
|
|
95
118
|
|
|
96
|
-
Repeated
|
|
119
|
+
Repeated names enforce equality:
|
|
97
120
|
|
|
98
|
-
```
|
|
99
|
-
-:[x] + :[x]
|
|
100
|
-
+double(:[x])
|
|
121
|
+
```spatch
|
|
122
|
+
-:[x] + :[x]
|
|
123
|
+
+double(:[x])
|
|
101
124
|
```
|
|
102
125
|
|
|
103
|
-
|
|
126
|
+
Regex constraint safety limits:
|
|
104
127
|
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
140
|
+
Formatting behavior:
|
|
113
141
|
|
|
114
|
-
|
|
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
|
-
|
|
145
|
+
Example: a single-line pattern can match multi-line code without reformatting the whole block.
|
|
117
146
|
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
|
|
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
|
-
|
|
158
|
+
// after spatch
|
|
159
|
+
const call = normalize(source /* keep comment */, cfg, optA, optB);
|
|
160
|
+
```
|
|
123
161
|
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
+
Notes:
|
|
142
181
|
|
|
143
|
-
|
|
144
|
-
-
|
|
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
|
-
##
|
|
185
|
+
## Output modes
|
|
151
186
|
|
|
152
|
-
|
|
187
|
+
Default output is compact diff-style text plus a summary.
|
|
153
188
|
|
|
154
189
|
```text
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
+
`patchInput` can be patch text or a patch file path.
|
|
169
234
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
}
|