@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 CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync } from 'node:child_process'
3
- import { existsSync, readFileSync } from 'node:fs'
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
- require.resolve(`@ripast/${name}`)
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 = ['--yes', '--package=@ripast/cli', ...missing.map(p => `--package=${p}`), '--', 'ripast', ...process.argv.slice(2)]
57
- const res = spawnSync('npx', args, {
58
- stdio: 'inherit',
59
- env: { ...process.env, RIPAST_REEXEC: '1' },
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 force pure ts-morph behavior."
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 = defineCommand({
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.split(",") : void 0,
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 = defineCommand({
219
+ const renameCmd = defineStrictCommand({
151
220
  meta: {
152
221
  name: "rename",
153
- description: "Scope-aware symbol rename via ts-morph (handles type-only imports, shadowing, JSX)."
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.split(",") : void 0,
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 = defineCommand({
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
- tsconfig: args.tsconfig,
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 = defineCommand({
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.split(",") : void 0,
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 = defineCommand({
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 ? "local" : resolveExportFilter(args.exports);
275
- const unused = buildUnusedDeclarations({
276
- glob: args.glob ? args.glob.split(",") : void 0,
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 = defineCommand({
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 = defineCommand({
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 = defineCommand({
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 = defineCommand({
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
- emitResult(await runCssClassRename(buildRenameMap(args.from, args.to, args.map), { glob: args.glob ? args.glob.split(",") : void 0 }), !!args.apply, false, !!args.json, resolveProfile(args.profile).agentProfile);
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 = defineCommand({
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.split(",") : void 0,
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 = defineCommand({
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.split(",") : void 0,
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 = defineCommand({
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.split(",") : void 0,
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 = defineCommand({
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.split(",") : void 0,
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
- runMain(defineCommand({
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": defineCommand({
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.3.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.ts",
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.ts",
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": ">=20.11"
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
- "std-env": "^4.1.0",
53
- "@ripast/core": "0.3.0"
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.