@theglitchking/gimme-the-lint 1.0.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.
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const manifestManager = require('./manifest-manager');
4
+
5
+ async function detectDrift({ manifestPath, configPath, currentDirs }) {
6
+ const manifest = await manifestManager.readManifest(manifestPath);
7
+ if (!manifest) {
8
+ return { noManifest: true, message: 'No manifest found - run baseline first' };
9
+ }
10
+
11
+ const drift = {
12
+ hasDirectoryDrift: false,
13
+ hasConfigDrift: false,
14
+ hasTimeDrift: false,
15
+ hasViolationDrift: false,
16
+ addedDirs: [],
17
+ removedDirs: [],
18
+ age: 0,
19
+ details: [],
20
+ };
21
+
22
+ const baselineDirs = manifest.directories_baselined || [];
23
+ drift.addedDirs = currentDirs.filter((d) => !baselineDirs.includes(d));
24
+ drift.removedDirs = baselineDirs.filter((d) => !currentDirs.includes(d));
25
+ drift.hasDirectoryDrift = drift.addedDirs.length > 0 || drift.removedDirs.length > 0;
26
+
27
+ if (drift.addedDirs.length > 0) {
28
+ drift.details.push(`Added directories: ${drift.addedDirs.join(', ')}`);
29
+ }
30
+ if (drift.removedDirs.length > 0) {
31
+ drift.details.push(`Removed directories: ${drift.removedDirs.join(', ')}`);
32
+ }
33
+
34
+ const currentHash = await manifestManager.hashFile(configPath);
35
+ drift.hasConfigDrift = currentHash !== 'unknown' && currentHash !== manifest.config_hash;
36
+ if (drift.hasConfigDrift) {
37
+ drift.details.push('Configuration changed (config file hash mismatch)');
38
+ }
39
+
40
+ drift.age = manifestManager.calculateAge(manifest.created_at);
41
+ drift.hasTimeDrift = drift.age > 30;
42
+ if (drift.hasTimeDrift) {
43
+ drift.details.push(`Baseline is ${drift.age} days old (consider refreshing)`);
44
+ }
45
+
46
+ return drift;
47
+ }
48
+
49
+ function formatDriftReport(drift) {
50
+ if (drift.noManifest) {
51
+ return drift.message;
52
+ }
53
+
54
+ const hasDrift = drift.hasDirectoryDrift || drift.hasConfigDrift || drift.hasTimeDrift;
55
+ if (!hasDrift) {
56
+ return null;
57
+ }
58
+
59
+ const lines = ['Drift Detected:'];
60
+ for (const detail of drift.details) {
61
+ lines.push(` - ${detail}`);
62
+ }
63
+ return lines.join('\n');
64
+ }
65
+
66
+ async function autoHeal({ manifestPath, configPath, currentDirs, tool, version, currentViolations, testExcluded }) {
67
+ const oldManifest = await manifestManager.readManifest(manifestPath);
68
+
69
+ const newManifest = await manifestManager.createManifest({
70
+ tool,
71
+ version,
72
+ directories: currentDirs,
73
+ violations: currentViolations,
74
+ configPath,
75
+ testExcluded,
76
+ });
77
+
78
+ await manifestManager.writeManifest(manifestPath, newManifest);
79
+
80
+ return {
81
+ oldDirs: oldManifest ? oldManifest.directories_baselined : [],
82
+ newDirs: currentDirs,
83
+ oldViolations: oldManifest ? oldManifest.total_violations : 0,
84
+ newViolations: currentViolations,
85
+ };
86
+ }
87
+
88
+ module.exports = {
89
+ detectDrift,
90
+ formatDriftReport,
91
+ autoHeal,
92
+ };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+
7
+ function getGitRoot(projectRoot) {
8
+ try {
9
+ return execSync('git rev-parse --show-toplevel', {
10
+ cwd: projectRoot,
11
+ encoding: 'utf8',
12
+ }).trim();
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ function getHooksDir(projectRoot) {
19
+ const gitRoot = getGitRoot(projectRoot);
20
+ if (!gitRoot) return null;
21
+ return path.join(gitRoot, '.git', 'hooks');
22
+ }
23
+
24
+ async function installHooks(projectRoot) {
25
+ const hooksDir = getHooksDir(projectRoot);
26
+ if (!hooksDir) {
27
+ throw new Error('Not a git repository. Run git init first.');
28
+ }
29
+
30
+ await fs.ensureDir(hooksDir);
31
+
32
+ const sourceHooksDir = path.join(__dirname, '..', 'githooks');
33
+ const hooks = ['pre-commit', 'pre-push'];
34
+ const installed = [];
35
+
36
+ for (const hook of hooks) {
37
+ const src = path.join(sourceHooksDir, hook);
38
+ const dest = path.join(hooksDir, hook);
39
+
40
+ if (!await fs.pathExists(src)) continue;
41
+
42
+ if (await fs.pathExists(dest)) {
43
+ const existing = await fs.readFile(dest, 'utf8');
44
+ if (!existing.includes('gimme-the-lint')) {
45
+ const backup = `${dest}.backup.${Date.now()}`;
46
+ await fs.copy(dest, backup);
47
+ }
48
+ }
49
+
50
+ await fs.copy(src, dest);
51
+ await fs.chmod(dest, 0o755);
52
+ installed.push(hook);
53
+ }
54
+
55
+ return installed;
56
+ }
57
+
58
+ async function uninstallHooks(projectRoot) {
59
+ const hooksDir = getHooksDir(projectRoot);
60
+ if (!hooksDir) return [];
61
+
62
+ const hooks = ['pre-commit', 'pre-push'];
63
+ const removed = [];
64
+
65
+ for (const hook of hooks) {
66
+ const hookPath = path.join(hooksDir, hook);
67
+ if (await fs.pathExists(hookPath)) {
68
+ const content = await fs.readFile(hookPath, 'utf8');
69
+ if (content.includes('gimme-the-lint')) {
70
+ await fs.remove(hookPath);
71
+
72
+ // Restore backup if exists
73
+ const backups = (await fs.readdir(hooksDir))
74
+ .filter((f) => f.startsWith(`${hook}.backup.`))
75
+ .sort()
76
+ .reverse();
77
+ if (backups.length > 0) {
78
+ await fs.move(path.join(hooksDir, backups[0]), hookPath);
79
+ }
80
+
81
+ removed.push(hook);
82
+ }
83
+ }
84
+ }
85
+
86
+ return removed;
87
+ }
88
+
89
+ async function getStatus(projectRoot) {
90
+ const hooksDir = getHooksDir(projectRoot);
91
+ if (!hooksDir) return { gitRepo: false, hooks: {} };
92
+
93
+ const hooks = ['pre-commit', 'pre-push'];
94
+ const status = {};
95
+
96
+ for (const hook of hooks) {
97
+ const hookPath = path.join(hooksDir, hook);
98
+ if (await fs.pathExists(hookPath)) {
99
+ const content = await fs.readFile(hookPath, 'utf8');
100
+ status[hook] = content.includes('gimme-the-lint') ? 'installed' : 'other';
101
+ } else {
102
+ status[hook] = 'missing';
103
+ }
104
+ }
105
+
106
+ return { gitRepo: true, hooks: status };
107
+ }
108
+
109
+ module.exports = {
110
+ getGitRoot,
111
+ getHooksDir,
112
+ installHooks,
113
+ uninstallHooks,
114
+ getStatus,
115
+ };
package/lib/index.js ADDED
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const directoryDiscovery = require('./directory-discovery');
4
+ const manifestManager = require('./manifest-manager');
5
+ const driftDetector = require('./drift-detector');
6
+ const venvManager = require('./venv-manager');
7
+ const configManager = require('./config-manager');
8
+ const gitHooksManager = require('./git-hooks-manager');
9
+ const installer = require('./installer');
10
+
11
+ module.exports = {
12
+ directoryDiscovery,
13
+ manifestManager,
14
+ driftDetector,
15
+ venvManager,
16
+ configManager,
17
+ gitHooksManager,
18
+ installer,
19
+ };
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const configManager = require('./config-manager');
6
+ const venvManager = require('./venv-manager');
7
+ const gitHooksManager = require('./git-hooks-manager');
8
+
9
+ async function init(projectRoot, options = {}) {
10
+ const results = { steps: [], errors: [] };
11
+
12
+ // Step 1: Detect project type
13
+ const projectType = await configManager.detectProjectType(projectRoot);
14
+ results.projectType = projectType;
15
+ results.steps.push(`Detected project type: ${projectType}`);
16
+
17
+ const enableFrontend = options.frontend !== false && (projectType === 'monorepo' || projectType === 'frontend');
18
+ const enableBackend = options.backend !== false && (projectType === 'monorepo' || projectType === 'backend');
19
+
20
+ // Step 2: Create config file
21
+ const configResult = await configManager.initConfig(projectRoot, {
22
+ force: options.force,
23
+ frontendDir: options.frontendDir,
24
+ backendDir: options.backendDir,
25
+ });
26
+ results.steps.push(configResult.created ? 'Created gimme-the-lint.config.js' : 'Config already exists');
27
+
28
+ // Step 3: Copy ESLint template (frontend)
29
+ if (enableFrontend) {
30
+ const frontendDir = path.join(projectRoot, options.frontendDir || 'frontend');
31
+ const eslintDest = path.join(frontendDir, 'eslint.config.js');
32
+
33
+ if (!await fs.pathExists(eslintDest) || options.force) {
34
+ try {
35
+ await configManager.copyTemplate('eslint.config.template.js', eslintDest, {
36
+ REACT_VERSION: options.reactVersion || '18.3',
37
+ });
38
+ results.steps.push('Created eslint.config.js template');
39
+ } catch (e) {
40
+ results.errors.push(`ESLint template: ${e.message}`);
41
+ }
42
+ }
43
+ }
44
+
45
+ // Step 4: Copy pyproject.toml template (backend)
46
+ if (enableBackend) {
47
+ const pyprojectDest = path.join(projectRoot, 'pyproject.toml');
48
+ if (!await fs.pathExists(pyprojectDest) || options.force) {
49
+ try {
50
+ await configManager.copyTemplate('pyproject.template.toml', pyprojectDest, {
51
+ PROJECT_NAME: path.basename(projectRoot),
52
+ });
53
+ results.steps.push('Created pyproject.toml with Ruff config');
54
+ } catch (e) {
55
+ results.errors.push(`pyproject.toml template: ${e.message}`);
56
+ }
57
+ }
58
+ }
59
+
60
+ // Step 5: Copy gitleaks template
61
+ const gitleaksDest = path.join(projectRoot, '.gitleaks.toml');
62
+ if (!await fs.pathExists(gitleaksDest) || options.force) {
63
+ try {
64
+ await configManager.copyTemplate('.gitleaks.template.toml', gitleaksDest);
65
+ results.steps.push('Created .gitleaks.toml');
66
+ } catch (e) {
67
+ results.errors.push(`gitleaks template: ${e.message}`);
68
+ }
69
+ }
70
+
71
+ // Step 6: Copy pre-commit config
72
+ const precommitDest = path.join(projectRoot, '.pre-commit-config.yaml');
73
+ if (!await fs.pathExists(precommitDest) || options.force) {
74
+ try {
75
+ await configManager.copyTemplate('.pre-commit-config.template.yaml', precommitDest);
76
+ results.steps.push('Created .pre-commit-config.yaml');
77
+ } catch (e) {
78
+ results.errors.push(`pre-commit template: ${e.message}`);
79
+ }
80
+ }
81
+
82
+ // Step 7: Copy commitlint config
83
+ const commitlintDest = path.join(projectRoot, 'commitlint.config.js');
84
+ if (!await fs.pathExists(commitlintDest) || options.force) {
85
+ try {
86
+ await configManager.copyTemplate('commitlint.config.template.js', commitlintDest);
87
+ results.steps.push('Created commitlint.config.js');
88
+ } catch (e) {
89
+ results.errors.push(`commitlint template: ${e.message}`);
90
+ }
91
+ }
92
+
93
+ // Step 8: Setup Python venv (backend)
94
+ if (enableBackend && options.setupVenv !== false) {
95
+ try {
96
+ const { pythonVersion } = venvManager.createVenv(projectRoot);
97
+ results.steps.push(`Created Python venv (${pythonVersion})`);
98
+
99
+ const reqFile = path.join(__dirname, '..', 'templates', 'requirements.linting.txt');
100
+ venvManager.installDeps(projectRoot, reqFile);
101
+ results.steps.push('Installed linting dependencies (ruff, mypy)');
102
+ } catch (e) {
103
+ results.errors.push(`Python venv: ${e.message}`);
104
+ }
105
+ }
106
+
107
+ return results;
108
+ }
109
+
110
+ module.exports = { init };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const crypto = require('crypto');
5
+ const path = require('path');
6
+
7
+ async function hashFile(filePath) {
8
+ if (!await fs.pathExists(filePath)) {
9
+ return 'unknown';
10
+ }
11
+ const content = await fs.readFile(filePath, 'utf8');
12
+ return crypto.createHash('md5').update(content).digest('hex');
13
+ }
14
+
15
+ function calculateAge(createdAt) {
16
+ const created = new Date(createdAt);
17
+ const now = new Date();
18
+ return Math.floor((now - created) / (1000 * 60 * 60 * 24));
19
+ }
20
+
21
+ async function createManifest({ tool, version, directories, violations, configPath, testExcluded }) {
22
+ const configHash = await hashFile(configPath);
23
+
24
+ return {
25
+ created_at: new Date().toISOString(),
26
+ tool,
27
+ version,
28
+ directories_baselined: directories,
29
+ total_directories: directories.length,
30
+ total_violations: violations,
31
+ config_hash: configHash,
32
+ test_excluded: testExcluded || [],
33
+ };
34
+ }
35
+
36
+ async function readManifest(manifestPath) {
37
+ if (!await fs.pathExists(manifestPath)) {
38
+ return null;
39
+ }
40
+ try {
41
+ return await fs.readJson(manifestPath);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ async function writeManifest(manifestPath, manifest) {
48
+ await fs.ensureDir(path.dirname(manifestPath));
49
+ await fs.writeJson(manifestPath, manifest, { spaces: 2 });
50
+ }
51
+
52
+ module.exports = {
53
+ hashFile,
54
+ calculateAge,
55
+ createManifest,
56
+ readManifest,
57
+ writeManifest,
58
+ };
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const fs = require('fs-extra');
5
+ const path = require('path');
6
+
7
+ function getPythonCmd() {
8
+ for (const cmd of ['python3', 'python']) {
9
+ try {
10
+ const version = execSync(`${cmd} --version`, { encoding: 'utf8' }).trim();
11
+ const match = version.match(/(\d+)\.(\d+)/);
12
+ if (match && parseInt(match[1]) >= 3 && parseInt(match[2]) >= 8) {
13
+ return { cmd, version };
14
+ }
15
+ } catch {
16
+ continue;
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function getVenvPath(projectRoot) {
23
+ return path.join(projectRoot, '.venv');
24
+ }
25
+
26
+ function isVenvActive(projectRoot) {
27
+ const venvPath = getVenvPath(projectRoot);
28
+ const activatePath = path.join(venvPath, 'bin', 'activate');
29
+ return fs.existsSync(activatePath);
30
+ }
31
+
32
+ function createVenv(projectRoot) {
33
+ const python = getPythonCmd();
34
+ if (!python) {
35
+ throw new Error('Python 3.8+ not found. Please install Python 3.8 or later.');
36
+ }
37
+
38
+ const venvPath = getVenvPath(projectRoot);
39
+
40
+ if (!fs.existsSync(venvPath)) {
41
+ execSync(`${python.cmd} -m venv "${venvPath}"`, { cwd: projectRoot, stdio: 'inherit' });
42
+ }
43
+
44
+ return { venvPath, pythonVersion: python.version };
45
+ }
46
+
47
+ function installDeps(projectRoot, depsFile) {
48
+ const venvPath = getVenvPath(projectRoot);
49
+ const pip = path.join(venvPath, 'bin', 'pip');
50
+
51
+ execSync(`"${pip}" install --quiet --upgrade pip`, { cwd: projectRoot, stdio: 'inherit' });
52
+
53
+ if (depsFile && fs.existsSync(depsFile)) {
54
+ execSync(`"${pip}" install --quiet -r "${depsFile}"`, { cwd: projectRoot, stdio: 'inherit' });
55
+ }
56
+ }
57
+
58
+ function runInVenv(projectRoot, command) {
59
+ const venvPath = getVenvPath(projectRoot);
60
+ const activate = path.join(venvPath, 'bin', 'activate');
61
+ return execSync(`source "${activate}" && ${command}`, {
62
+ cwd: projectRoot,
63
+ encoding: 'utf8',
64
+ shell: '/bin/bash',
65
+ });
66
+ }
67
+
68
+ function getStatus(projectRoot) {
69
+ const venvPath = getVenvPath(projectRoot);
70
+ const exists = fs.existsSync(venvPath);
71
+ let ruffVersion = null;
72
+ let pythonVersion = null;
73
+
74
+ if (exists) {
75
+ try {
76
+ ruffVersion = runInVenv(projectRoot, 'ruff --version').trim();
77
+ } catch { /* not installed */ }
78
+ try {
79
+ pythonVersion = runInVenv(projectRoot, 'python --version').trim();
80
+ } catch { /* not available */ }
81
+ }
82
+
83
+ return { exists, path: venvPath, ruffVersion, pythonVersion };
84
+ }
85
+
86
+ module.exports = {
87
+ getPythonCmd,
88
+ getVenvPath,
89
+ isVenvActive,
90
+ createVenv,
91
+ installDeps,
92
+ runInVenv,
93
+ getStatus,
94
+ };
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@theglitchking/gimme-the-lint",
3
+ "version": "1.0.0",
4
+ "description": "Progressive linting system with directory-chunked baselines, drift detection, and auto-healing for monorepo projects (Python + JS/TS)",
5
+ "keywords": [
6
+ "linting",
7
+ "progressive-linting",
8
+ "eslint",
9
+ "ruff",
10
+ "python",
11
+ "javascript",
12
+ "typescript",
13
+ "monorepo",
14
+ "git-hooks",
15
+ "baseline",
16
+ "drift-detection",
17
+ "claude-plugin"
18
+ ],
19
+ "author": {
20
+ "name": "TheGlitchKing",
21
+ "email": "theglitchking@users.noreply.github.com"
22
+ },
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/TheGlitchKing/gimme-the-lint.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/TheGlitchKing/gimme-the-lint/issues"
30
+ },
31
+ "homepage": "https://github.com/TheGlitchKing/gimme-the-lint",
32
+ "main": "lib/index.js",
33
+ "bin": {
34
+ "gimme-the-lint": "./bin/gimme-the-lint.js"
35
+ },
36
+ "scripts": {
37
+ "postinstall": "node ./bin/postinstall.js",
38
+ "setup:venv": "bash ./scripts/setup-venv.sh",
39
+ "install:hooks": "bash ./githooks/install.sh",
40
+ "lint:init": "node ./bin/gimme-the-lint.js init",
41
+ "check": "node ./bin/gimme-the-lint.js check",
42
+ "check:fix": "node ./bin/gimme-the-lint.js check --fix",
43
+ "baseline": "node ./bin/gimme-the-lint.js baseline",
44
+ "dashboard": "node ./bin/gimme-the-lint.js dashboard",
45
+ "prepublishOnly": "bash ./scripts/validate-version.sh",
46
+ "test": "node --test tests/*.test.js"
47
+ },
48
+ "files": [
49
+ "bin/",
50
+ "scripts/",
51
+ "templates/",
52
+ "githooks/",
53
+ "lib/",
54
+ ".claude-plugin/",
55
+ "agents/",
56
+ "docs/",
57
+ "install.sh",
58
+ "uninstall.sh",
59
+ "README.md",
60
+ "LICENSE",
61
+ "CHANGELOG.md"
62
+ ],
63
+ "engines": {
64
+ "node": ">=18.0.0"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "dependencies": {
70
+ "chalk": "^4.1.2",
71
+ "commander": "^12.1.0",
72
+ "fs-extra": "^11.2.0"
73
+ },
74
+ "peerDependencies": {
75
+ "eslint": "^9.0.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "eslint": {
79
+ "optional": true
80
+ }
81
+ }
82
+ }