Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ Use these approaches:
- **Build Order**: Respect dependency graph, use TypeScript project references
- **Testing**: Test packages in isolation + integration between packages

## Example Utils Package - Cross-Platform Compatibility

The `@ts-monorepo-template/example-utils` package provides cross-platform utilities for handling Windows 8.3 short path names (RUNNER~1) that cause test failures on Windows CI.

**Key utilities:**
- `normalizedTmpdir()`, `mkdirSyncReal()`, `normalizePath()` - Path utilities that resolve Windows short names
- `getTestOutputDir()` - Creates isolated test output directories

**ESLint enforcement:** Custom rules (`no-os-tmpdir`, `no-fs-mkdirSync`, `no-fs-realpathSync`, `no-unix-shell-commands`) automatically enforce usage of these utilities instead of unsafe alternatives.

**📖 For complete API documentation and usage examples:** See [packages/example-utils/README.md](packages/example-utils/README.md)

## Development Tools Package

All tools are TypeScript (not shell scripts) for cross-platform compatibility:
Expand Down Expand Up @@ -302,11 +314,31 @@ This is "good overkill" - prevents technical debt from accumulating through AI-a

Located in `packages/dev-tools/eslint-local-rules/`:

**Security & Cross-Platform:**
- **`no-child-process-execSync`** - Enforces `safeExecSync()` instead of raw `execSync()`
- Why: `execSync()` uses shell interpreter → command injection risk
- Why: `safeExecSync()` uses `which` pattern + no shell → cross-platform + secure
- **Auto-fix**: Replaces `execSync` with `safeExecSync` and adds import

**Cross-Platform Path Handling (Windows 8.3 Short Names):**
- **`no-os-tmpdir`** - Enforces `normalizedTmpdir()` from `@ts-monorepo-template/example-utils`
- Why: `os.tmpdir()` returns Windows 8.3 short paths (RUNNER~1) causing test failures
- Why: `normalizedTmpdir()` uses `realpathSync.native()` to resolve actual filesystem paths

- **`no-fs-mkdirSync`** - Enforces `mkdirSyncReal()` from `@ts-monorepo-template/example-utils`
- Why: After `fs.mkdirSync()`, the path might not match what the filesystem uses on Windows
- Why: `mkdirSyncReal()` returns the real (normalized) path to handle 8.3 short name issues

- **`no-fs-realpathSync`** - Enforces `normalizePath()` from `@ts-monorepo-template/example-utils`
- Why: `fs.realpathSync()` doesn't consistently resolve Windows 8.3 short paths across Node versions
- Why: `normalizePath()` uses `realpathSync.native()` with fallbacks for better cross-platform compatibility

- **`no-unix-shell-commands`** - Prevents Unix-specific commands in exec/spawn calls
- Why: Commands like `tar`, `ls`, `touch`, `grep` are Unix-specific and fail on Windows
- Why: Use cross-platform alternatives: Node.js fs APIs or cross-platform npm packages
- Detects commands in: `safeExecSync()`, `safeExecResult()`, `spawn()`, `spawnSync()`, `execSync()`
- Provides alternatives: `ls` → `fs.readdirSync()`, `grep` → `Grep tool`, `cat` → `fs.readFileSync()`, etc.

### Creating New Rules

When you identify a dangerous pattern (security, platform-specific, error-prone):
Expand Down
38 changes: 29 additions & 9 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 20 additions & 4 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ export default [
// Global ignores
{
ignores: [
'dist/',
'build/',
'coverage/',
'node_modules/',
'**/dist/**',
'**/build/**',
'**/coverage/**',
'**/node_modules/**',
'**/*.d.ts',
'vitest.config.ts',
'vitest.*.config.ts',
'.worktrees/**', // Git worktrees
],
},

Expand Down Expand Up @@ -64,8 +65,13 @@ export default [
rules: {
// Local rules - agentic code safety
'local/no-child-process-execSync': 'error',
'local/no-fs-mkdirSync': 'error',
'local/no-fs-realpathSync': 'error',
'local/no-os-tmpdir': 'error',
'local/no-unix-shell-commands': 'error',

// TypeScript
'no-unused-vars': 'off', // Use @typescript-eslint/no-unused-vars instead
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
Expand All @@ -85,13 +91,17 @@ export default [
}],
'@typescript-eslint/switch-exhaustiveness-check': 'error',
'@typescript-eslint/prefer-readonly': 'error',
'@typescript-eslint/no-redundant-type-constituents': 'error',
'@typescript-eslint/prefer-function-type': 'error',
'@typescript-eslint/no-require-imports': 'error',

// General
'no-console': 'off',
'no-undef': 'off',
'prefer-const': 'error',
'no-var': 'error',
'max-depth': ['error', 4],
'max-params': ['error', 7], // Matches SonarQube threshold

// Security
'security/detect-object-injection': 'off',
Expand Down Expand Up @@ -121,13 +131,19 @@ export default [
'unicorn/prefer-string-replace-all': 'error',
'unicorn/prefer-array-find': 'error',
'unicorn/prefer-array-some': 'error',
'unicorn/prefer-at': 'error',
'unicorn/prefer-includes': 'error',
'unicorn/no-for-loop': 'error',
'unicorn/prefer-spread': 'error',
'unicorn/prefer-string-raw': 'error',
'unicorn/no-instanceof-array': 'error',
'unicorn/prefer-date-now': 'error',
'unicorn/prefer-ternary': 'off',
'unicorn/prefer-number-properties': 'error',
'unicorn/no-negated-condition': 'error',
'unicorn/prefer-export-from': 'error',
'unicorn/prefer-structured-clone': 'error',
'unicorn/no-zero-fractions': 'error',
},
},
];
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@
"packages/*"
],
"scripts": {
"build": "bun run build:packages",
"build:packages": "tsx packages/dev-tools/src/run-in-packages.ts build",
"build:clean": "bun run clean && bun run build",
"build": "tsc --build",
"build:packages": "tsc --build",
"build:clean": "tsc --build --clean && tsc --build",
"test": "vibe-validate run \"vitest run\"",
"test:watch": "vitest",
"test:coverage": "vibe-validate run \"vitest run --coverage\"",
"test:integration": "vibe-validate run \"vitest run --config vitest.integration.config.ts\"",
"test:system": "vibe-validate run \"vitest run --config vitest.system.config.ts\"",
"lint": "eslint . --max-warnings=0",
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit && tsx packages/dev-tools/src/run-in-packages.ts typecheck",
"typecheck": "tsc --build --dry --force",
"duplication-check": "tsx packages/dev-tools/src/duplication-check.ts",
"duplication-update-baseline": "tsx packages/dev-tools/src/jscpd-update-baseline.ts",
"validate": "vibe-validate validate",
"pre-commit": "vibe-validate pre-commit",
"clean": "tsx packages/dev-tools/src/run-in-packages.ts clean && rimraf node_modules",
"clean": "tsc --build --clean && rimraf node_modules",
"prepare": "husky",
"bump-version": "tsx packages/dev-tools/src/bump-version.ts",
"pre-publish": "tsx packages/dev-tools/src/pre-publish-check.ts",
Expand All @@ -64,7 +64,7 @@
"semver": "^7.7.3",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vibe-validate": "^0.18.1",
"vibe-validate": "^0.18.2",
"vitest": "^3.2.4"
}
}
Loading