tlc-claude-code 1.6.4 → 1.8.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/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
- package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
- package/package.json +1 -1
- package/server/lib/access-control.test.js +1 -1
- package/server/lib/agents-cancel-command.test.js +1 -1
- package/server/lib/agents-get-command.test.js +1 -1
- package/server/lib/agents-list-command.test.js +1 -1
- package/server/lib/agents-logs-command.test.js +1 -1
- package/server/lib/agents-retry-command.test.js +1 -1
- package/server/lib/budget-limits.test.js +2 -2
- package/server/lib/code-gate/bypass-logger.js +129 -0
- package/server/lib/code-gate/bypass-logger.test.js +142 -0
- package/server/lib/code-gate/first-commit-audit.js +138 -0
- package/server/lib/code-gate/first-commit-audit.test.js +203 -0
- package/server/lib/code-gate/gate-command.js +114 -0
- package/server/lib/code-gate/gate-command.test.js +111 -0
- package/server/lib/code-gate/gate-config.js +163 -0
- package/server/lib/code-gate/gate-config.test.js +181 -0
- package/server/lib/code-gate/gate-engine.js +193 -0
- package/server/lib/code-gate/gate-engine.test.js +258 -0
- package/server/lib/code-gate/gate-reporter.js +123 -0
- package/server/lib/code-gate/gate-reporter.test.js +159 -0
- package/server/lib/code-gate/hooks-generator.js +149 -0
- package/server/lib/code-gate/hooks-generator.test.js +142 -0
- package/server/lib/code-gate/llm-reviewer.js +176 -0
- package/server/lib/code-gate/llm-reviewer.test.js +161 -0
- package/server/lib/code-gate/multi-model-reviewer.js +172 -0
- package/server/lib/code-gate/multi-model-reviewer.test.js +217 -0
- package/server/lib/code-gate/push-gate.js +133 -0
- package/server/lib/code-gate/push-gate.test.js +190 -0
- package/server/lib/code-gate/rules/architecture-rules.js +228 -0
- package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
- package/server/lib/code-gate/rules/client-rules.js +120 -0
- package/server/lib/code-gate/rules/client-rules.test.js +121 -0
- package/server/lib/code-gate/rules/config-rules.js +140 -0
- package/server/lib/code-gate/rules/config-rules.test.js +103 -0
- package/server/lib/code-gate/rules/database-rules.js +158 -0
- package/server/lib/code-gate/rules/database-rules.test.js +119 -0
- package/server/lib/code-gate/rules/docker-rules.js +201 -0
- package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
- package/server/lib/code-gate/rules/quality-rules.js +304 -0
- package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
- package/server/lib/code-gate/rules/security-rules.js +228 -0
- package/server/lib/code-gate/rules/security-rules.test.js +131 -0
- package/server/lib/code-gate/rules/structure-rules.js +155 -0
- package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
- package/server/lib/code-gate/rules/test-rules.js +93 -0
- package/server/lib/code-gate/rules/test-rules.test.js +97 -0
- package/server/lib/code-gate/typescript-gate.js +128 -0
- package/server/lib/code-gate/typescript-gate.test.js +131 -0
- package/server/lib/code-generator.test.js +1 -1
- package/server/lib/cost-command.test.js +1 -1
- package/server/lib/cost-optimizer.test.js +1 -1
- package/server/lib/cost-projections.test.js +1 -1
- package/server/lib/cost-reports.test.js +1 -1
- package/server/lib/cost-tracker.test.js +1 -1
- package/server/lib/crypto-patterns.test.js +1 -1
- package/server/lib/design-command.test.js +1 -1
- package/server/lib/design-parser.test.js +1 -1
- package/server/lib/gemini-vision.test.js +1 -1
- package/server/lib/infra/infra-generator.js +331 -0
- package/server/lib/infra/infra-generator.test.js +146 -0
- package/server/lib/input-validator.test.js +1 -1
- package/server/lib/litellm-client.test.js +1 -1
- package/server/lib/litellm-command.test.js +1 -1
- package/server/lib/litellm-config.test.js +1 -1
- package/server/lib/llm/adapters/api-adapter.js +95 -0
- package/server/lib/llm/adapters/api-adapter.test.js +81 -0
- package/server/lib/llm/adapters/codex-adapter.js +85 -0
- package/server/lib/llm/adapters/codex-adapter.test.js +54 -0
- package/server/lib/llm/adapters/gemini-adapter.js +100 -0
- package/server/lib/llm/adapters/gemini-adapter.test.js +54 -0
- package/server/lib/llm/index.js +109 -0
- package/server/lib/llm/index.test.js +147 -0
- package/server/lib/llm/provider-executor.js +168 -0
- package/server/lib/llm/provider-executor.test.js +244 -0
- package/server/lib/llm/provider-registry.js +104 -0
- package/server/lib/llm/provider-registry.test.js +157 -0
- package/server/lib/llm/review-service.js +222 -0
- package/server/lib/llm/review-service.test.js +220 -0
- package/server/lib/model-pricing.test.js +1 -1
- package/server/lib/models-command.test.js +1 -1
- package/server/lib/optimize-command.test.js +1 -1
- package/server/lib/orchestration-integration.test.js +1 -1
- package/server/lib/output-encoder.test.js +1 -1
- package/server/lib/quality-evaluator.test.js +1 -1
- package/server/lib/quality-gate-command.test.js +1 -1
- package/server/lib/quality-gate-scorer.test.js +1 -1
- package/server/lib/quality-history.test.js +1 -1
- package/server/lib/quality-presets.test.js +1 -1
- package/server/lib/quality-retry.test.js +1 -1
- package/server/lib/quality-thresholds.test.js +1 -1
- package/server/lib/secure-auth.test.js +1 -1
- package/server/lib/secure-code-command.test.js +1 -1
- package/server/lib/secure-errors.test.js +1 -1
- package/server/lib/security/auth-security.test.js +4 -3
- package/server/lib/shame/shame-registry.js +224 -0
- package/server/lib/shame/shame-registry.test.js +202 -0
- package/server/lib/standards/cleanup-dry-run.js +254 -0
- package/server/lib/standards/cleanup-dry-run.test.js +220 -0
- package/server/lib/vision-command.test.js +1 -1
- package/server/lib/visual-command.test.js +1 -1
- package/server/lib/visual-testing.test.js +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-Commit Audit Hook
|
|
3
|
+
*
|
|
4
|
+
* Auto-runs architectural audit on first commit to catch
|
|
5
|
+
* AI-generated code issues before they accumulate.
|
|
6
|
+
* The "2-hour audit on day 1 saves 10 days" lesson.
|
|
7
|
+
*
|
|
8
|
+
* @module code-gate/first-commit-audit
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const defaultFs = require('fs').promises;
|
|
13
|
+
|
|
14
|
+
/** Marker file path relative to project root */
|
|
15
|
+
const MARKER_FILE = '.tlc/first-audit-done';
|
|
16
|
+
|
|
17
|
+
/** Fix suggestions by audit issue type */
|
|
18
|
+
const FIX_SUGGESTIONS = {
|
|
19
|
+
'hardcoded-url': 'Extract to environment variable using process.env',
|
|
20
|
+
'hardcoded-port': 'Extract port to environment variable',
|
|
21
|
+
'flat-folder': 'Reorganize into entity-based folder structure (src/{entity}/)',
|
|
22
|
+
'inline-interface': 'Extract interface to separate types file',
|
|
23
|
+
'magic-string': 'Replace with named constant',
|
|
24
|
+
'flat-seeds': 'Move seeds into per-entity seed folders',
|
|
25
|
+
'missing-jsdoc': 'Add JSDoc comment to exported function',
|
|
26
|
+
'deep-import': 'Use path aliases or restructure to reduce nesting',
|
|
27
|
+
'missing': 'Create required standards file',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if the first audit has already run
|
|
32
|
+
* @param {string} projectPath - Path to project root
|
|
33
|
+
* @param {Object} options - Injectable dependencies
|
|
34
|
+
* @param {Object} options.fs - File system module
|
|
35
|
+
* @returns {Promise<boolean>} True if marker exists
|
|
36
|
+
*/
|
|
37
|
+
async function hasFirstAuditRun(projectPath, options = {}) {
|
|
38
|
+
const fsModule = options.fs || defaultFs;
|
|
39
|
+
const markerPath = path.join(projectPath, MARKER_FILE);
|
|
40
|
+
try {
|
|
41
|
+
await fsModule.access(markerPath);
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert audit results to gate findings
|
|
50
|
+
* @param {Object} auditResults - Results from auditProject()
|
|
51
|
+
* @returns {Array<Object>} Gate findings with severity: warn
|
|
52
|
+
*/
|
|
53
|
+
function convertAuditToFindings(auditResults) {
|
|
54
|
+
const findings = [];
|
|
55
|
+
|
|
56
|
+
const categories = [
|
|
57
|
+
'standardsFiles', 'flatFolders', 'inlineInterfaces',
|
|
58
|
+
'hardcodedUrls', 'magicStrings', 'seedOrganization',
|
|
59
|
+
'jsDocCoverage', 'importStyle',
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
for (const category of categories) {
|
|
63
|
+
const result = auditResults[category];
|
|
64
|
+
if (!result || !result.issues) continue;
|
|
65
|
+
|
|
66
|
+
for (const issue of result.issues) {
|
|
67
|
+
findings.push({
|
|
68
|
+
severity: 'warn',
|
|
69
|
+
rule: `first-audit/${issue.type}`,
|
|
70
|
+
file: issue.file || issue.folder || 'project',
|
|
71
|
+
line: 0,
|
|
72
|
+
message: `First-commit audit: ${issue.type}${issue.value ? ` (${issue.value})` : ''}`,
|
|
73
|
+
fix: FIX_SUGGESTIONS[issue.type] || 'Review and fix manually',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return findings;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run the first-commit audit
|
|
83
|
+
* @param {string} projectPath - Path to project root
|
|
84
|
+
* @param {Object} options - Injectable dependencies
|
|
85
|
+
* @param {Object} options.fs - File system module
|
|
86
|
+
* @param {Function} options.auditProject - Audit function from audit-checker
|
|
87
|
+
* @param {Object} options.config - Gate config
|
|
88
|
+
* @returns {Promise<Object>} Result with findings or skipped flag
|
|
89
|
+
*/
|
|
90
|
+
async function runFirstCommitAudit(projectPath, options = {}) {
|
|
91
|
+
const fsModule = options.fs || defaultFs;
|
|
92
|
+
const { auditProject, config } = options;
|
|
93
|
+
|
|
94
|
+
// Check if disabled via config
|
|
95
|
+
if (config && config.firstCommitAudit === false) {
|
|
96
|
+
return { skipped: true, reason: 'disabled' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check if already run
|
|
100
|
+
const alreadyRun = await hasFirstAuditRun(projectPath, { fs: fsModule });
|
|
101
|
+
if (alreadyRun) {
|
|
102
|
+
return { skipped: true, reason: 'already-run' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Run audit
|
|
106
|
+
const auditResults = await auditProject(projectPath, { fs: fsModule });
|
|
107
|
+
const findings = convertAuditToFindings(auditResults);
|
|
108
|
+
|
|
109
|
+
// Create marker file
|
|
110
|
+
const markerPath = path.join(projectPath, MARKER_FILE);
|
|
111
|
+
const markerDir = path.dirname(markerPath);
|
|
112
|
+
await fsModule.mkdir(markerDir, { recursive: true });
|
|
113
|
+
await fsModule.writeFile(markerPath, `First audit completed at ${new Date().toISOString()}\n`);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
skipped: false,
|
|
117
|
+
findings,
|
|
118
|
+
auditResults,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create a first-commit audit instance with dependencies
|
|
124
|
+
* @param {Object} deps - Injectable dependencies
|
|
125
|
+
* @returns {Object} Audit instance with run method
|
|
126
|
+
*/
|
|
127
|
+
function createFirstCommitAudit(deps = {}) {
|
|
128
|
+
return {
|
|
129
|
+
run: (projectPath, config) => runFirstCommitAudit(projectPath, { ...deps, config }),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
createFirstCommitAudit,
|
|
135
|
+
hasFirstAuditRun,
|
|
136
|
+
convertAuditToFindings,
|
|
137
|
+
runFirstCommitAudit,
|
|
138
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-Commit Audit Hook Tests
|
|
3
|
+
*
|
|
4
|
+
* Auto-runs architectural audit on first commit to catch
|
|
5
|
+
* AI-generated code issues before they accumulate.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
createFirstCommitAudit,
|
|
11
|
+
hasFirstAuditRun,
|
|
12
|
+
convertAuditToFindings,
|
|
13
|
+
runFirstCommitAudit,
|
|
14
|
+
} = require('./first-commit-audit.js');
|
|
15
|
+
|
|
16
|
+
describe('First-Commit Audit Hook', () => {
|
|
17
|
+
describe('hasFirstAuditRun', () => {
|
|
18
|
+
it('returns false when no marker exists', async () => {
|
|
19
|
+
const mockFs = {
|
|
20
|
+
access: vi.fn().mockRejectedValue(new Error('ENOENT')),
|
|
21
|
+
};
|
|
22
|
+
const result = await hasFirstAuditRun('/project', { fs: mockFs });
|
|
23
|
+
expect(result).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns true when marker exists', async () => {
|
|
27
|
+
const mockFs = {
|
|
28
|
+
access: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
};
|
|
30
|
+
const result = await hasFirstAuditRun('/project', { fs: mockFs });
|
|
31
|
+
expect(result).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('convertAuditToFindings', () => {
|
|
36
|
+
it('converts audit issues to gate findings with severity warn', () => {
|
|
37
|
+
const auditResults = {
|
|
38
|
+
hardcodedUrls: {
|
|
39
|
+
passed: false,
|
|
40
|
+
issues: [
|
|
41
|
+
{ type: 'hardcoded-url', file: 'src/api.js', value: 'http://localhost:3000' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
flatFolders: {
|
|
45
|
+
passed: false,
|
|
46
|
+
issues: [
|
|
47
|
+
{ type: 'flat-folder', folder: 'services' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
summary: { totalIssues: 2, passed: false },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const findings = convertAuditToFindings(auditResults);
|
|
54
|
+
expect(findings).toHaveLength(2);
|
|
55
|
+
expect(findings[0].severity).toBe('warn');
|
|
56
|
+
expect(findings[1].severity).toBe('warn');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns correct severity for all findings', () => {
|
|
60
|
+
const auditResults = {
|
|
61
|
+
magicStrings: {
|
|
62
|
+
passed: false,
|
|
63
|
+
issues: [
|
|
64
|
+
{ type: 'magic-string', file: 'src/auth.js', value: 'admin' },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
summary: { totalIssues: 1, passed: false },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const findings = convertAuditToFindings(auditResults);
|
|
71
|
+
expect(findings.every(f => f.severity === 'warn')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('includes fix suggestions from audit', () => {
|
|
75
|
+
const auditResults = {
|
|
76
|
+
hardcodedUrls: {
|
|
77
|
+
passed: false,
|
|
78
|
+
issues: [
|
|
79
|
+
{ type: 'hardcoded-url', file: 'src/api.js', value: 'http://localhost:3000' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
summary: { totalIssues: 1, passed: false },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const findings = convertAuditToFindings(auditResults);
|
|
86
|
+
expect(findings[0].fix).toBeDefined();
|
|
87
|
+
expect(findings[0].fix.length).toBeGreaterThan(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns empty array for clean audit', () => {
|
|
91
|
+
const auditResults = {
|
|
92
|
+
hardcodedUrls: { passed: true, issues: [] },
|
|
93
|
+
flatFolders: { passed: true, issues: [] },
|
|
94
|
+
summary: { totalIssues: 0, passed: true },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const findings = convertAuditToFindings(auditResults);
|
|
98
|
+
expect(findings).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('handles multiple issues from same category', () => {
|
|
102
|
+
const auditResults = {
|
|
103
|
+
hardcodedUrls: {
|
|
104
|
+
passed: false,
|
|
105
|
+
issues: [
|
|
106
|
+
{ type: 'hardcoded-url', file: 'src/api.js', value: 'http://localhost:3000' },
|
|
107
|
+
{ type: 'hardcoded-url', file: 'src/config.js', value: 'http://localhost:5000' },
|
|
108
|
+
{ type: 'hardcoded-port', file: 'src/server.js', value: '8080' },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
summary: { totalIssues: 3, passed: false },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const findings = convertAuditToFindings(auditResults);
|
|
115
|
+
expect(findings).toHaveLength(3);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('runFirstCommitAudit', () => {
|
|
120
|
+
it('runs audit when no marker exists', async () => {
|
|
121
|
+
const mockFs = {
|
|
122
|
+
access: vi.fn().mockRejectedValue(new Error('ENOENT')),
|
|
123
|
+
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
124
|
+
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
125
|
+
};
|
|
126
|
+
const mockAuditProject = vi.fn().mockResolvedValue({
|
|
127
|
+
hardcodedUrls: { passed: true, issues: [] },
|
|
128
|
+
summary: { totalIssues: 0, passed: true },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = await runFirstCommitAudit('/project', {
|
|
132
|
+
fs: mockFs,
|
|
133
|
+
auditProject: mockAuditProject,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(mockAuditProject).toHaveBeenCalledWith('/project', { fs: mockFs });
|
|
137
|
+
expect(result.findings).toBeDefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('skips audit when marker exists', async () => {
|
|
141
|
+
const mockFs = {
|
|
142
|
+
access: vi.fn().mockResolvedValue(undefined),
|
|
143
|
+
};
|
|
144
|
+
const mockAuditProject = vi.fn();
|
|
145
|
+
|
|
146
|
+
const result = await runFirstCommitAudit('/project', {
|
|
147
|
+
fs: mockFs,
|
|
148
|
+
auditProject: mockAuditProject,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(mockAuditProject).not.toHaveBeenCalled();
|
|
152
|
+
expect(result.skipped).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('creates marker after successful run', async () => {
|
|
156
|
+
const mockFs = {
|
|
157
|
+
access: vi.fn().mockRejectedValue(new Error('ENOENT')),
|
|
158
|
+
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
159
|
+
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
160
|
+
};
|
|
161
|
+
const mockAuditProject = vi.fn().mockResolvedValue({
|
|
162
|
+
summary: { totalIssues: 0, passed: true },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await runFirstCommitAudit('/project', {
|
|
166
|
+
fs: mockFs,
|
|
167
|
+
auditProject: mockAuditProject,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
|
171
|
+
expect.stringContaining('first-audit-done'),
|
|
172
|
+
expect.any(String)
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('respects enabled/disabled config', async () => {
|
|
177
|
+
const mockFs = {
|
|
178
|
+
access: vi.fn().mockRejectedValue(new Error('ENOENT')),
|
|
179
|
+
};
|
|
180
|
+
const mockAuditProject = vi.fn();
|
|
181
|
+
|
|
182
|
+
const result = await runFirstCommitAudit('/project', {
|
|
183
|
+
fs: mockFs,
|
|
184
|
+
auditProject: mockAuditProject,
|
|
185
|
+
config: { firstCommitAudit: false },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(mockAuditProject).not.toHaveBeenCalled();
|
|
189
|
+
expect(result.skipped).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('createFirstCommitAudit', () => {
|
|
194
|
+
it('works with injectable dependencies', () => {
|
|
195
|
+
const audit = createFirstCommitAudit({
|
|
196
|
+
fs: {},
|
|
197
|
+
auditProject: vi.fn(),
|
|
198
|
+
});
|
|
199
|
+
expect(audit).toBeDefined();
|
|
200
|
+
expect(audit.run).toBeDefined();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Command
|
|
3
|
+
*
|
|
4
|
+
* /tlc:gate command to install, configure, and run the code gate.
|
|
5
|
+
* Subcommands: install, check, status, config
|
|
6
|
+
*
|
|
7
|
+
* @module code-gate/gate-command
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse gate command arguments into structured options.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} args - Raw argument string
|
|
14
|
+
* @returns {{ subcommand: string }}
|
|
15
|
+
*/
|
|
16
|
+
function parseGateArgs(args) {
|
|
17
|
+
const trimmed = (args || '').trim();
|
|
18
|
+
const subcommand = trimmed.split(/\s+/)[0] || 'check';
|
|
19
|
+
|
|
20
|
+
const valid = ['install', 'check', 'status', 'config'];
|
|
21
|
+
return {
|
|
22
|
+
subcommand: valid.includes(subcommand) ? subcommand : 'check',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a gate command with injectable dependencies.
|
|
28
|
+
* This allows testing without real file system or git operations.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} deps - Dependencies
|
|
31
|
+
* @param {string} deps.projectPath - Project root path
|
|
32
|
+
* @param {Function} [deps.installHooks] - Hook installer function
|
|
33
|
+
* @param {Function} [deps.runGate] - Gate engine runner function
|
|
34
|
+
* @param {Function} [deps.getStagedFiles] - Get staged files function
|
|
35
|
+
* @param {Function} [deps.loadConfig] - Config loader function
|
|
36
|
+
* @param {Function} [deps.saveConfig] - Config saver function
|
|
37
|
+
* @param {Function} [deps.isHookInstalled] - Hook check function
|
|
38
|
+
* @returns {{ execute: Function }}
|
|
39
|
+
*/
|
|
40
|
+
function createGateCommand(deps) {
|
|
41
|
+
const {
|
|
42
|
+
projectPath,
|
|
43
|
+
installHooks,
|
|
44
|
+
runGate,
|
|
45
|
+
getStagedFiles,
|
|
46
|
+
loadConfig,
|
|
47
|
+
saveConfig,
|
|
48
|
+
isHookInstalled,
|
|
49
|
+
} = deps;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
/**
|
|
53
|
+
* Execute a gate subcommand.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} subcommand - install|check|status|config
|
|
56
|
+
* @param {Object} [options] - Subcommand-specific options
|
|
57
|
+
* @returns {Promise<Object>} Subcommand result
|
|
58
|
+
*/
|
|
59
|
+
async execute(subcommand, options = {}) {
|
|
60
|
+
switch (subcommand) {
|
|
61
|
+
case 'install':
|
|
62
|
+
return handleInstall();
|
|
63
|
+
case 'check':
|
|
64
|
+
return handleCheck();
|
|
65
|
+
case 'status':
|
|
66
|
+
return handleStatus();
|
|
67
|
+
case 'config':
|
|
68
|
+
return handleConfig(options);
|
|
69
|
+
default:
|
|
70
|
+
return { success: false, error: `Unknown subcommand: ${subcommand}` };
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
async function handleInstall() {
|
|
76
|
+
if (!installHooks) {
|
|
77
|
+
return { success: false, error: 'Hook installer not available' };
|
|
78
|
+
}
|
|
79
|
+
const result = await installHooks(projectPath);
|
|
80
|
+
return { success: true, installed: result.installed };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleCheck() {
|
|
84
|
+
if (!runGate) {
|
|
85
|
+
return { success: false, error: 'Gate engine not available' };
|
|
86
|
+
}
|
|
87
|
+
const files = getStagedFiles ? await getStagedFiles() : [];
|
|
88
|
+
return await runGate(files);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function handleStatus() {
|
|
92
|
+
const config = loadConfig ? loadConfig(projectPath) : {};
|
|
93
|
+
const hooks = {
|
|
94
|
+
'pre-commit': isHookInstalled ? isHookInstalled(projectPath, 'pre-commit') : false,
|
|
95
|
+
'pre-push': isHookInstalled ? isHookInstalled(projectPath, 'pre-push') : false,
|
|
96
|
+
};
|
|
97
|
+
return { config, hooks };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function handleConfig(updates) {
|
|
101
|
+
if (!saveConfig) {
|
|
102
|
+
return { success: false, error: 'Config saver not available' };
|
|
103
|
+
}
|
|
104
|
+
const current = loadConfig ? loadConfig(projectPath) : {};
|
|
105
|
+
const merged = { ...current, ...updates };
|
|
106
|
+
saveConfig(merged);
|
|
107
|
+
return { success: true, config: merged };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
createGateCommand,
|
|
113
|
+
parseGateArgs,
|
|
114
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate Command Tests
|
|
3
|
+
*
|
|
4
|
+
* /tlc:gate command to install, configure, and run the code gate.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
createGateCommand,
|
|
10
|
+
parseGateArgs,
|
|
11
|
+
} = require('./gate-command.js');
|
|
12
|
+
|
|
13
|
+
describe('Gate Command', () => {
|
|
14
|
+
describe('parseGateArgs', () => {
|
|
15
|
+
it('parses install subcommand', () => {
|
|
16
|
+
const args = parseGateArgs('install');
|
|
17
|
+
expect(args.subcommand).toBe('install');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('parses check subcommand', () => {
|
|
21
|
+
const args = parseGateArgs('check');
|
|
22
|
+
expect(args.subcommand).toBe('check');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('parses status subcommand', () => {
|
|
26
|
+
const args = parseGateArgs('status');
|
|
27
|
+
expect(args.subcommand).toBe('status');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('defaults to check when no subcommand', () => {
|
|
31
|
+
const args = parseGateArgs('');
|
|
32
|
+
expect(args.subcommand).toBe('check');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('parses config subcommand', () => {
|
|
36
|
+
const args = parseGateArgs('config');
|
|
37
|
+
expect(args.subcommand).toBe('config');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('createGateCommand', () => {
|
|
42
|
+
it('creates command with injectable dependencies', () => {
|
|
43
|
+
const cmd = createGateCommand({ projectPath: '/test' });
|
|
44
|
+
expect(cmd).toBeDefined();
|
|
45
|
+
expect(cmd.execute).toBeTypeOf('function');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('install subcommand calls hooks installer', async () => {
|
|
49
|
+
const mockInstallHooks = vi.fn().mockResolvedValue({ installed: ['pre-commit', 'pre-push'] });
|
|
50
|
+
const cmd = createGateCommand({
|
|
51
|
+
projectPath: '/test',
|
|
52
|
+
installHooks: mockInstallHooks,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const result = await cmd.execute('install');
|
|
56
|
+
expect(mockInstallHooks).toHaveBeenCalled();
|
|
57
|
+
expect(result.success).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('check subcommand runs gate engine', async () => {
|
|
61
|
+
const mockRunGate = vi.fn().mockResolvedValue({
|
|
62
|
+
passed: true,
|
|
63
|
+
findings: [],
|
|
64
|
+
summary: { total: 0, block: 0, warn: 0, info: 0 },
|
|
65
|
+
});
|
|
66
|
+
const mockGetStagedFiles = vi.fn().mockResolvedValue([]);
|
|
67
|
+
const cmd = createGateCommand({
|
|
68
|
+
projectPath: '/test',
|
|
69
|
+
runGate: mockRunGate,
|
|
70
|
+
getStagedFiles: mockGetStagedFiles,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const result = await cmd.execute('check');
|
|
74
|
+
expect(result.passed).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('status subcommand returns gate configuration', async () => {
|
|
78
|
+
const mockLoadConfig = vi.fn().mockReturnValue({
|
|
79
|
+
enabled: true,
|
|
80
|
+
strictness: 'strict',
|
|
81
|
+
preCommit: true,
|
|
82
|
+
prePush: true,
|
|
83
|
+
});
|
|
84
|
+
const mockIsInstalled = vi.fn().mockReturnValue(true);
|
|
85
|
+
const cmd = createGateCommand({
|
|
86
|
+
projectPath: '/test',
|
|
87
|
+
loadConfig: mockLoadConfig,
|
|
88
|
+
isHookInstalled: mockIsInstalled,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = await cmd.execute('status');
|
|
92
|
+
expect(result.config).toBeDefined();
|
|
93
|
+
expect(result.config.strictness).toBe('strict');
|
|
94
|
+
expect(result.hooks).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('config subcommand updates .tlc.json', async () => {
|
|
98
|
+
let savedConfig = null;
|
|
99
|
+
const mockSaveConfig = vi.fn((config) => { savedConfig = config; });
|
|
100
|
+
const cmd = createGateCommand({
|
|
101
|
+
projectPath: '/test',
|
|
102
|
+
saveConfig: mockSaveConfig,
|
|
103
|
+
loadConfig: vi.fn().mockReturnValue({ enabled: true, strictness: 'strict' }),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await cmd.execute('config', { strictness: 'standard' });
|
|
107
|
+
expect(mockSaveConfig).toHaveBeenCalled();
|
|
108
|
+
expect(result.success).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|