@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 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."
@@ -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.split(",") : void 0,
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 = 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,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 = defineCommand({
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 = defineCommand({
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
- 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);
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 = defineCommand({
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.split(",") : void 0,
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 = defineCommand({
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.split(",") : void 0,
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 = defineCommand({
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.split(",") : void 0,
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 = defineCommand({
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.split(",") : void 0,
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
- 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({
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": defineCommand({
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.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.4.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.