@ripast/cli 0.4.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 +204 -134
- 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."
|
|
@@ -267,20 +339,18 @@ const unusedCmd = defineCommand({
|
|
|
267
339
|
type: "string",
|
|
268
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 }) {
|
|
344
|
+
async run({ args }) {
|
|
274
345
|
const exportFilter = args.exports == null ? "exported" : resolveExportFilter(args.exports);
|
|
275
|
-
const unused = buildUnusedDeclarations({
|
|
276
|
-
glob: args.glob ? args.glob
|
|
277
|
-
exports: exportFilter
|
|
278
|
-
tsconfig: args.tsconfig
|
|
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,10 +410,7 @@ 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) {
|
|
@@ -416,7 +482,7 @@ function emitResult(r, apply, verify = false, json = false, agentProfile = false
|
|
|
416
482
|
const suffix = apply ? "" : " (dry run, pass --apply to write)";
|
|
417
483
|
process.stdout.write(`\n${r.changes.length}/${r.scanned} files changed${suffix}\n`);
|
|
418
484
|
}
|
|
419
|
-
const renameFileCmd =
|
|
485
|
+
const renameFileCmd = defineStrictCommand({
|
|
420
486
|
meta: {
|
|
421
487
|
name: "rename-file",
|
|
422
488
|
description: "Rename a file and rewrite every import site (including .vue consumers and component-name refs)."
|
|
@@ -434,6 +500,7 @@ const renameFileCmd = defineCommand({
|
|
|
434
500
|
apply: applyArg,
|
|
435
501
|
verify: verifyArg,
|
|
436
502
|
verifyMode: verifyModeArg,
|
|
503
|
+
vue: vueArg,
|
|
437
504
|
profile: profileArg,
|
|
438
505
|
json: jsonArg
|
|
439
506
|
},
|
|
@@ -443,7 +510,8 @@ const renameFileCmd = defineCommand({
|
|
|
443
510
|
if (recovered.warning) process.stderr.write(`warning: ${recovered.warning}\n`);
|
|
444
511
|
const r = await runRenameFile(recovered.old, recovered.new, {
|
|
445
512
|
tsconfig: args.tsconfig,
|
|
446
|
-
verify: verifyMode
|
|
513
|
+
verify: verifyMode,
|
|
514
|
+
vue: args.vue
|
|
447
515
|
});
|
|
448
516
|
const apply = !!args.apply;
|
|
449
517
|
const verify = verifyMode !== "none";
|
|
@@ -530,7 +598,7 @@ const renameFileCmd = defineCommand({
|
|
|
530
598
|
process.stdout.write(`\n${r.changes.length} consumer file(s) updated${suffix}\n`);
|
|
531
599
|
}
|
|
532
600
|
});
|
|
533
|
-
const cssClassRenameCmd =
|
|
601
|
+
const cssClassRenameCmd = defineStrictCommand({
|
|
534
602
|
meta: {
|
|
535
603
|
name: "css-class-rename",
|
|
536
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."
|
|
@@ -554,7 +622,8 @@ const cssClassRenameCmd = defineCommand({
|
|
|
554
622
|
json: jsonArg
|
|
555
623
|
},
|
|
556
624
|
async run({ args }) {
|
|
557
|
-
|
|
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);
|
|
558
627
|
}
|
|
559
628
|
});
|
|
560
629
|
function buildRenameMap(from, to, mapPath) {
|
|
@@ -568,7 +637,7 @@ function buildRenameMap(from, to, mapPath) {
|
|
|
568
637
|
process.stderr.write(`ripast css-class-rename: missing input. Pass "from to" positionals or --map <file.json>.\n`);
|
|
569
638
|
process.exit(2);
|
|
570
639
|
}
|
|
571
|
-
if (hasPair) return new Map([[from, to]]);
|
|
640
|
+
if (hasPair) return /* @__PURE__ */ new Map([[from, to]]);
|
|
572
641
|
const raw = readFileSync(resolve(process.cwd(), mapPath), "utf8");
|
|
573
642
|
let parsed;
|
|
574
643
|
try {
|
|
@@ -599,7 +668,7 @@ function buildRenameMap(from, to, mapPath) {
|
|
|
599
668
|
}
|
|
600
669
|
return new Map(entries);
|
|
601
670
|
}
|
|
602
|
-
const cssClassScanCmd =
|
|
671
|
+
const cssClassScanCmd = defineStrictCommand({
|
|
603
672
|
meta: {
|
|
604
673
|
name: "css-class-scan",
|
|
605
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."
|
|
@@ -625,7 +694,7 @@ const cssClassScanCmd = defineCommand({
|
|
|
625
694
|
const { agentProfile } = resolveProfile(args.profile);
|
|
626
695
|
const by = resolveCssClassScanGroup(args.by);
|
|
627
696
|
const base = {
|
|
628
|
-
glob: args.glob ? args.glob
|
|
697
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
629
698
|
pattern: args.pattern ? args.pattern.split(",") : void 0
|
|
630
699
|
};
|
|
631
700
|
if (by === "file") {
|
|
@@ -684,7 +753,7 @@ const rootOnlyArg = {
|
|
|
684
753
|
default: false,
|
|
685
754
|
description: "Match only template-root elements (direct children of <template>); ignores nested matches."
|
|
686
755
|
};
|
|
687
|
-
const vueTemplateWrapCmd =
|
|
756
|
+
const vueTemplateWrapCmd = defineStrictCommand({
|
|
688
757
|
meta: {
|
|
689
758
|
name: "vue-template-wrap",
|
|
690
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\\\"\")."
|
|
@@ -707,13 +776,13 @@ const vueTemplateWrapCmd = defineCommand({
|
|
|
707
776
|
},
|
|
708
777
|
async run({ args }) {
|
|
709
778
|
emitResult(await runVueTemplateWrap(args.selector, args.wrapper, {
|
|
710
|
-
glob: args.glob ? args.glob
|
|
779
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
711
780
|
scope: args.scope,
|
|
712
781
|
rootOnly: args.rootOnly
|
|
713
782
|
}), !!args.apply, false, !!args.json, resolveProfile(args.profile).agentProfile);
|
|
714
783
|
}
|
|
715
784
|
});
|
|
716
|
-
const vueTemplateUnwrapCmd =
|
|
785
|
+
const vueTemplateUnwrapCmd = defineStrictCommand({
|
|
717
786
|
meta: {
|
|
718
787
|
name: "vue-template-unwrap",
|
|
719
788
|
description: "Remove every matching element in a Vue <template>, hoisting its children up one level. Inverse of vue-template-wrap."
|
|
@@ -732,13 +801,13 @@ const vueTemplateUnwrapCmd = defineCommand({
|
|
|
732
801
|
},
|
|
733
802
|
async run({ args }) {
|
|
734
803
|
emitResult(await runVueTemplateUnwrap(args.selector, {
|
|
735
|
-
glob: args.glob ? args.glob
|
|
804
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
736
805
|
scope: args.scope,
|
|
737
806
|
rootOnly: args.rootOnly
|
|
738
807
|
}), !!args.apply, false, !!args.json, resolveProfile(args.profile).agentProfile);
|
|
739
808
|
}
|
|
740
809
|
});
|
|
741
|
-
const componentsCmd =
|
|
810
|
+
const componentsCmd = defineStrictCommand({
|
|
742
811
|
meta: {
|
|
743
812
|
name: "components",
|
|
744
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."
|
|
@@ -765,7 +834,7 @@ const componentsCmd = defineCommand({
|
|
|
765
834
|
async run({ args }) {
|
|
766
835
|
const { agentProfile } = resolveProfile(args.profile);
|
|
767
836
|
const opts = {
|
|
768
|
-
glob: args.glob ? args.glob
|
|
837
|
+
glob: args.glob ? splitGlobs(args.glob) : void 0,
|
|
769
838
|
source: resolveComponentsSource(args.source)
|
|
770
839
|
};
|
|
771
840
|
if (args.name) {
|
|
@@ -814,7 +883,100 @@ function resolveComponentsSource(raw) {
|
|
|
814
883
|
}
|
|
815
884
|
return raw;
|
|
816
885
|
}
|
|
817
|
-
|
|
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({
|
|
818
980
|
meta: {
|
|
819
981
|
name: "ripast",
|
|
820
982
|
description: "AST-aware refactor primitives. ripgrep-prefiltered, dry-run by default."
|
|
@@ -823,99 +985,7 @@ runMain(defineCommand({
|
|
|
823
985
|
"scan": scanCmd,
|
|
824
986
|
"tree": treeCmd,
|
|
825
987
|
"unused": unusedCmd,
|
|
826
|
-
"doctor":
|
|
827
|
-
meta: {
|
|
828
|
-
name: "doctor",
|
|
829
|
-
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]`."
|
|
830
|
-
},
|
|
831
|
-
args: {
|
|
832
|
-
glob: globArg,
|
|
833
|
-
checks: {
|
|
834
|
-
type: "string",
|
|
835
|
-
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)."
|
|
836
|
-
},
|
|
837
|
-
entry: {
|
|
838
|
-
type: "string",
|
|
839
|
-
description: "Comma-separated entry files exempt from orphan check (relative to cwd)."
|
|
840
|
-
},
|
|
841
|
-
fix: {
|
|
842
|
-
type: "boolean",
|
|
843
|
-
default: false,
|
|
844
|
-
description: "Compute safe fixes for inconsistent-import-path and dangling-reexport findings."
|
|
845
|
-
},
|
|
846
|
-
apply: applyArg,
|
|
847
|
-
changed: {
|
|
848
|
-
type: "string",
|
|
849
|
-
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."
|
|
850
|
-
},
|
|
851
|
-
profile: profileArg,
|
|
852
|
-
json: jsonArg
|
|
853
|
-
},
|
|
854
|
-
async run({ args }) {
|
|
855
|
-
const { agentProfile } = resolveProfile(args.profile);
|
|
856
|
-
const checks = args.checks ? args.checks.split(",") : void 0;
|
|
857
|
-
const entry = args.entry ? args.entry.split(",") : void 0;
|
|
858
|
-
let changedFiles;
|
|
859
|
-
if (args.changed != null) {
|
|
860
|
-
const ref = typeof args.changed === "string" && args.changed !== "" && args.changed !== "true" ? args.changed : void 0;
|
|
861
|
-
changedFiles = getChangedFiles({
|
|
862
|
-
cwd: process.cwd(),
|
|
863
|
-
ref
|
|
864
|
-
});
|
|
865
|
-
if (!changedFiles.length) {
|
|
866
|
-
process.stderr.write(`ripast doctor: --changed${ref ? ` ${ref}` : ""} matched no files; nothing to report.\n`);
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
const report = await runDoctor({
|
|
871
|
-
glob: args.glob ? args.glob.split(",") : void 0,
|
|
872
|
-
checks,
|
|
873
|
-
entry,
|
|
874
|
-
changedFiles
|
|
875
|
-
});
|
|
876
|
-
if (args.fix) {
|
|
877
|
-
const fix = buildDoctorFixes(report, process.cwd());
|
|
878
|
-
if (args.json) {
|
|
879
|
-
process.stdout.write(`${JSON.stringify({
|
|
880
|
-
findings: report.findings,
|
|
881
|
-
filesScanned: report.filesScanned,
|
|
882
|
-
fix: {
|
|
883
|
-
applied: !!args.apply,
|
|
884
|
-
files: fix.changes.length,
|
|
885
|
-
fixed: fix.fixed,
|
|
886
|
-
skipped: fix.skipped.length
|
|
887
|
-
},
|
|
888
|
-
changes: args.apply ? void 0 : fix.changes.map((c) => ({
|
|
889
|
-
path: c.rel,
|
|
890
|
-
before: c.before,
|
|
891
|
-
after: c.after
|
|
892
|
-
}))
|
|
893
|
-
})}\n`);
|
|
894
|
-
if (args.apply && fix.changes.length) writeChanges(fix.changes);
|
|
895
|
-
if (fix.skipped.length) process.exit(1);
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
const s = summarize(fix.changes);
|
|
899
|
-
process.stdout.write(`doctor --fix: ${fix.fixed.length} fixable / ${fix.skipped.length} non-fixable findings\n`);
|
|
900
|
-
process.stdout.write(`${fix.changes.length} file${fix.changes.length === 1 ? "" : "s"}, +${s.linesAdded} -${s.linesRemoved} lines\n\n`);
|
|
901
|
-
if (!args.apply) {
|
|
902
|
-
printDiffs(fix.changes);
|
|
903
|
-
process.stdout.write(`\n(dry run, pass --apply to write)\n`);
|
|
904
|
-
} else {
|
|
905
|
-
writeChanges(fix.changes);
|
|
906
|
-
for (const c of fix.changes) process.stdout.write(`wrote ${c.rel}\n`);
|
|
907
|
-
}
|
|
908
|
-
if (fix.skipped.length) process.exit(1);
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
if (agentProfile && !args.json) {
|
|
912
|
-
process.stdout.write(`${profileHeader()}\n${formatAgentDoctorReport(report)}\n`);
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
process.stdout.write(formatDoctorReport(report, !!args.json));
|
|
916
|
-
if (report.findings.length) process.exit(1);
|
|
917
|
-
}
|
|
918
|
-
}),
|
|
988
|
+
"doctor": doctorCmd,
|
|
919
989
|
"rename": renameCmd,
|
|
920
990
|
"replace": replaceCmd,
|
|
921
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.
|