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.
Files changed (105) hide show
  1. package/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
  2. package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
  3. package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
  5. package/package.json +1 -1
  6. package/server/lib/access-control.test.js +1 -1
  7. package/server/lib/agents-cancel-command.test.js +1 -1
  8. package/server/lib/agents-get-command.test.js +1 -1
  9. package/server/lib/agents-list-command.test.js +1 -1
  10. package/server/lib/agents-logs-command.test.js +1 -1
  11. package/server/lib/agents-retry-command.test.js +1 -1
  12. package/server/lib/budget-limits.test.js +2 -2
  13. package/server/lib/code-gate/bypass-logger.js +129 -0
  14. package/server/lib/code-gate/bypass-logger.test.js +142 -0
  15. package/server/lib/code-gate/first-commit-audit.js +138 -0
  16. package/server/lib/code-gate/first-commit-audit.test.js +203 -0
  17. package/server/lib/code-gate/gate-command.js +114 -0
  18. package/server/lib/code-gate/gate-command.test.js +111 -0
  19. package/server/lib/code-gate/gate-config.js +163 -0
  20. package/server/lib/code-gate/gate-config.test.js +181 -0
  21. package/server/lib/code-gate/gate-engine.js +193 -0
  22. package/server/lib/code-gate/gate-engine.test.js +258 -0
  23. package/server/lib/code-gate/gate-reporter.js +123 -0
  24. package/server/lib/code-gate/gate-reporter.test.js +159 -0
  25. package/server/lib/code-gate/hooks-generator.js +149 -0
  26. package/server/lib/code-gate/hooks-generator.test.js +142 -0
  27. package/server/lib/code-gate/llm-reviewer.js +176 -0
  28. package/server/lib/code-gate/llm-reviewer.test.js +161 -0
  29. package/server/lib/code-gate/multi-model-reviewer.js +172 -0
  30. package/server/lib/code-gate/multi-model-reviewer.test.js +217 -0
  31. package/server/lib/code-gate/push-gate.js +133 -0
  32. package/server/lib/code-gate/push-gate.test.js +190 -0
  33. package/server/lib/code-gate/rules/architecture-rules.js +228 -0
  34. package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
  35. package/server/lib/code-gate/rules/client-rules.js +120 -0
  36. package/server/lib/code-gate/rules/client-rules.test.js +121 -0
  37. package/server/lib/code-gate/rules/config-rules.js +140 -0
  38. package/server/lib/code-gate/rules/config-rules.test.js +103 -0
  39. package/server/lib/code-gate/rules/database-rules.js +158 -0
  40. package/server/lib/code-gate/rules/database-rules.test.js +119 -0
  41. package/server/lib/code-gate/rules/docker-rules.js +201 -0
  42. package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
  43. package/server/lib/code-gate/rules/quality-rules.js +304 -0
  44. package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
  45. package/server/lib/code-gate/rules/security-rules.js +228 -0
  46. package/server/lib/code-gate/rules/security-rules.test.js +131 -0
  47. package/server/lib/code-gate/rules/structure-rules.js +155 -0
  48. package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
  49. package/server/lib/code-gate/rules/test-rules.js +93 -0
  50. package/server/lib/code-gate/rules/test-rules.test.js +97 -0
  51. package/server/lib/code-gate/typescript-gate.js +128 -0
  52. package/server/lib/code-gate/typescript-gate.test.js +131 -0
  53. package/server/lib/code-generator.test.js +1 -1
  54. package/server/lib/cost-command.test.js +1 -1
  55. package/server/lib/cost-optimizer.test.js +1 -1
  56. package/server/lib/cost-projections.test.js +1 -1
  57. package/server/lib/cost-reports.test.js +1 -1
  58. package/server/lib/cost-tracker.test.js +1 -1
  59. package/server/lib/crypto-patterns.test.js +1 -1
  60. package/server/lib/design-command.test.js +1 -1
  61. package/server/lib/design-parser.test.js +1 -1
  62. package/server/lib/gemini-vision.test.js +1 -1
  63. package/server/lib/infra/infra-generator.js +331 -0
  64. package/server/lib/infra/infra-generator.test.js +146 -0
  65. package/server/lib/input-validator.test.js +1 -1
  66. package/server/lib/litellm-client.test.js +1 -1
  67. package/server/lib/litellm-command.test.js +1 -1
  68. package/server/lib/litellm-config.test.js +1 -1
  69. package/server/lib/llm/adapters/api-adapter.js +95 -0
  70. package/server/lib/llm/adapters/api-adapter.test.js +81 -0
  71. package/server/lib/llm/adapters/codex-adapter.js +85 -0
  72. package/server/lib/llm/adapters/codex-adapter.test.js +54 -0
  73. package/server/lib/llm/adapters/gemini-adapter.js +100 -0
  74. package/server/lib/llm/adapters/gemini-adapter.test.js +54 -0
  75. package/server/lib/llm/index.js +109 -0
  76. package/server/lib/llm/index.test.js +147 -0
  77. package/server/lib/llm/provider-executor.js +168 -0
  78. package/server/lib/llm/provider-executor.test.js +244 -0
  79. package/server/lib/llm/provider-registry.js +104 -0
  80. package/server/lib/llm/provider-registry.test.js +157 -0
  81. package/server/lib/llm/review-service.js +222 -0
  82. package/server/lib/llm/review-service.test.js +220 -0
  83. package/server/lib/model-pricing.test.js +1 -1
  84. package/server/lib/models-command.test.js +1 -1
  85. package/server/lib/optimize-command.test.js +1 -1
  86. package/server/lib/orchestration-integration.test.js +1 -1
  87. package/server/lib/output-encoder.test.js +1 -1
  88. package/server/lib/quality-evaluator.test.js +1 -1
  89. package/server/lib/quality-gate-command.test.js +1 -1
  90. package/server/lib/quality-gate-scorer.test.js +1 -1
  91. package/server/lib/quality-history.test.js +1 -1
  92. package/server/lib/quality-presets.test.js +1 -1
  93. package/server/lib/quality-retry.test.js +1 -1
  94. package/server/lib/quality-thresholds.test.js +1 -1
  95. package/server/lib/secure-auth.test.js +1 -1
  96. package/server/lib/secure-code-command.test.js +1 -1
  97. package/server/lib/secure-errors.test.js +1 -1
  98. package/server/lib/security/auth-security.test.js +4 -3
  99. package/server/lib/shame/shame-registry.js +224 -0
  100. package/server/lib/shame/shame-registry.test.js +202 -0
  101. package/server/lib/standards/cleanup-dry-run.js +254 -0
  102. package/server/lib/standards/cleanup-dry-run.test.js +220 -0
  103. package/server/lib/vision-command.test.js +1 -1
  104. package/server/lib/visual-command.test.js +1 -1
  105. 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
+ });