@staff0rd/assist 0.108.2 → 0.110.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/README.md CHANGED
@@ -86,10 +86,12 @@ After installation, the `assist` command will be available globally. You can als
86
86
  - `assist verify all` - Run all checks, ignoring diff-based filters
87
87
  - `assist verify init` - Add verify scripts to a project
88
88
  - `assist verify hardcoded-colors` - Check for hardcoded hex colors in src/ (supports `hardcodedColors.ignore` globs in config)
89
- - `assist lint` - Run lint checks for conventions not enforced by biomejs
89
+ - `assist lint [-f, --fix]` - Run lint checks for conventions not enforced by biomejs (use `-f` to auto-fix)
90
90
  - `assist lint init` - Initialize Biome with standard linter config
91
91
  - `assist refactor check [pattern]` - Check for files that exceed the maximum line count
92
92
  - `assist refactor ignore <file>` - Add a file to the refactor ignore list
93
+ - `assist refactor rename file <source> <destination>` - Rename/move a TypeScript file and update all imports (dry-run by default, use `--apply` to execute)
94
+ - `assist refactor rename symbol <file> <oldName> <newName>` - Rename a variable, function, class, or type across the project (dry-run by default, use `--apply` to execute)
93
95
  - `assist refactor restructure [pattern]` - Analyze import graph and restructure tightly-coupled files into nested directories
94
96
  - `assist devlog list` - Group git commits by date
95
97
  - `assist devlog next` - Show commits for the day after the last versioned entry
package/claude/CLAUDE.md CHANGED
@@ -1,3 +1,8 @@
1
1
  After any code change, run `/verify` to ensure all checks pass.
2
2
 
3
3
  The tool is invoked using the `assist` command and is installed globally. Use it directly (e.g., `assist commit "message"`). Do NOT try to invoke it via `npx tsx src/index.ts` or guess at the entry point.
4
+
5
+ When renaming TypeScript files or symbols, use the refactor commands instead of doing it manually:
6
+ - `assist refactor rename file <source> <destination>` — rename/move a file and update all imports
7
+ - `assist refactor rename symbol <file> <oldName> <newName>` — rename a variable, function, class, or type across the project
8
+ Both default to dry-run; add `--apply` to execute.
@@ -0,0 +1,50 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { Project } from "ts-morph";
4
+ import { renameExports } from "./renameExports";
5
+
6
+ type Move = { sourcePath: string; destPath: string };
7
+
8
+ function isCaseOnly(a: string, b: string): boolean {
9
+ return a.toLowerCase() === b.toLowerCase();
10
+ }
11
+
12
+ function moveCaseInsensitive(absSource: string, absDest: string): void {
13
+ const tmp = `${absSource}.tmp`;
14
+ fs.renameSync(absSource, tmp);
15
+ fs.renameSync(tmp, absDest);
16
+ }
17
+
18
+ export function applyMoves(
19
+ project: Project,
20
+ moves: Move[],
21
+ cwd: string,
22
+ emit: (line: string) => void,
23
+ ): void {
24
+ for (const { sourcePath, destPath } of moves) {
25
+ const start = performance.now();
26
+ const absSource = path.resolve(sourcePath);
27
+ const absDest = path.resolve(destPath);
28
+
29
+ for (const r of renameExports(project, absSource, absDest)) {
30
+ emit(` Renamed export ${r} in ${sourcePath}`);
31
+ }
32
+
33
+ const sourceFile = project.getSourceFile(absSource);
34
+ if (sourceFile) sourceFile.move(absDest);
35
+
36
+ const ms = (performance.now() - start).toFixed(0);
37
+ const rel = `${path.relative(cwd, absSource)} → ${path.relative(cwd, absDest)}`;
38
+ emit(` Renamed ${rel} (${ms}ms)`);
39
+ }
40
+
41
+ project.saveSync();
42
+
43
+ for (const { sourcePath, destPath } of moves) {
44
+ const absSource = path.resolve(sourcePath);
45
+ const absDest = path.resolve(destPath);
46
+ if (isCaseOnly(absSource, absDest) && fs.existsSync(absSource)) {
47
+ moveCaseInsensitive(absSource, absDest);
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,81 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { findSourceFiles } from "../../../shared/findSourceFiles";
4
+
5
+ export type FileNameViolation = {
6
+ filePath: string;
7
+ fileName: string;
8
+ suggestedName: string;
9
+ };
10
+
11
+ function hasClassOrComponent(content: string): boolean {
12
+ const classPattern = /^(export\s+)?(abstract\s+)?class\s+\w+/m;
13
+ const functionComponentPattern =
14
+ /^(export\s+)?(default\s+)?function\s+[A-Z]\w*\s*\(/m;
15
+ const arrowComponentPattern = /^(export\s+)?(const|let)\s+[A-Z]\w*\s*=.*=>/m;
16
+
17
+ return (
18
+ classPattern.test(content) ||
19
+ functionComponentPattern.test(content) ||
20
+ arrowComponentPattern.test(content)
21
+ );
22
+ }
23
+
24
+ function hasMatchingTypeExport(
25
+ content: string,
26
+ nameWithoutExt: string,
27
+ ): boolean {
28
+ const typePattern = new RegExp(
29
+ `^export\\s+type\\s+${nameWithoutExt}\\b`,
30
+ "m",
31
+ );
32
+ const interfacePattern = new RegExp(
33
+ `^export\\s+interface\\s+${nameWithoutExt}\\b`,
34
+ "m",
35
+ );
36
+ return typePattern.test(content) || interfacePattern.test(content);
37
+ }
38
+
39
+ function suggestName(fileName: string): string {
40
+ const nameWithoutExt = fileName.replace(/\.(ts|tsx)$/, "");
41
+ const ext = fileName.slice(nameWithoutExt.length);
42
+
43
+ // SCREAMING_SNAKE_CASE → camelCase (e.g. OC_COLORS → ocColors)
44
+ if (/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/.test(nameWithoutExt)) {
45
+ const camel = nameWithoutExt
46
+ .toLowerCase()
47
+ .replace(/_([a-z0-9])/g, (_, c: string) => c.toUpperCase());
48
+ return `${camel}${ext}`;
49
+ }
50
+
51
+ return `${nameWithoutExt.charAt(0).toLowerCase()}${nameWithoutExt.slice(1)}${ext}`;
52
+ }
53
+
54
+ export function checkFileNames(): FileNameViolation[] {
55
+ const sourceFiles = findSourceFiles("src");
56
+ const violations: FileNameViolation[] = [];
57
+
58
+ for (const filePath of sourceFiles) {
59
+ const fileName = path.basename(filePath);
60
+ const nameWithoutExt = fileName.replace(/\.(ts|tsx)$/, "");
61
+
62
+ // Skip .stories and .test files — they mirror the component/module name
63
+ if (/\.(stories|test)\.(ts|tsx)$/.test(fileName)) continue;
64
+
65
+ if (/^[A-Z]/.test(nameWithoutExt)) {
66
+ const content = fs.readFileSync(filePath, "utf-8");
67
+ if (
68
+ !hasClassOrComponent(content) &&
69
+ !hasMatchingTypeExport(content, nameWithoutExt)
70
+ ) {
71
+ violations.push({
72
+ filePath,
73
+ fileName,
74
+ suggestedName: suggestName(fileName),
75
+ });
76
+ }
77
+ }
78
+ }
79
+
80
+ return violations;
81
+ }
@@ -0,0 +1,16 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { Project } from "ts-morph";
4
+
5
+ export function createLintProject(): Project {
6
+ const tsConfigPath = path.resolve("tsconfig.json");
7
+ const project = fs.existsSync(tsConfigPath)
8
+ ? new Project({
9
+ tsConfigFilePath: tsConfigPath,
10
+ skipAddingFilesFromTsConfig: true,
11
+ })
12
+ : new Project();
13
+
14
+ project.addSourceFilesAtPaths("src/**/*.{ts,tsx}");
15
+ return project;
16
+ }
@@ -0,0 +1,16 @@
1
+ import chalk from "chalk";
2
+ import { applyMoves } from "./applyMoves";
3
+ import { createLintProject } from "./createLintProject";
4
+
5
+ type Move = { sourcePath: string; destPath: string };
6
+
7
+ export function fixFileNameViolations(moves: Move[]): void {
8
+ const start = performance.now();
9
+ const project = createLintProject();
10
+ const cwd = process.cwd();
11
+
12
+ applyMoves(project, moves, cwd, (line) => console.log(chalk.green(line)));
13
+
14
+ const ms = (performance.now() - start).toFixed(0);
15
+ console.log(chalk.dim(` Done in ${ms}ms`));
16
+ }
@@ -3,8 +3,12 @@ import { runFileNameCheck } from "./runFileNameCheck";
3
3
  import { runImportExtensionCheck } from "./runImportExtensionCheck";
4
4
  import { runStaticImportCheck } from "./runStaticImportCheck";
5
5
 
6
- export function lint(): void {
7
- const fileNamePassed = runFileNameCheck();
6
+ type LintOptions = {
7
+ fix?: boolean;
8
+ };
9
+
10
+ export function lint(options: LintOptions = {}): void {
11
+ const fileNamePassed = runFileNameCheck(options.fix);
8
12
  const staticImportPassed = runStaticImportCheck();
9
13
  const importExtensionPassed = runImportExtensionCheck();
10
14
 
@@ -0,0 +1,37 @@
1
+ import path from "node:path";
2
+ import type { Project } from "ts-morph";
3
+ import { SyntaxKind } from "ts-morph";
4
+
5
+ function nameWithoutExtension(filePath: string): string {
6
+ return path.basename(filePath).replace(/\.(ts|tsx)$/, "");
7
+ }
8
+
9
+ export function renameExports(
10
+ project: Project,
11
+ absSource: string,
12
+ absDest: string,
13
+ ): string[] {
14
+ const oldName = nameWithoutExtension(absSource);
15
+ const newName = nameWithoutExtension(absDest);
16
+ const sourceFile = project.getSourceFile(absSource);
17
+ if (!sourceFile) return [];
18
+
19
+ const renamed: string[] = [];
20
+ for (const [, declarations] of sourceFile.getExportedDeclarations()) {
21
+ for (const decl of declarations) {
22
+ // Skip type aliases and interfaces — they should stay PascalCase
23
+ const kind = decl.getKind();
24
+ if (
25
+ kind === SyntaxKind.TypeAliasDeclaration ||
26
+ kind === SyntaxKind.InterfaceDeclaration
27
+ ) {
28
+ continue;
29
+ }
30
+ const nameNode = decl.getFirstChildByKind(SyntaxKind.Identifier);
31
+ if (!nameNode || nameNode.getText() !== oldName) continue;
32
+ nameNode.rename(newName);
33
+ renamed.push(`${oldName} → ${newName}`);
34
+ }
35
+ }
36
+ return renamed;
37
+ }
@@ -1,69 +1,45 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
2
  import chalk from "chalk";
4
- import { findSourceFiles } from "../../../shared/findSourceFiles";
5
-
6
- type FileNameViolation = {
7
- filePath: string;
8
- fileName: string;
9
- };
10
-
11
- function hasClassOrComponent(content: string): boolean {
12
- const classPattern = /^(export\s+)?(abstract\s+)?class\s+\w+/m;
13
- const functionComponentPattern =
14
- /^(export\s+)?(default\s+)?function\s+[A-Z]\w*\s*\(/m;
15
- const arrowComponentPattern = /^(export\s+)?(const|let)\s+[A-Z]\w*\s*=.*=>/m;
16
-
17
- return (
18
- classPattern.test(content) ||
19
- functionComponentPattern.test(content) ||
20
- arrowComponentPattern.test(content)
3
+ import { checkFileNames, type FileNameViolation } from "./checkFileNames";
4
+ import { fixFileNameViolations } from "./fixFileNameViolations";
5
+
6
+ function reportViolations(violations: FileNameViolation[]): void {
7
+ console.error(chalk.red("\nFile name check failed:\n"));
8
+ console.error(
9
+ chalk.red(
10
+ " Files without classes or React components should not start with a capital letter.\n",
11
+ ),
21
12
  );
22
- }
23
-
24
- function checkFileNames(): FileNameViolation[] {
25
- const sourceFiles = findSourceFiles("src");
26
- const violations: FileNameViolation[] = [];
27
-
28
- for (const filePath of sourceFiles) {
29
- const fileName = path.basename(filePath);
30
- const nameWithoutExt = fileName.replace(/\.(ts|tsx)$/, "");
31
-
32
- if (/^[A-Z]/.test(nameWithoutExt)) {
33
- const content = fs.readFileSync(filePath, "utf-8");
34
- if (!hasClassOrComponent(content)) {
35
- violations.push({ filePath, fileName });
36
- }
37
- }
13
+ for (const violation of violations) {
14
+ console.error(chalk.red(` ${violation.filePath}`));
15
+ console.error(chalk.gray(` Rename to: ${violation.suggestedName}\n`));
38
16
  }
39
-
40
- return violations;
17
+ console.error(chalk.dim(" Run with -f to auto-fix.\n"));
41
18
  }
42
19
 
43
- export function runFileNameCheck(): boolean {
20
+ export function runFileNameCheck(fix = false): boolean {
44
21
  const violations = checkFileNames();
45
- if (violations.length > 0) {
46
- console.error(chalk.red("\nFile name check failed:\n"));
47
- console.error(
48
- chalk.red(
49
- " Files without classes or React components should not start with a capital letter.\n",
50
- ),
51
- );
52
- for (const violation of violations) {
53
- console.error(chalk.red(` ${violation.filePath}`));
54
- console.error(
55
- chalk.gray(
56
- ` Rename to: ${violation.fileName.charAt(0).toLowerCase()}${violation.fileName.slice(1)}\n`,
57
- ),
22
+
23
+ if (violations.length === 0) {
24
+ if (!process.env.CLAUDECODE) {
25
+ console.log(
26
+ "File name check passed. All PascalCase files contain classes or components.",
58
27
  );
59
28
  }
60
- return false;
29
+ return true;
61
30
  }
62
31
 
63
- if (!process.env.CLAUDECODE) {
64
- console.log(
65
- "File name check passed. All PascalCase files contain classes or components.",
66
- );
32
+ if (!fix) {
33
+ reportViolations(violations);
34
+ return false;
67
35
  }
36
+
37
+ fixFileNameViolations(
38
+ violations.map((v) => ({
39
+ sourcePath: v.filePath,
40
+ destPath: path.join(path.dirname(v.filePath), v.suggestedName),
41
+ })),
42
+ );
43
+
68
44
  return true;
69
45
  }