@ripast/cli 0.3.0 → 0.5.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.
- package/bin/ripast.mjs +36 -11
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +210 -137
- package/package.json +9 -10
- package/skills/ripast/SKILL.md +84 -0
package/bin/ripast.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { createRequire } from 'node:module'
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
5
4
|
import { dirname, join } from 'node:path'
|
|
6
5
|
import process from 'node:process'
|
|
6
|
+
import spawn from 'cross-spawn'
|
|
7
7
|
|
|
8
8
|
const FRAMEWORK_MARKERS = {
|
|
9
9
|
vue: ['vue', 'nuxt', '@nuxt/kit'],
|
|
@@ -42,25 +42,50 @@ function ensureAdapters(needed) {
|
|
|
42
42
|
if (!needed.length)
|
|
43
43
|
return
|
|
44
44
|
|
|
45
|
-
const require = createRequire(import.meta.url)
|
|
46
45
|
const missing = []
|
|
47
46
|
for (const name of needed) {
|
|
48
47
|
try {
|
|
49
|
-
|
|
48
|
+
if (!existsSync(new URL(import.meta.resolve(`@ripast/${name}`))))
|
|
49
|
+
missing.push(`@ripast/${name}`)
|
|
50
50
|
}
|
|
51
51
|
catch { missing.push(`@ripast/${name}`) }
|
|
52
52
|
}
|
|
53
53
|
if (!missing.length)
|
|
54
54
|
return
|
|
55
55
|
|
|
56
|
-
const args = ['
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const args = ['dlx', '--package=@ripast/cli', ...missing.map(p => `--package=${p}`), 'ripast', ...process.argv.slice(2)]
|
|
57
|
+
const options = { stdio: 'inherit', env: { ...process.env, RIPAST_REEXEC: '1' } }
|
|
58
|
+
let manager = 'pnpm'
|
|
59
|
+
let res = spawn.sync(manager, args, options)
|
|
60
|
+
if (res.error?.code === 'ENOENT') {
|
|
61
|
+
manager = 'npm'
|
|
62
|
+
// Keep npm's project metadata separate. The executed CLI still uses the caller's cwd.
|
|
63
|
+
const prefix = mkdtempSync(join(tmpdir(), 'ripast-adapters-'))
|
|
64
|
+
try {
|
|
65
|
+
res = spawn.sync(manager, ['exec', '--yes', `--prefix=${prefix}`, '--package=@ripast/cli', ...missing.map(p => `--package=${p}`), '--', 'ripast', ...process.argv.slice(2)], options)
|
|
66
|
+
}
|
|
67
|
+
finally { rmSync(prefix, { recursive: true, force: true }) }
|
|
68
|
+
}
|
|
69
|
+
if (res.error) {
|
|
70
|
+
if (res.error.code === 'ENOENT') {
|
|
71
|
+
process.stderr.write([
|
|
72
|
+
`ripast: Neither pnpm nor npm was found on PATH. Missing adapters: ${missing.join(', ')}.`,
|
|
73
|
+
'If either package manager is installed, add its directory to PATH.',
|
|
74
|
+
'Install pnpm: https://pnpm.io/installation',
|
|
75
|
+
'Then run pnpm --version and retry.',
|
|
76
|
+
'After installing npm, you can install the CLI and adapters together:',
|
|
77
|
+
` npm install -g @ripast/cli ${missing.join(' ')}`,
|
|
78
|
+
'For script-only rename, move, or rename-file commands, retry with --no-vue.',
|
|
79
|
+
'',
|
|
80
|
+
].join('\n'))
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
process.stderr.write(`ripast: Could not start ${manager}: ${res.error.message}\n`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
61
86
|
process.exit(res.status ?? 1)
|
|
62
87
|
}
|
|
63
88
|
|
|
64
|
-
ensureAdapters(detectFrameworks(process.cwd()))
|
|
89
|
+
ensureAdapters(process.argv.includes('--no-vue') ? [] : detectFrameworks(process.cwd()))
|
|
65
90
|
|
|
66
91
|
await import('../dist/cli.mjs')
|
package/dist/cli.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
package/dist/cli.mjs
CHANGED
|
@@ -4,10 +4,79 @@ import process from "node:process";
|
|
|
4
4
|
import { buildComponentDetail, buildComponentInventory, buildDeclarationTree, buildDoctorFixes, buildScanGraph, buildUnusedDeclarations, formatAgentDeclarationTree, formatAgentDoctorReport, formatAgentFileScanHits, formatAgentHits, formatAgentInventory, formatAgentScanHits, formatDeclarationTree, formatDetail, formatDoctorReport, formatFileScanHits, formatHits, formatInventory, formatRegressions, formatScanGraph, formatScanHits, formatUnusedDeclarations, getChangedFiles, printDiffs, resolveVerifyMode, runCssClassFileScan, runCssClassRename, runCssClassScan, runDelete, runDoctor, runMove, runRename, runRenameFile, runReplace, runVueTemplateUnwrap, runVueTemplateWrap, scan, summarize, writeChanges } from "@ripast/core";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
6
6
|
import { agent, isAgent } from "std-env";
|
|
7
|
+
function defineStrictCommand(definition) {
|
|
8
|
+
return defineCommand({
|
|
9
|
+
...definition,
|
|
10
|
+
async setup(context) {
|
|
11
|
+
if (!definition.subCommands) {
|
|
12
|
+
const args = (typeof definition.args === "function" ? await definition.args() : await definition.args) ?? {};
|
|
13
|
+
const allowed = /* @__PURE__ */ new Set(["_"]);
|
|
14
|
+
const options = /* @__PURE__ */ new Set();
|
|
15
|
+
let positionalCount = 0;
|
|
16
|
+
for (const [name, arg] of Object.entries(args)) {
|
|
17
|
+
const alias = "alias" in arg ? arg.alias : void 0;
|
|
18
|
+
const aliases = typeof alias === "string" ? [alias] : alias ?? [];
|
|
19
|
+
for (const key of [name, ...aliases]) {
|
|
20
|
+
const variants = [
|
|
21
|
+
key,
|
|
22
|
+
key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`),
|
|
23
|
+
key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
24
|
+
];
|
|
25
|
+
for (const variant of variants) {
|
|
26
|
+
allowed.add(variant);
|
|
27
|
+
if (arg.type !== "positional") options.add(variant);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (arg.type === "positional") positionalCount++;
|
|
31
|
+
}
|
|
32
|
+
for (const name of Object.keys(context.args)) if (!allowed.has(name)) throw new Error(`Unknown option: --${name}. Run the command with --help.`);
|
|
33
|
+
for (const token of context.rawArgs) {
|
|
34
|
+
if (token === "--") break;
|
|
35
|
+
if (!token.startsWith("--")) continue;
|
|
36
|
+
const name = token.slice(2).split("=")[0].replace(/^no-/, "");
|
|
37
|
+
if (!options.has(name)) throw new Error(`Unknown option: --${name}. Run the command with --help.`);
|
|
38
|
+
}
|
|
39
|
+
const extra = context.args._[positionalCount];
|
|
40
|
+
if (extra !== void 0) throw new Error(`Unexpected positional argument: ${extra}. Run the command with --help.`);
|
|
41
|
+
}
|
|
42
|
+
await definition.setup?.(context);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
7
46
|
const globArg = {
|
|
8
47
|
type: "string",
|
|
9
48
|
description: "File glob(s), comma-separated. Prefix with ! to exclude (e.g. \"*.ts,!.nuxt/**,!**/*.d.ts\"). Defaults to *.ts,*.tsx,*.vue,... Respects .gitignore."
|
|
10
49
|
};
|
|
50
|
+
function splitGlobs(value) {
|
|
51
|
+
const globs = [];
|
|
52
|
+
let start = 0;
|
|
53
|
+
let braces = 0;
|
|
54
|
+
let characterClass = false;
|
|
55
|
+
let escaped = false;
|
|
56
|
+
for (let index = 0; index < value.length; index++) {
|
|
57
|
+
const character = value[index];
|
|
58
|
+
if (escaped) {
|
|
59
|
+
escaped = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (character === "\\") {
|
|
63
|
+
escaped = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (character === "[") characterClass = true;
|
|
67
|
+
else if (character === "]") characterClass = false;
|
|
68
|
+
else if (!characterClass) {
|
|
69
|
+
if (character === "{") braces++;
|
|
70
|
+
else if (character === "}") braces--;
|
|
71
|
+
else if (character === "," && braces === 0) {
|
|
72
|
+
globs.push(value.slice(start, index));
|
|
73
|
+
start = index + 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
globs.push(value.slice(start));
|
|
78
|
+
return globs;
|
|
79
|
+
}
|
|
11
80
|
const applyArg = {
|
|
12
81
|
type: "boolean",
|
|
13
82
|
default: false,
|
|
@@ -25,7 +94,7 @@ const verifyModeArg = {
|
|
|
25
94
|
const vueArg = {
|
|
26
95
|
type: "boolean",
|
|
27
96
|
default: true,
|
|
28
|
-
description: "Enable Volar pass for .vue files. Disable with --no-vue to
|
|
97
|
+
description: "Enable Volar pass for .vue files. Disable with --no-vue to skip the Volar pass."
|
|
29
98
|
};
|
|
30
99
|
const jsonArg = {
|
|
31
100
|
type: "boolean",
|
|
@@ -103,7 +172,7 @@ function resolveExportFilter(raw) {
|
|
|
103
172
|
}
|
|
104
173
|
return raw;
|
|
105
174
|
}
|
|
106
|
-
const scanCmd =
|
|
175
|
+
const scanCmd = defineStrictCommand({
|
|
107
176
|
meta: {
|
|
108
177
|
name: "scan",
|
|
109
178
|
description: "rg-prefilter + AST-classify occurrences of an identifier."
|
|
@@ -131,7 +200,7 @@ const scanCmd = defineCommand({
|
|
|
131
200
|
run({ args }) {
|
|
132
201
|
const { agentProfile } = resolveProfile(args.profile);
|
|
133
202
|
const opts = {
|
|
134
|
-
glob: args.glob ? args.glob
|
|
203
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
135
204
|
kinds: args.kind ? args.kind.split(",") : void 0
|
|
136
205
|
};
|
|
137
206
|
if (args.graph) {
|
|
@@ -147,10 +216,10 @@ const scanCmd = defineCommand({
|
|
|
147
216
|
process.stdout.write(`${formatHits(hits, args.json)}\n`);
|
|
148
217
|
}
|
|
149
218
|
});
|
|
150
|
-
const renameCmd =
|
|
219
|
+
const renameCmd = defineStrictCommand({
|
|
151
220
|
meta: {
|
|
152
221
|
name: "rename",
|
|
153
|
-
description: "Scope-aware symbol rename via
|
|
222
|
+
description: "Scope-aware symbol rename via the native TypeScript server (handles type-only imports, shadowing, JSX)."
|
|
154
223
|
},
|
|
155
224
|
args: {
|
|
156
225
|
from: {
|
|
@@ -183,7 +252,7 @@ const renameCmd = defineCommand({
|
|
|
183
252
|
const verifyMode = resolveCliVerifyMode(args.verify, args.verifyMode);
|
|
184
253
|
emitResult(await runRename(args.from, args.to, {
|
|
185
254
|
tsconfig: args.tsconfig,
|
|
186
|
-
glob: args.glob ? args.glob
|
|
255
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
187
256
|
verify: verifyMode,
|
|
188
257
|
scope: args.scope,
|
|
189
258
|
allowMultiple: args.all,
|
|
@@ -191,7 +260,7 @@ const renameCmd = defineCommand({
|
|
|
191
260
|
}), !!args.apply, verifyMode !== "none", !!args.json, resolveProfile(args.profile).agentProfile);
|
|
192
261
|
}
|
|
193
262
|
});
|
|
194
|
-
const replaceCmd =
|
|
263
|
+
const replaceCmd = defineStrictCommand({
|
|
195
264
|
meta: {
|
|
196
265
|
name: "replace",
|
|
197
266
|
description: "Replace an imported symbol with another project export; updates imports and call sites."
|
|
@@ -205,12 +274,15 @@ const replaceCmd = defineCommand({
|
|
|
205
274
|
type: "positional",
|
|
206
275
|
required: true
|
|
207
276
|
},
|
|
208
|
-
"tsconfig": { type: "string" },
|
|
209
277
|
"glob": globArg,
|
|
210
278
|
"target-scope": {
|
|
211
279
|
type: "string",
|
|
212
280
|
description: "Restrict target symbol resolution to a single file when multiple files export the same name."
|
|
213
281
|
},
|
|
282
|
+
"target-import": {
|
|
283
|
+
type: "string",
|
|
284
|
+
description: "Import specifier for the validated replacement, including framework aliases."
|
|
285
|
+
},
|
|
214
286
|
"apply": applyArg,
|
|
215
287
|
"verify": verifyArg,
|
|
216
288
|
"verifyMode": verifyModeArg,
|
|
@@ -220,14 +292,14 @@ const replaceCmd = defineCommand({
|
|
|
220
292
|
async run({ args }) {
|
|
221
293
|
const verifyMode = resolveCliVerifyMode(args.verify, args.verifyMode);
|
|
222
294
|
emitResult(await runReplace(args.from, args.to, {
|
|
223
|
-
|
|
224
|
-
glob: args.glob ? args.glob.split(",") : void 0,
|
|
295
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
225
296
|
verify: verifyMode,
|
|
226
|
-
targetScope: args["target-scope"]
|
|
297
|
+
targetScope: args["target-scope"],
|
|
298
|
+
targetImport: args["target-import"]
|
|
227
299
|
}), !!args.apply, verifyMode !== "none", !!args.json, resolveProfile(args.profile).agentProfile);
|
|
228
300
|
}
|
|
229
301
|
});
|
|
230
|
-
const treeCmd =
|
|
302
|
+
const treeCmd = defineStrictCommand({
|
|
231
303
|
meta: {
|
|
232
304
|
name: "tree",
|
|
233
305
|
description: "Print a project declaration tree from top-level AST declarations, grouped by file."
|
|
@@ -245,7 +317,7 @@ const treeCmd = defineCommand({
|
|
|
245
317
|
const { agentProfile } = resolveProfile(args.profile);
|
|
246
318
|
const exportFilter = args.exports == null ? agentProfile ? "exported" : "all" : resolveExportFilter(args.exports);
|
|
247
319
|
const tree = buildDeclarationTree({
|
|
248
|
-
glob: args.glob ? args.glob
|
|
320
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
249
321
|
exports: agentProfile ? "all" : exportFilter
|
|
250
322
|
});
|
|
251
323
|
if (agentProfile && !args.json) {
|
|
@@ -256,7 +328,7 @@ const treeCmd = defineCommand({
|
|
|
256
328
|
process.stdout.write(`${formatDeclarationTree(tree, !!args.json)}\n`);
|
|
257
329
|
}
|
|
258
330
|
});
|
|
259
|
-
const unusedCmd =
|
|
331
|
+
const unusedCmd = defineStrictCommand({
|
|
260
332
|
meta: {
|
|
261
333
|
name: "unused",
|
|
262
334
|
description: "Find unreferenced top-level declarations."
|
|
@@ -265,22 +337,20 @@ const unusedCmd = defineCommand({
|
|
|
265
337
|
glob: globArg,
|
|
266
338
|
exports: {
|
|
267
339
|
type: "string",
|
|
268
|
-
description: "Declaration filter: all, exported, or local. Defaults to local."
|
|
340
|
+
description: "Declaration filter: all, exported, or local. Defaults to exported (unused local declarations are already caught by tsc noUnusedLocals)."
|
|
269
341
|
},
|
|
270
|
-
tsconfig: { type: "string" },
|
|
271
342
|
json: jsonArg
|
|
272
343
|
},
|
|
273
|
-
run({ args }) {
|
|
274
|
-
const exportFilter = args.exports == null ? "
|
|
275
|
-
const unused = buildUnusedDeclarations({
|
|
276
|
-
glob: args.glob ? args.glob
|
|
277
|
-
exports: exportFilter
|
|
278
|
-
tsconfig: args.tsconfig
|
|
344
|
+
async run({ args }) {
|
|
345
|
+
const exportFilter = args.exports == null ? "exported" : resolveExportFilter(args.exports);
|
|
346
|
+
const unused = await buildUnusedDeclarations({
|
|
347
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
348
|
+
exports: exportFilter
|
|
279
349
|
});
|
|
280
350
|
process.stdout.write(`${formatUnusedDeclarations(unused, !!args.json)}\n`);
|
|
281
351
|
}
|
|
282
352
|
});
|
|
283
|
-
const moveCmd =
|
|
353
|
+
const moveCmd = defineStrictCommand({
|
|
284
354
|
meta: {
|
|
285
355
|
name: "move",
|
|
286
356
|
description: "Move a top-level exported symbol between files; rewrites import sites project-wide."
|
|
@@ -317,7 +387,7 @@ const moveCmd = defineCommand({
|
|
|
317
387
|
}), !!args.apply, verifyMode !== "none", !!args.json, resolveProfile(args.profile).agentProfile);
|
|
318
388
|
}
|
|
319
389
|
});
|
|
320
|
-
const deleteCmd =
|
|
390
|
+
const deleteCmd = defineStrictCommand({
|
|
321
391
|
meta: {
|
|
322
392
|
name: "delete",
|
|
323
393
|
description: "Delete an unused top-level declaration from a file; refuses if references remain."
|
|
@@ -332,7 +402,6 @@ const deleteCmd = defineCommand({
|
|
|
332
402
|
required: true,
|
|
333
403
|
description: "Source file path."
|
|
334
404
|
},
|
|
335
|
-
tsconfig: { type: "string" },
|
|
336
405
|
apply: applyArg,
|
|
337
406
|
verify: verifyArg,
|
|
338
407
|
verifyMode: verifyModeArg,
|
|
@@ -341,14 +410,12 @@ const deleteCmd = defineCommand({
|
|
|
341
410
|
},
|
|
342
411
|
async run({ args }) {
|
|
343
412
|
const verifyMode = resolveCliVerifyMode(args.verify, args.verifyMode);
|
|
344
|
-
emitResult(await runDelete(args.symbol, args.from, {
|
|
345
|
-
tsconfig: args.tsconfig,
|
|
346
|
-
verify: verifyMode
|
|
347
|
-
}), !!args.apply, verifyMode !== "none", !!args.json, resolveProfile(args.profile).agentProfile);
|
|
413
|
+
emitResult(await runDelete(args.symbol, args.from, { verify: verifyMode }), !!args.apply, verifyMode !== "none", !!args.json, resolveProfile(args.profile).agentProfile);
|
|
348
414
|
}
|
|
349
415
|
});
|
|
350
416
|
function emitResult(r, apply, verify = false, json = false, agentProfile = false) {
|
|
351
417
|
const s = summarize(r.changes);
|
|
418
|
+
const warnings = r.warnings ?? [];
|
|
352
419
|
if (json) {
|
|
353
420
|
const blockedByRegression = verify && apply && r.regressions.length > 0;
|
|
354
421
|
const wrote = apply && !blockedByRegression;
|
|
@@ -365,12 +432,14 @@ function emitResult(r, apply, verify = false, json = false, agentProfile = false
|
|
|
365
432
|
before: c.before,
|
|
366
433
|
after: c.after
|
|
367
434
|
})),
|
|
368
|
-
regressions: r.regressions
|
|
435
|
+
regressions: r.regressions,
|
|
436
|
+
warnings
|
|
369
437
|
};
|
|
370
438
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
371
439
|
if (blockedByRegression) process.exit(1);
|
|
372
440
|
return;
|
|
373
441
|
}
|
|
442
|
+
for (const w of warnings) process.stderr.write(`warning: ${w}\n`);
|
|
374
443
|
if (agentProfile) {
|
|
375
444
|
const blockedByRegression = verify && apply && r.regressions.length > 0;
|
|
376
445
|
process.stdout.write(`${profileHeader()}\n`);
|
|
@@ -413,7 +482,7 @@ function emitResult(r, apply, verify = false, json = false, agentProfile = false
|
|
|
413
482
|
const suffix = apply ? "" : " (dry run, pass --apply to write)";
|
|
414
483
|
process.stdout.write(`\n${r.changes.length}/${r.scanned} files changed${suffix}\n`);
|
|
415
484
|
}
|
|
416
|
-
const renameFileCmd =
|
|
485
|
+
const renameFileCmd = defineStrictCommand({
|
|
417
486
|
meta: {
|
|
418
487
|
name: "rename-file",
|
|
419
488
|
description: "Rename a file and rewrite every import site (including .vue consumers and component-name refs)."
|
|
@@ -431,6 +500,7 @@ const renameFileCmd = defineCommand({
|
|
|
431
500
|
apply: applyArg,
|
|
432
501
|
verify: verifyArg,
|
|
433
502
|
verifyMode: verifyModeArg,
|
|
503
|
+
vue: vueArg,
|
|
434
504
|
profile: profileArg,
|
|
435
505
|
json: jsonArg
|
|
436
506
|
},
|
|
@@ -440,7 +510,8 @@ const renameFileCmd = defineCommand({
|
|
|
440
510
|
if (recovered.warning) process.stderr.write(`warning: ${recovered.warning}\n`);
|
|
441
511
|
const r = await runRenameFile(recovered.old, recovered.new, {
|
|
442
512
|
tsconfig: args.tsconfig,
|
|
443
|
-
verify: verifyMode
|
|
513
|
+
verify: verifyMode,
|
|
514
|
+
vue: args.vue
|
|
444
515
|
});
|
|
445
516
|
const apply = !!args.apply;
|
|
446
517
|
const verify = verifyMode !== "none";
|
|
@@ -527,7 +598,7 @@ const renameFileCmd = defineCommand({
|
|
|
527
598
|
process.stdout.write(`\n${r.changes.length} consumer file(s) updated${suffix}\n`);
|
|
528
599
|
}
|
|
529
600
|
});
|
|
530
|
-
const cssClassRenameCmd =
|
|
601
|
+
const cssClassRenameCmd = defineStrictCommand({
|
|
531
602
|
meta: {
|
|
532
603
|
name: "css-class-rename",
|
|
533
604
|
description: "Rename CSS utility class token(s) across strings, Vue templates, and @apply. Pass a single \"from to\" pair, or --map <file.json> for bulk. No typecheck verify."
|
|
@@ -551,7 +622,8 @@ const cssClassRenameCmd = defineCommand({
|
|
|
551
622
|
json: jsonArg
|
|
552
623
|
},
|
|
553
624
|
async run({ args }) {
|
|
554
|
-
|
|
625
|
+
const map = buildRenameMap(args.from, args.to, args.map);
|
|
626
|
+
emitResult(await runCssClassRename(map, { glob: args.glob ? splitGlobs(args.glob) : void 0 }), !!args.apply, false, !!args.json, resolveProfile(args.profile).agentProfile);
|
|
555
627
|
}
|
|
556
628
|
});
|
|
557
629
|
function buildRenameMap(from, to, mapPath) {
|
|
@@ -565,7 +637,7 @@ function buildRenameMap(from, to, mapPath) {
|
|
|
565
637
|
process.stderr.write(`ripast css-class-rename: missing input. Pass "from to" positionals or --map <file.json>.\n`);
|
|
566
638
|
process.exit(2);
|
|
567
639
|
}
|
|
568
|
-
if (hasPair) return new Map([[from, to]]);
|
|
640
|
+
if (hasPair) return /* @__PURE__ */ new Map([[from, to]]);
|
|
569
641
|
const raw = readFileSync(resolve(process.cwd(), mapPath), "utf8");
|
|
570
642
|
let parsed;
|
|
571
643
|
try {
|
|
@@ -596,7 +668,7 @@ function buildRenameMap(from, to, mapPath) {
|
|
|
596
668
|
}
|
|
597
669
|
return new Map(entries);
|
|
598
670
|
}
|
|
599
|
-
const cssClassScanCmd =
|
|
671
|
+
const cssClassScanCmd = defineStrictCommand({
|
|
600
672
|
meta: {
|
|
601
673
|
name: "css-class-scan",
|
|
602
674
|
description: "Tokenize every class site (strings, Vue class attrs, @apply) and emit sortable token or file frequency lists. Use --sort count-asc for rare tokens or --by file for files introducing the most unique classes."
|
|
@@ -622,7 +694,7 @@ const cssClassScanCmd = defineCommand({
|
|
|
622
694
|
const { agentProfile } = resolveProfile(args.profile);
|
|
623
695
|
const by = resolveCssClassScanGroup(args.by);
|
|
624
696
|
const base = {
|
|
625
|
-
glob: args.glob ? args.glob
|
|
697
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
626
698
|
pattern: args.pattern ? args.pattern.split(",") : void 0
|
|
627
699
|
};
|
|
628
700
|
if (by === "file") {
|
|
@@ -681,7 +753,7 @@ const rootOnlyArg = {
|
|
|
681
753
|
default: false,
|
|
682
754
|
description: "Match only template-root elements (direct children of <template>); ignores nested matches."
|
|
683
755
|
};
|
|
684
|
-
const vueTemplateWrapCmd =
|
|
756
|
+
const vueTemplateWrapCmd = defineStrictCommand({
|
|
685
757
|
meta: {
|
|
686
758
|
name: "vue-template-wrap",
|
|
687
759
|
description: "Wrap every matching element in a Vue <template> with a parent component. Selector: \"Tag\" or \"Tag[attr]\" / \"Tag[attr=value]\". Wrapper: tag name with optional inline attributes (e.g. \"ProPageStates name=\\\"lh\\\"\")."
|
|
@@ -704,13 +776,13 @@ const vueTemplateWrapCmd = defineCommand({
|
|
|
704
776
|
},
|
|
705
777
|
async run({ args }) {
|
|
706
778
|
emitResult(await runVueTemplateWrap(args.selector, args.wrapper, {
|
|
707
|
-
glob: args.glob ? args.glob
|
|
779
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
708
780
|
scope: args.scope,
|
|
709
781
|
rootOnly: args.rootOnly
|
|
710
782
|
}), !!args.apply, false, !!args.json, resolveProfile(args.profile).agentProfile);
|
|
711
783
|
}
|
|
712
784
|
});
|
|
713
|
-
const vueTemplateUnwrapCmd =
|
|
785
|
+
const vueTemplateUnwrapCmd = defineStrictCommand({
|
|
714
786
|
meta: {
|
|
715
787
|
name: "vue-template-unwrap",
|
|
716
788
|
description: "Remove every matching element in a Vue <template>, hoisting its children up one level. Inverse of vue-template-wrap."
|
|
@@ -729,13 +801,13 @@ const vueTemplateUnwrapCmd = defineCommand({
|
|
|
729
801
|
},
|
|
730
802
|
async run({ args }) {
|
|
731
803
|
emitResult(await runVueTemplateUnwrap(args.selector, {
|
|
732
|
-
glob: args.glob ? args.glob
|
|
804
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
733
805
|
scope: args.scope,
|
|
734
806
|
rootOnly: args.rootOnly
|
|
735
807
|
}), !!args.apply, false, !!args.json, resolveProfile(args.profile).agentProfile);
|
|
736
808
|
}
|
|
737
809
|
});
|
|
738
|
-
const componentsCmd =
|
|
810
|
+
const componentsCmd = defineStrictCommand({
|
|
739
811
|
meta: {
|
|
740
812
|
name: "components",
|
|
741
813
|
description: "Inventory Vue/Nuxt components (manifest-first, glob fallback); flag shadowed entries and duplicate-name groups. Pass a name positional for the focused view."
|
|
@@ -762,7 +834,7 @@ const componentsCmd = defineCommand({
|
|
|
762
834
|
async run({ args }) {
|
|
763
835
|
const { agentProfile } = resolveProfile(args.profile);
|
|
764
836
|
const opts = {
|
|
765
|
-
glob: args.glob ? args.glob
|
|
837
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
766
838
|
source: resolveComponentsSource(args.source)
|
|
767
839
|
};
|
|
768
840
|
if (args.name) {
|
|
@@ -811,7 +883,100 @@ function resolveComponentsSource(raw) {
|
|
|
811
883
|
}
|
|
812
884
|
return raw;
|
|
813
885
|
}
|
|
814
|
-
|
|
886
|
+
const doctorCmd = defineStrictCommand({
|
|
887
|
+
meta: {
|
|
888
|
+
name: "doctor",
|
|
889
|
+
description: "Health checks over the AST graph. Suppress per-file with `// ripast-doctor-ignore-file[: c1,c2]` or per-line with `// ripast-doctor-ignore-next-line[: c1,c2]`."
|
|
890
|
+
},
|
|
891
|
+
args: {
|
|
892
|
+
glob: globArg,
|
|
893
|
+
checks: {
|
|
894
|
+
type: "string",
|
|
895
|
+
description: "Comma-separated subset: dangling-reexport, stale-reexport, stale-import, duplicate-export, orphan-file, orphan-test, inconsistent-import-path, circular-dep, phantom-component, shadowed-component, cross-realm-import, stale-nuxt-config-ref. Default: all (adapter checks run when their framework is detected)."
|
|
896
|
+
},
|
|
897
|
+
entry: {
|
|
898
|
+
type: "string",
|
|
899
|
+
description: "Comma-separated entry files exempt from orphan check (relative to cwd)."
|
|
900
|
+
},
|
|
901
|
+
fix: {
|
|
902
|
+
type: "boolean",
|
|
903
|
+
default: false,
|
|
904
|
+
description: "Compute safe fixes for inconsistent-import-path and dangling-reexport findings."
|
|
905
|
+
},
|
|
906
|
+
apply: applyArg,
|
|
907
|
+
changed: {
|
|
908
|
+
type: "string",
|
|
909
|
+
description: "Report only findings on files changed vs git. No value = dirty working tree (staged+unstaged+untracked). With value (e.g. main, HEAD~1) = diff against that ref. Full project is still scanned so cross-file checks stay accurate."
|
|
910
|
+
},
|
|
911
|
+
profile: profileArg,
|
|
912
|
+
json: jsonArg
|
|
913
|
+
},
|
|
914
|
+
async run({ args }) {
|
|
915
|
+
const { agentProfile } = resolveProfile(args.profile);
|
|
916
|
+
const checks = args.checks ? args.checks.split(",") : void 0;
|
|
917
|
+
const entry = args.entry ? args.entry.split(",") : void 0;
|
|
918
|
+
let changedFiles;
|
|
919
|
+
if (args.changed != null) {
|
|
920
|
+
const ref = typeof args.changed === "string" && args.changed !== "" && args.changed !== "true" ? args.changed : void 0;
|
|
921
|
+
changedFiles = getChangedFiles({
|
|
922
|
+
cwd: process.cwd(),
|
|
923
|
+
ref
|
|
924
|
+
});
|
|
925
|
+
if (!changedFiles.length) {
|
|
926
|
+
process.stderr.write(`ripast doctor: --changed${ref ? ` ${ref}` : ""} matched no files; nothing to report.\n`);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const report = await runDoctor({
|
|
931
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
932
|
+
checks,
|
|
933
|
+
entry,
|
|
934
|
+
changedFiles
|
|
935
|
+
});
|
|
936
|
+
if (args.fix) {
|
|
937
|
+
const fix = buildDoctorFixes(report, process.cwd());
|
|
938
|
+
if (args.json) {
|
|
939
|
+
process.stdout.write(`${JSON.stringify({
|
|
940
|
+
findings: report.findings,
|
|
941
|
+
filesScanned: report.filesScanned,
|
|
942
|
+
fix: {
|
|
943
|
+
applied: !!args.apply,
|
|
944
|
+
files: fix.changes.length,
|
|
945
|
+
fixed: fix.fixed,
|
|
946
|
+
skipped: fix.skipped.length
|
|
947
|
+
},
|
|
948
|
+
changes: args.apply ? void 0 : fix.changes.map((c) => ({
|
|
949
|
+
path: c.rel,
|
|
950
|
+
before: c.before,
|
|
951
|
+
after: c.after
|
|
952
|
+
}))
|
|
953
|
+
})}\n`);
|
|
954
|
+
if (args.apply && fix.changes.length) writeChanges(fix.changes);
|
|
955
|
+
if (fix.skipped.length) process.exit(1);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const s = summarize(fix.changes);
|
|
959
|
+
process.stdout.write(`doctor --fix: ${fix.fixed.length} fixable / ${fix.skipped.length} non-fixable findings\n`);
|
|
960
|
+
process.stdout.write(`${fix.changes.length} file${fix.changes.length === 1 ? "" : "s"}, +${s.linesAdded} -${s.linesRemoved} lines\n\n`);
|
|
961
|
+
if (!args.apply) {
|
|
962
|
+
printDiffs(fix.changes);
|
|
963
|
+
process.stdout.write(`\n(dry run, pass --apply to write)\n`);
|
|
964
|
+
} else {
|
|
965
|
+
writeChanges(fix.changes);
|
|
966
|
+
for (const c of fix.changes) process.stdout.write(`wrote ${c.rel}\n`);
|
|
967
|
+
}
|
|
968
|
+
if (fix.skipped.length) process.exit(1);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (agentProfile && !args.json) {
|
|
972
|
+
process.stdout.write(`${profileHeader()}\n${formatAgentDoctorReport(report)}\n`);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
process.stdout.write(formatDoctorReport(report, !!args.json));
|
|
976
|
+
if (report.findings.length) process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
runMain(defineStrictCommand({
|
|
815
980
|
meta: {
|
|
816
981
|
name: "ripast",
|
|
817
982
|
description: "AST-aware refactor primitives. ripgrep-prefiltered, dry-run by default."
|
|
@@ -820,99 +985,7 @@ runMain(defineCommand({
|
|
|
820
985
|
"scan": scanCmd,
|
|
821
986
|
"tree": treeCmd,
|
|
822
987
|
"unused": unusedCmd,
|
|
823
|
-
"doctor":
|
|
824
|
-
meta: {
|
|
825
|
-
name: "doctor",
|
|
826
|
-
description: "Health checks over the AST graph. Suppress per-file with `// ripast-doctor-ignore-file[: c1,c2]` or per-line with `// ripast-doctor-ignore-next-line[: c1,c2]`."
|
|
827
|
-
},
|
|
828
|
-
args: {
|
|
829
|
-
glob: globArg,
|
|
830
|
-
checks: {
|
|
831
|
-
type: "string",
|
|
832
|
-
description: "Comma-separated subset: dangling-reexport, stale-reexport, stale-import, duplicate-export, orphan-file, orphan-test, inconsistent-import-path, circular-dep. Default: all."
|
|
833
|
-
},
|
|
834
|
-
entry: {
|
|
835
|
-
type: "string",
|
|
836
|
-
description: "Comma-separated entry files exempt from orphan check (relative to cwd)."
|
|
837
|
-
},
|
|
838
|
-
fix: {
|
|
839
|
-
type: "boolean",
|
|
840
|
-
default: false,
|
|
841
|
-
description: "Compute safe fixes for inconsistent-import-path and dangling-reexport findings."
|
|
842
|
-
},
|
|
843
|
-
apply: applyArg,
|
|
844
|
-
changed: {
|
|
845
|
-
type: "string",
|
|
846
|
-
description: "Report only findings on files changed vs git. No value = dirty working tree (staged+unstaged+untracked). With value (e.g. main, HEAD~1) = diff against that ref. Full project is still scanned so cross-file checks stay accurate."
|
|
847
|
-
},
|
|
848
|
-
profile: profileArg,
|
|
849
|
-
json: jsonArg
|
|
850
|
-
},
|
|
851
|
-
async run({ args }) {
|
|
852
|
-
const { agentProfile } = resolveProfile(args.profile);
|
|
853
|
-
const checks = args.checks ? args.checks.split(",") : void 0;
|
|
854
|
-
const entry = args.entry ? args.entry.split(",") : void 0;
|
|
855
|
-
let changedFiles;
|
|
856
|
-
if (args.changed != null) {
|
|
857
|
-
const ref = typeof args.changed === "string" && args.changed !== "" && args.changed !== "true" ? args.changed : void 0;
|
|
858
|
-
changedFiles = getChangedFiles({
|
|
859
|
-
cwd: process.cwd(),
|
|
860
|
-
ref
|
|
861
|
-
});
|
|
862
|
-
if (!changedFiles.length) {
|
|
863
|
-
process.stderr.write(`ripast doctor: --changed${ref ? ` ${ref}` : ""} matched no files; nothing to report.\n`);
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
const report = await runDoctor({
|
|
868
|
-
glob: args.glob ? args.glob.split(",") : void 0,
|
|
869
|
-
checks,
|
|
870
|
-
entry,
|
|
871
|
-
changedFiles
|
|
872
|
-
});
|
|
873
|
-
if (args.fix) {
|
|
874
|
-
const fix = buildDoctorFixes(report, process.cwd());
|
|
875
|
-
if (args.json) {
|
|
876
|
-
process.stdout.write(`${JSON.stringify({
|
|
877
|
-
findings: report.findings,
|
|
878
|
-
filesScanned: report.filesScanned,
|
|
879
|
-
fix: {
|
|
880
|
-
applied: !!args.apply,
|
|
881
|
-
files: fix.changes.length,
|
|
882
|
-
fixed: fix.fixed,
|
|
883
|
-
skipped: fix.skipped.length
|
|
884
|
-
},
|
|
885
|
-
changes: args.apply ? void 0 : fix.changes.map((c) => ({
|
|
886
|
-
path: c.rel,
|
|
887
|
-
before: c.before,
|
|
888
|
-
after: c.after
|
|
889
|
-
}))
|
|
890
|
-
})}\n`);
|
|
891
|
-
if (args.apply && fix.changes.length) writeChanges(fix.changes);
|
|
892
|
-
if (fix.skipped.length) process.exit(1);
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
const s = summarize(fix.changes);
|
|
896
|
-
process.stdout.write(`doctor --fix: ${fix.fixed.length} fixable / ${fix.skipped.length} non-fixable findings\n`);
|
|
897
|
-
process.stdout.write(`${fix.changes.length} file${fix.changes.length === 1 ? "" : "s"}, +${s.linesAdded} -${s.linesRemoved} lines\n\n`);
|
|
898
|
-
if (!args.apply) {
|
|
899
|
-
printDiffs(fix.changes);
|
|
900
|
-
process.stdout.write(`\n(dry run, pass --apply to write)\n`);
|
|
901
|
-
} else {
|
|
902
|
-
writeChanges(fix.changes);
|
|
903
|
-
for (const c of fix.changes) process.stdout.write(`wrote ${c.rel}\n`);
|
|
904
|
-
}
|
|
905
|
-
if (fix.skipped.length) process.exit(1);
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
if (agentProfile && !args.json) {
|
|
909
|
-
process.stdout.write(`${profileHeader()}\n${formatAgentDoctorReport(report)}\n`);
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
process.stdout.write(formatDoctorReport(report, !!args.json));
|
|
913
|
-
if (report.findings.length) process.exit(1);
|
|
914
|
-
}
|
|
915
|
-
}),
|
|
988
|
+
"doctor": doctorCmd,
|
|
916
989
|
"rename": renameCmd,
|
|
917
990
|
"replace": replaceCmd,
|
|
918
991
|
"rename-file": renameFileCmd,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ripast/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "CLI for ripast. Deterministic AST refactors (rename, move, find-usages, tailwind/CSS class migration) for TypeScript and frontend frameworks. Auto-installs framework adapters as needed.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -28,29 +28,28 @@
|
|
|
28
28
|
],
|
|
29
29
|
"exports": {
|
|
30
30
|
".": {
|
|
31
|
-
"types": "./dist/cli.d.
|
|
31
|
+
"types": "./dist/cli.d.mts",
|
|
32
32
|
"import": "./dist/cli.mjs"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"main": "./dist/cli.mjs",
|
|
36
|
-
"types": "./dist/cli.d.
|
|
36
|
+
"types": "./dist/cli.d.mts",
|
|
37
37
|
"bin": {
|
|
38
38
|
"ripast": "./bin/ripast.mjs"
|
|
39
39
|
},
|
|
40
40
|
"files": [
|
|
41
41
|
"bin",
|
|
42
|
-
"dist"
|
|
42
|
+
"dist",
|
|
43
|
+
"skills"
|
|
43
44
|
],
|
|
44
45
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
46
|
-
},
|
|
47
|
-
"peerDependencies": {
|
|
48
|
-
"typescript": ">=5.4"
|
|
46
|
+
"node": ">=22.13"
|
|
49
47
|
},
|
|
50
48
|
"dependencies": {
|
|
49
|
+
"@ripast/core": "0.5.0",
|
|
51
50
|
"citty": "^0.2.2",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
51
|
+
"cross-spawn": "^7.0.6",
|
|
52
|
+
"std-env": "^4.3.0"
|
|
54
53
|
},
|
|
55
54
|
"scripts": {
|
|
56
55
|
"build": "obuild",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ripast
|
|
3
|
+
description: "Use Ripast for AST-aware renames, moves, usages, imports, and CSS class migrations across TS, JS, and Vue. Trigger for mechanical changes spanning files."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Ripast ships on npm as `@ripast/cli`. Run commands from the target project's root.
|
|
7
|
+
Requires Node 22.13+.
|
|
8
|
+
If `rg` is missing, Ripast searches files in Node. This can be slower.
|
|
9
|
+
Programmatic regex searches still require `rg`.
|
|
10
|
+
|
|
11
|
+
## Invocation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm dlx @ripast/cli <command> ...
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If the project already installs Ripast, use `pnpm exec ripast <command> ...`.
|
|
18
|
+
The CLI installs framework adapters when needed, including `@ripast/vue` for Vue and Nuxt.
|
|
19
|
+
Adapter installation prefers pnpm and uses npm if pnpm is missing.
|
|
20
|
+
If pnpm is unavailable, invoke the published CLI with npm:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm exec --yes --package=@ripast/cli -- ripast <command> ...
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
| Command | Use |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `ripast scan <pattern>` | Classify occurrences before a rename. |
|
|
31
|
+
| `ripast tree` | Show top-level declarations and imports. |
|
|
32
|
+
| `ripast unused` | Find declarations without project references. |
|
|
33
|
+
| `ripast rename <from> <to>` | Rename a symbol through the native TypeScript server. |
|
|
34
|
+
| `ripast replace <from> <to>` | Replace an imported symbol with another project export. |
|
|
35
|
+
| `ripast move <symbol> --from <a> --to <b>` | Move an export and update imports. |
|
|
36
|
+
| `ripast delete <symbol> --from <file>` | Delete a declaration if no references remain. |
|
|
37
|
+
| `ripast rename-file <old> <new>` | Rename a file and update importers, including Vue consumers. |
|
|
38
|
+
| `ripast css-class-rename <from> <to>` | Rename a CSS class token. Use `--map <file.json>` for bulk changes. |
|
|
39
|
+
| `ripast css-class-scan` | List class tokens before a migration. |
|
|
40
|
+
|
|
41
|
+
Run `pnpm dlx @ripast/cli <command> --help` for each command's full flags.
|
|
42
|
+
|
|
43
|
+
## Apply changes
|
|
44
|
+
|
|
45
|
+
Keep unrelated work separate. Follow the target repository's worktree rules before writing.
|
|
46
|
+
Start with a scan, then review the dry-run diff:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm dlx @ripast/cli scan useStore
|
|
50
|
+
pnpm dlx @ripast/cli rename useStore useAppStore --profile full
|
|
51
|
+
pnpm dlx @ripast/cli rename useStore useAppStore --apply
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Mutating refactor commands default to dry-run. Pass `--apply` to write.
|
|
55
|
+
Rename, replace, move, delete, and rename-file verify types by default.
|
|
56
|
+
If new diagnostics appear, verification blocks `--apply`.
|
|
57
|
+
Use `--verify-mode touched|project|none` to choose the verification scope.
|
|
58
|
+
Keep verification enabled unless you have reviewed the reported diagnostics.
|
|
59
|
+
CSS class changes have no typecheck verification.
|
|
60
|
+
|
|
61
|
+
If several files declare the same name, use `rename --scope <file>`.
|
|
62
|
+
If several files export the replacement, use `replace --target-scope <file>`.
|
|
63
|
+
Use `--json` for machine-readable results where supported.
|
|
64
|
+
Use `--profile full` for full diffs instead of compact agent output.
|
|
65
|
+
|
|
66
|
+
For Nuxt, prepare the generated configuration before refactoring auto-imports.
|
|
67
|
+
Pass `--tsconfig .nuxt/tsconfig.json` to rename, move, or rename-file:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm exec nuxi prepare
|
|
71
|
+
pnpm dlx @ripast/cli rename useCounter useTally --tsconfig .nuxt/tsconfig.json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Review the diff and run the target project's checks after applying it.
|
|
75
|
+
|
|
76
|
+
## When to use vs Edit
|
|
77
|
+
|
|
78
|
+
Use Ripast for mechanical changes across files.
|
|
79
|
+
Use a direct edit for one small change in a file you already understand.
|
|
80
|
+
Use `rg` and direct edits for prose or patterns that only occur in strings and comments.
|
|
81
|
+
Start with `scan` when the correct operation is unclear.
|
|
82
|
+
|
|
83
|
+
Vue support covers script references and template expressions through its adapter.
|
|
84
|
+
Svelte markup and arbitrary custom codemods are outside the supported refactor surface.
|