@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 +3 -1
- package/claude/CLAUDE.md +5 -0
- package/dist/commands/lint/lint/applyMoves.ts +50 -0
- package/dist/commands/lint/lint/checkFileNames.ts +81 -0
- package/dist/commands/lint/lint/createLintProject.ts +16 -0
- package/dist/commands/lint/lint/fixFileNameViolations.ts +16 -0
- package/dist/commands/lint/lint/index.ts +6 -2
- package/dist/commands/lint/lint/renameExports.ts +37 -0
- package/dist/commands/lint/lint/runFileNameCheck.ts +31 -55
- package/dist/index.js +752 -485
- package/package.json +2 -1
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
|
-
|
|
7
|
-
|
|
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 {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
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
|
|
29
|
+
return true;
|
|
61
30
|
}
|
|
62
31
|
|
|
63
|
-
if (!
|
|
64
|
-
|
|
65
|
-
|
|
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
|
}
|