@ripast/cli 0.2.1 → 0.3.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.
Files changed (2) hide show
  1. package/dist/cli.mjs +272 -6
  2. package/package.json +2 -2
package/dist/cli.mjs CHANGED
@@ -1,12 +1,12 @@
1
- import { mkdirSync, readFileSync, renameSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
2
2
  import { dirname, relative, resolve } from "node:path";
3
3
  import process from "node:process";
4
- import { buildDeclarationTree, buildScanGraph, buildUnusedDeclarations, formatAgentDeclarationTree, formatAgentFileScanHits, formatAgentHits, formatAgentScanHits, formatDeclarationTree, formatFileScanHits, formatHits, formatRegressions, formatScanGraph, formatScanHits, formatUnusedDeclarations, printDiffs, resolveVerifyMode, runCssClassFileScan, runCssClassRename, runCssClassScan, runDelete, runMove, runRename, runRenameFile, runReplace, scan, summarize, writeChanges } from "@ripast/core";
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
7
  const globArg = {
8
8
  type: "string",
9
- description: "File glob(s), comma-separated. Defaults to *.ts,*.tsx,*.vue,... Respects .gitignore."
9
+ description: "File glob(s), comma-separated. Prefix with ! to exclude (e.g. \"*.ts,!.nuxt/**,!**/*.d.ts\"). Defaults to *.ts,*.tsx,*.vue,... Respects .gitignore."
10
10
  };
11
11
  const applyArg = {
12
12
  type: "boolean",
@@ -67,6 +67,35 @@ function resolveCliVerifyMode(verify, verifyMode) {
67
67
  }
68
68
  return resolveVerifyMode(verify);
69
69
  }
70
+ function recoverSmushedPair(oldArg, newArg) {
71
+ const cwd = process.cwd();
72
+ if (!!oldArg && existsSync(resolve(cwd, oldArg)) && newArg) return {
73
+ old: oldArg,
74
+ new: newArg
75
+ };
76
+ if (!oldArg) return {
77
+ old: oldArg,
78
+ new: newArg
79
+ };
80
+ const candidates = [];
81
+ for (const sep of [
82
+ /\s+/,
83
+ /\s*,\s*/,
84
+ /\s*:\s*/
85
+ ]) {
86
+ const parts = oldArg.split(sep).filter(Boolean);
87
+ if (parts.length === 2) candidates.push([parts[0], parts[1]]);
88
+ }
89
+ for (const [a, b] of candidates) if (existsSync(resolve(cwd, a)) && !existsSync(resolve(cwd, b))) return {
90
+ old: a,
91
+ new: b,
92
+ warning: `recovered smushed positional args: treating "${oldArg}" as old="${a}" new="${b}". Quote each path separately next time.`
93
+ };
94
+ return {
95
+ old: oldArg,
96
+ new: newArg
97
+ };
98
+ }
70
99
  function resolveExportFilter(raw) {
71
100
  if (raw !== "all" && raw !== "exported" && raw !== "local") {
72
101
  process.stderr.write(`ripast: --exports must be "all", "exported", or "local".\n`);
@@ -407,7 +436,9 @@ const renameFileCmd = defineCommand({
407
436
  },
408
437
  async run({ args }) {
409
438
  const verifyMode = resolveCliVerifyMode(args.verify, args.verifyMode);
410
- const r = await runRenameFile(args.old, args.new, {
439
+ const recovered = recoverSmushedPair(args.old, args.new);
440
+ if (recovered.warning) process.stderr.write(`warning: ${recovered.warning}\n`);
441
+ const r = await runRenameFile(recovered.old, recovered.new, {
411
442
  tsconfig: args.tsconfig,
412
443
  verify: verifyMode
413
444
  });
@@ -455,7 +486,7 @@ const renameFileCmd = defineCommand({
455
486
  for (const w of r.warnings) process.stderr.write(`warning: ${w}\n`);
456
487
  if (agentProfile) {
457
488
  process.stdout.write(`${profileHeader()}\n`);
458
- process.stdout.write(`rename-file: ${args.old} -> ${args.new}\n`);
489
+ process.stdout.write(`rename-file: ${recovered.old} -> ${recovered.new}\n`);
459
490
  process.stdout.write(`consumers: ${r.changes.length}/${r.scanned}, +${s.linesAdded} -${s.linesRemoved} lines\n`);
460
491
  if (selfChangeDisplay) process.stdout.write(`self: rewrote moved file's own relative imports\n`);
461
492
  process.stdout.write(`mode: ${apply ? blockedByRegression ? "blocked" : "applied" : "dry-run"}\n`);
@@ -641,6 +672,145 @@ function resolveCssClassFileScanSort(raw) {
641
672
  }
642
673
  return raw;
643
674
  }
675
+ const scopeArg = {
676
+ type: "string",
677
+ description: "Restrict to a single .vue file (skips glob/rg)."
678
+ };
679
+ const rootOnlyArg = {
680
+ type: "boolean",
681
+ default: false,
682
+ description: "Match only template-root elements (direct children of <template>); ignores nested matches."
683
+ };
684
+ const vueTemplateWrapCmd = defineCommand({
685
+ meta: {
686
+ name: "vue-template-wrap",
687
+ 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\\\"\")."
688
+ },
689
+ args: {
690
+ selector: {
691
+ type: "positional",
692
+ required: true
693
+ },
694
+ wrapper: {
695
+ type: "positional",
696
+ required: true
697
+ },
698
+ glob: globArg,
699
+ scope: scopeArg,
700
+ rootOnly: rootOnlyArg,
701
+ apply: applyArg,
702
+ profile: profileArg,
703
+ json: jsonArg
704
+ },
705
+ async run({ args }) {
706
+ emitResult(await runVueTemplateWrap(args.selector, args.wrapper, {
707
+ glob: args.glob ? args.glob.split(",") : void 0,
708
+ scope: args.scope,
709
+ rootOnly: args.rootOnly
710
+ }), !!args.apply, false, !!args.json, resolveProfile(args.profile).agentProfile);
711
+ }
712
+ });
713
+ const vueTemplateUnwrapCmd = defineCommand({
714
+ meta: {
715
+ name: "vue-template-unwrap",
716
+ description: "Remove every matching element in a Vue <template>, hoisting its children up one level. Inverse of vue-template-wrap."
717
+ },
718
+ args: {
719
+ selector: {
720
+ type: "positional",
721
+ required: true
722
+ },
723
+ glob: globArg,
724
+ scope: scopeArg,
725
+ rootOnly: rootOnlyArg,
726
+ apply: applyArg,
727
+ profile: profileArg,
728
+ json: jsonArg
729
+ },
730
+ async run({ args }) {
731
+ emitResult(await runVueTemplateUnwrap(args.selector, {
732
+ glob: args.glob ? args.glob.split(",") : void 0,
733
+ scope: args.scope,
734
+ rootOnly: args.rootOnly
735
+ }), !!args.apply, false, !!args.json, resolveProfile(args.profile).agentProfile);
736
+ }
737
+ });
738
+ const componentsCmd = defineCommand({
739
+ meta: {
740
+ name: "components",
741
+ description: "Inventory Vue/Nuxt components (manifest-first, glob fallback); flag shadowed entries and duplicate-name groups. Pass a name positional for the focused view."
742
+ },
743
+ args: {
744
+ name: {
745
+ type: "positional",
746
+ required: false,
747
+ description: "Component name. If given, prints file, aliases, usages, and same-name candidates."
748
+ },
749
+ glob: globArg,
750
+ source: {
751
+ type: "string",
752
+ description: "Discovery source: auto, manifest, or filesystem. Default: auto."
753
+ },
754
+ dups: {
755
+ type: "boolean",
756
+ default: false,
757
+ description: "Print only duplicate-name groups."
758
+ },
759
+ profile: profileArg,
760
+ json: jsonArg
761
+ },
762
+ async run({ args }) {
763
+ const { agentProfile } = resolveProfile(args.profile);
764
+ const opts = {
765
+ glob: args.glob ? args.glob.split(",") : void 0,
766
+ source: resolveComponentsSource(args.source)
767
+ };
768
+ if (args.name) {
769
+ const detail = await buildComponentDetail(args.name, opts);
770
+ if (!detail) {
771
+ process.stderr.write(`ripast components: no component named "${args.name}".\n`);
772
+ process.exit(1);
773
+ }
774
+ if (args.json) {
775
+ process.stdout.write(`${JSON.stringify(detail, null, 2)}\n`);
776
+ return;
777
+ }
778
+ process.stdout.write(`${formatDetail(detail)}\n`);
779
+ return;
780
+ }
781
+ const inv = await buildComponentInventory(opts);
782
+ if (args.json) {
783
+ process.stdout.write(`${JSON.stringify(args.dups ? inv.duplicates : inv, null, 2)}\n`);
784
+ return;
785
+ }
786
+ if (args.dups) {
787
+ if (!inv.duplicates.length) {
788
+ process.stdout.write("no duplicate-name groups\n");
789
+ return;
790
+ }
791
+ const lines = [];
792
+ for (const dup of inv.duplicates) {
793
+ lines.push(dup.name);
794
+ for (const e of dup.entries) lines.push(` ${e.rel}${e.shadowed ? " (shadowed)" : ""}`);
795
+ }
796
+ process.stdout.write(`${lines.join("\n")}\n`);
797
+ return;
798
+ }
799
+ if (agentProfile) {
800
+ process.stdout.write(`${profileHeader()}\n${formatAgentInventory(inv)}\n`);
801
+ return;
802
+ }
803
+ process.stdout.write(`${formatInventory(inv)}\n`);
804
+ }
805
+ });
806
+ function resolveComponentsSource(raw) {
807
+ if (raw == null) return void 0;
808
+ if (raw !== "auto" && raw !== "manifest" && raw !== "filesystem") {
809
+ process.stderr.write(`ripast components: --source must be "auto", "manifest", or "filesystem".\n`);
810
+ process.exit(2);
811
+ }
812
+ return raw;
813
+ }
644
814
  runMain(defineCommand({
645
815
  meta: {
646
816
  name: "ripast",
@@ -650,13 +820,109 @@ runMain(defineCommand({
650
820
  "scan": scanCmd,
651
821
  "tree": treeCmd,
652
822
  "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
+ }),
653
916
  "rename": renameCmd,
654
917
  "replace": replaceCmd,
655
918
  "rename-file": renameFileCmd,
656
919
  "move": moveCmd,
657
920
  "delete": deleteCmd,
921
+ "components": componentsCmd,
658
922
  "css-class-rename": cssClassRenameCmd,
659
- "css-class-scan": cssClassScanCmd
923
+ "css-class-scan": cssClassScanCmd,
924
+ "vue-template-wrap": vueTemplateWrapCmd,
925
+ "vue-template-unwrap": vueTemplateUnwrapCmd
660
926
  }
661
927
  }));
662
928
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ripast/cli",
3
3
  "type": "module",
4
- "version": "0.2.1",
4
+ "version": "0.3.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",
@@ -50,7 +50,7 @@
50
50
  "dependencies": {
51
51
  "citty": "^0.2.2",
52
52
  "std-env": "^4.1.0",
53
- "@ripast/core": "0.2.1"
53
+ "@ripast/core": "0.3.0"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "obuild",