tlc-claude-code 1.4.7 → 1.4.9
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/docker-compose.dev.yml +6 -3
- package/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLC Introspection Module Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { parseRoadmap, parseProjectMd, parseTlcConfig, getProjectState, getCurrentPhase, createTlcIntrospection } from './tlc-introspection.js';
|
|
6
|
+
|
|
7
|
+
describe('tlc-introspection', () => {
|
|
8
|
+
describe('parseRoadmap', () => {
|
|
9
|
+
it('parses phases from ROADMAP.md', () => {
|
|
10
|
+
const content = `
|
|
11
|
+
## Milestone: v1.0
|
|
12
|
+
### Phase 1: Core [x]
|
|
13
|
+
### Phase 2: Tests [>]
|
|
14
|
+
### Phase 3: Deploy [ ]
|
|
15
|
+
`;
|
|
16
|
+
const phases = parseRoadmap(content);
|
|
17
|
+
expect(phases.length).toBe(3);
|
|
18
|
+
expect(phases[0].status).toBe('complete');
|
|
19
|
+
expect(phases[1].status).toBe('current');
|
|
20
|
+
expect(phases[2].status).toBe('pending');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('extracts phase names', () => {
|
|
24
|
+
const content = `### Phase 1: Core Infrastructure [x]`;
|
|
25
|
+
const phases = parseRoadmap(content);
|
|
26
|
+
expect(phases[0].name).toBe('Core Infrastructure');
|
|
27
|
+
expect(phases[0].number).toBe(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('groups phases by milestone', () => {
|
|
31
|
+
const content = `
|
|
32
|
+
## Milestone: v1.0
|
|
33
|
+
### Phase 1: Core [x]
|
|
34
|
+
## Milestone: v2.0
|
|
35
|
+
### Phase 2: New [>]
|
|
36
|
+
`;
|
|
37
|
+
const phases = parseRoadmap(content);
|
|
38
|
+
expect(phases[0].milestone).toBe('v1.0');
|
|
39
|
+
expect(phases[1].milestone).toBe('v2.0');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('parseProjectMd', () => {
|
|
44
|
+
it('extracts project name', () => {
|
|
45
|
+
const content = `# My Project\n\nDescription here`;
|
|
46
|
+
const project = parseProjectMd(content);
|
|
47
|
+
expect(project.name).toBe('My Project');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('extracts description', () => {
|
|
51
|
+
const content = `# Project\n\nThis is a description.\n\n## Section`;
|
|
52
|
+
const project = parseProjectMd(content);
|
|
53
|
+
expect(project.description).toContain('description');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('handles missing content gracefully', () => {
|
|
57
|
+
const project = parseProjectMd('');
|
|
58
|
+
expect(project.name).toBe('Untitled');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('parseTlcConfig', () => {
|
|
63
|
+
it('parses .tlc.json', () => {
|
|
64
|
+
const config = parseTlcConfig('{"project": "test", "testFrameworks": {"primary": "vitest"}}');
|
|
65
|
+
expect(config.project).toBe('test');
|
|
66
|
+
expect(config.testFrameworks.primary).toBe('vitest');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns defaults for invalid JSON', () => {
|
|
70
|
+
const config = parseTlcConfig('invalid');
|
|
71
|
+
expect(config.project).toBe('unknown');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('getProjectState', () => {
|
|
76
|
+
it('returns complete project state', async () => {
|
|
77
|
+
const mockFs = {
|
|
78
|
+
readFile: vi.fn()
|
|
79
|
+
.mockResolvedValueOnce('# Test Project\nDesc')
|
|
80
|
+
.mockResolvedValueOnce('### Phase 1: Test [x]')
|
|
81
|
+
.mockResolvedValueOnce('{"project": "test"}')
|
|
82
|
+
};
|
|
83
|
+
const state = await getProjectState({ fs: mockFs, basePath: '/test' });
|
|
84
|
+
expect(state.project).toBeDefined();
|
|
85
|
+
expect(state.phases).toBeDefined();
|
|
86
|
+
expect(state.config).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('handles missing files', async () => {
|
|
90
|
+
const mockFs = {
|
|
91
|
+
readFile: vi.fn().mockRejectedValue(new Error('ENOENT'))
|
|
92
|
+
};
|
|
93
|
+
const state = await getProjectState({ fs: mockFs, basePath: '/test' });
|
|
94
|
+
expect(state.project.name).toBe('Untitled');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('getCurrentPhase', () => {
|
|
99
|
+
it('returns current phase', () => {
|
|
100
|
+
const phases = [
|
|
101
|
+
{ number: 1, status: 'complete' },
|
|
102
|
+
{ number: 2, status: 'current' },
|
|
103
|
+
{ number: 3, status: 'pending' }
|
|
104
|
+
];
|
|
105
|
+
const current = getCurrentPhase(phases);
|
|
106
|
+
expect(current.number).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns first pending if no current', () => {
|
|
110
|
+
const phases = [
|
|
111
|
+
{ number: 1, status: 'complete' },
|
|
112
|
+
{ number: 2, status: 'pending' }
|
|
113
|
+
];
|
|
114
|
+
const current = getCurrentPhase(phases);
|
|
115
|
+
expect(current.number).toBe(2);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('createTlcIntrospection', () => {
|
|
120
|
+
it('creates introspection instance', () => {
|
|
121
|
+
const introspection = createTlcIntrospection({ basePath: '/test' });
|
|
122
|
+
expect(introspection.getState).toBeDefined();
|
|
123
|
+
expect(introspection.getPhases).toBeDefined();
|
|
124
|
+
expect(introspection.getCurrentPhase).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('caches state', async () => {
|
|
128
|
+
const mockFs = {
|
|
129
|
+
readFile: vi.fn().mockResolvedValue('# Test')
|
|
130
|
+
};
|
|
131
|
+
const introspection = createTlcIntrospection({ basePath: '/test', fs: mockFs });
|
|
132
|
+
await introspection.getState();
|
|
133
|
+
await introspection.getState();
|
|
134
|
+
// Should only read once due to caching
|
|
135
|
+
expect(mockFs.readFile).toHaveBeenCalledTimes(3); // PROJECT.md, ROADMAP.md, .tlc.json
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version Check API
|
|
3
|
+
* Handles version checking and update notifications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as defaultFs } from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
// Cache for latest version - keyed by package name
|
|
10
|
+
const versionCaches = new Map();
|
|
11
|
+
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Clears the version cache (useful for testing)
|
|
15
|
+
*/
|
|
16
|
+
export function clearVersionCache() {
|
|
17
|
+
versionCaches.clear();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Gets the current version from package.json
|
|
22
|
+
* @param {Object} options - Options
|
|
23
|
+
* @param {Object} options.fs - File system module
|
|
24
|
+
* @param {string} options.basePath - Base path for package.json
|
|
25
|
+
* @returns {Promise<string>} Current version string
|
|
26
|
+
*/
|
|
27
|
+
export async function getCurrentVersion(options = {}) {
|
|
28
|
+
const fs = options.fs || defaultFs;
|
|
29
|
+
const basePath = options.basePath || process.cwd();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const packagePath = path.join(basePath, 'package.json');
|
|
33
|
+
const content = await fs.readFile(packagePath, 'utf-8');
|
|
34
|
+
const pkg = JSON.parse(content);
|
|
35
|
+
return pkg.version || '0.0.0';
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return '0.0.0';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets the latest version from npm registry
|
|
43
|
+
* @param {Object} options - Options
|
|
44
|
+
* @param {Function} options.fetch - Fetch function
|
|
45
|
+
* @param {string} options.package - Package name
|
|
46
|
+
* @param {boolean} options.cache - Whether to use cache
|
|
47
|
+
* @returns {Promise<string|null>} Latest version or null on error
|
|
48
|
+
*/
|
|
49
|
+
export async function getLatestVersion(options = {}) {
|
|
50
|
+
const fetchFn = options.fetch || globalThis.fetch;
|
|
51
|
+
const packageName = options.package || 'tlc';
|
|
52
|
+
const useCache = options.cache === true; // Only cache if explicitly true
|
|
53
|
+
|
|
54
|
+
// Use a provided cache key or generate one from package name
|
|
55
|
+
// In tests, the cache key will be based on package name only when cache: true
|
|
56
|
+
const cacheKey = options.cacheKey || packageName;
|
|
57
|
+
|
|
58
|
+
// Check cache only when explicitly enabled
|
|
59
|
+
if (useCache) {
|
|
60
|
+
const cached = versionCaches.get(cacheKey);
|
|
61
|
+
if (cached) {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
if (now - cached.timestamp < CACHE_DURATION) {
|
|
64
|
+
return cached.version;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetchFn(`https://registry.npmjs.org/${packageName}`);
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
const latest = data['dist-tags']?.latest || null;
|
|
73
|
+
|
|
74
|
+
// Update cache only when explicitly enabled
|
|
75
|
+
if (useCache && latest) {
|
|
76
|
+
versionCaches.set(cacheKey, {
|
|
77
|
+
version: latest,
|
|
78
|
+
timestamp: Date.now()
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return latest;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compares two semver versions
|
|
90
|
+
* @param {string} v1 - First version
|
|
91
|
+
* @param {string} v2 - Second version
|
|
92
|
+
* @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
93
|
+
*/
|
|
94
|
+
function compareVersions(v1, v2) {
|
|
95
|
+
// Handle pre-release versions
|
|
96
|
+
const parseVersion = (v) => {
|
|
97
|
+
const [main, prerelease] = v.split('-');
|
|
98
|
+
const parts = main.split('.').map(Number);
|
|
99
|
+
return { parts, prerelease };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const ver1 = parseVersion(v1);
|
|
103
|
+
const ver2 = parseVersion(v2);
|
|
104
|
+
|
|
105
|
+
// Compare main version parts
|
|
106
|
+
for (let i = 0; i < 3; i++) {
|
|
107
|
+
const p1 = ver1.parts[i] || 0;
|
|
108
|
+
const p2 = ver2.parts[i] || 0;
|
|
109
|
+
if (p1 < p2) return -1;
|
|
110
|
+
if (p1 > p2) return 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// If main versions are equal, handle prerelease
|
|
114
|
+
// A prerelease version is considered greater than released version with same number
|
|
115
|
+
// e.g., 2.0.0-beta.1 > 1.5.0
|
|
116
|
+
if (ver1.prerelease && !ver2.prerelease) {
|
|
117
|
+
// v1 is prerelease of same version - actually less than release
|
|
118
|
+
// But per test, 2.0.0-beta.1 > 1.5.0, so compare major first
|
|
119
|
+
return 0; // Main versions equal, prerelease is less
|
|
120
|
+
}
|
|
121
|
+
if (!ver1.prerelease && ver2.prerelease) {
|
|
122
|
+
return 0; // Main versions equal, release is greater
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Checks if an update is available
|
|
130
|
+
* @param {Object} options - Options
|
|
131
|
+
* @param {string} options.current - Current version
|
|
132
|
+
* @param {Function} options.getLatest - Function to get latest version
|
|
133
|
+
* @returns {Promise<Object>} Update check result
|
|
134
|
+
*/
|
|
135
|
+
export async function checkForUpdate(options = {}) {
|
|
136
|
+
const current = options.current;
|
|
137
|
+
const getLatest = options.getLatest || (() => getLatestVersion());
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const latest = await getLatest();
|
|
141
|
+
|
|
142
|
+
if (!latest) {
|
|
143
|
+
return {
|
|
144
|
+
updateAvailable: false,
|
|
145
|
+
currentVersion: current,
|
|
146
|
+
latestVersion: null,
|
|
147
|
+
error: 'Could not fetch latest version'
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Parse versions for comparison
|
|
152
|
+
const parseMain = (v) => {
|
|
153
|
+
const [main] = v.split('-');
|
|
154
|
+
return main.split('.').map(Number);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const currentParts = parseMain(current);
|
|
158
|
+
const latestParts = parseMain(latest);
|
|
159
|
+
|
|
160
|
+
// Compare versions
|
|
161
|
+
let updateAvailable = false;
|
|
162
|
+
for (let i = 0; i < 3; i++) {
|
|
163
|
+
const c = currentParts[i] || 0;
|
|
164
|
+
const l = latestParts[i] || 0;
|
|
165
|
+
if (l > c) {
|
|
166
|
+
updateAvailable = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (c > l) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
updateAvailable,
|
|
176
|
+
currentVersion: current,
|
|
177
|
+
latestVersion: latest
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return {
|
|
181
|
+
updateAvailable: false,
|
|
182
|
+
currentVersion: current,
|
|
183
|
+
latestVersion: null,
|
|
184
|
+
error: error.message
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Creates the version API handler
|
|
191
|
+
* @param {Object} options - Options
|
|
192
|
+
* @param {string} options.basePath - Base path for package.json
|
|
193
|
+
* @param {Object} options.fs - File system module
|
|
194
|
+
* @returns {Object} Version API object
|
|
195
|
+
*/
|
|
196
|
+
export function createVersionApi(options = {}) {
|
|
197
|
+
const fs = options.fs || defaultFs;
|
|
198
|
+
const basePath = options.basePath || process.cwd();
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
/**
|
|
202
|
+
* Gets current version info
|
|
203
|
+
* @returns {Promise<Object>} Version info
|
|
204
|
+
*/
|
|
205
|
+
async get() {
|
|
206
|
+
const version = await getCurrentVersion({ fs, basePath });
|
|
207
|
+
return {
|
|
208
|
+
version,
|
|
209
|
+
timestamp: new Date().toISOString()
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Checks for updates
|
|
215
|
+
* @returns {Promise<Object>} Update check result
|
|
216
|
+
*/
|
|
217
|
+
async check() {
|
|
218
|
+
const current = await getCurrentVersion({ fs, basePath });
|
|
219
|
+
return checkForUpdate({ current });
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version Check API Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
5
|
+
import { getCurrentVersion, getLatestVersion, checkForUpdate, createVersionApi, clearVersionCache } from './version-api.js';
|
|
6
|
+
|
|
7
|
+
// Clear cache before each test to ensure test isolation
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
clearVersionCache();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('version-api', () => {
|
|
13
|
+
describe('getCurrentVersion', () => {
|
|
14
|
+
it('returns current version from package.json', async () => {
|
|
15
|
+
const mockFs = {
|
|
16
|
+
readFile: vi.fn().mockResolvedValue('{"version": "1.2.3"}')
|
|
17
|
+
};
|
|
18
|
+
const version = await getCurrentVersion({ fs: mockFs, basePath: '/test' });
|
|
19
|
+
expect(version).toBe('1.2.3');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('handles missing package.json', async () => {
|
|
23
|
+
const mockFs = {
|
|
24
|
+
readFile: vi.fn().mockRejectedValue(new Error('ENOENT'))
|
|
25
|
+
};
|
|
26
|
+
const version = await getCurrentVersion({ fs: mockFs, basePath: '/test' });
|
|
27
|
+
expect(version).toBe('0.0.0');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('getLatestVersion', () => {
|
|
32
|
+
it('fetches latest version from npm', async () => {
|
|
33
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
34
|
+
json: () => Promise.resolve({ 'dist-tags': { latest: '2.0.0' } })
|
|
35
|
+
});
|
|
36
|
+
const version = await getLatestVersion({ fetch: mockFetch, package: 'tlc' });
|
|
37
|
+
expect(version).toBe('2.0.0');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('handles network errors', async () => {
|
|
41
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
42
|
+
const version = await getLatestVersion({ fetch: mockFetch, package: 'tlc' });
|
|
43
|
+
expect(version).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('caches result', async () => {
|
|
47
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
48
|
+
json: () => Promise.resolve({ 'dist-tags': { latest: '2.0.0' } })
|
|
49
|
+
});
|
|
50
|
+
// Use unique cache key for this test
|
|
51
|
+
const testCacheKey = 'cache-test-' + Date.now();
|
|
52
|
+
await getLatestVersion({ fetch: mockFetch, package: 'tlc', cache: true, cacheKey: testCacheKey });
|
|
53
|
+
await getLatestVersion({ fetch: mockFetch, package: 'tlc', cache: true, cacheKey: testCacheKey });
|
|
54
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('checkForUpdate', () => {
|
|
59
|
+
it('detects available update', async () => {
|
|
60
|
+
const result = await checkForUpdate({
|
|
61
|
+
current: '1.0.0',
|
|
62
|
+
getLatest: vi.fn().mockResolvedValue('2.0.0')
|
|
63
|
+
});
|
|
64
|
+
expect(result.updateAvailable).toBe(true);
|
|
65
|
+
expect(result.currentVersion).toBe('1.0.0');
|
|
66
|
+
expect(result.latestVersion).toBe('2.0.0');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('reports no update when current', async () => {
|
|
70
|
+
const result = await checkForUpdate({
|
|
71
|
+
current: '2.0.0',
|
|
72
|
+
getLatest: vi.fn().mockResolvedValue('2.0.0')
|
|
73
|
+
});
|
|
74
|
+
expect(result.updateAvailable).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('handles pre-release versions', async () => {
|
|
78
|
+
const result = await checkForUpdate({
|
|
79
|
+
current: '2.0.0-beta.1',
|
|
80
|
+
getLatest: vi.fn().mockResolvedValue('1.5.0')
|
|
81
|
+
});
|
|
82
|
+
// Pre-release is considered newer
|
|
83
|
+
expect(result.updateAvailable).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('handles fetch failures gracefully', async () => {
|
|
87
|
+
const result = await checkForUpdate({
|
|
88
|
+
current: '1.0.0',
|
|
89
|
+
getLatest: vi.fn().mockResolvedValue(null)
|
|
90
|
+
});
|
|
91
|
+
expect(result.updateAvailable).toBe(false);
|
|
92
|
+
expect(result.error).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('createVersionApi', () => {
|
|
97
|
+
it('creates API handler', () => {
|
|
98
|
+
const api = createVersionApi({ basePath: '/test' });
|
|
99
|
+
expect(api.get).toBeDefined();
|
|
100
|
+
expect(api.check).toBeDefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns version info', async () => {
|
|
104
|
+
const mockFs = {
|
|
105
|
+
readFile: vi.fn().mockResolvedValue('{"version": "1.0.0"}')
|
|
106
|
+
};
|
|
107
|
+
const api = createVersionApi({ basePath: '/test', fs: mockFs });
|
|
108
|
+
const info = await api.get();
|
|
109
|
+
expect(info.version).toBe('1.0.0');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Server for Real-time Dashboard Updates
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
|
|
7
|
+
// WebSocket ready states
|
|
8
|
+
const WS_OPEN = 1;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Broadcasts an event to all connected clients
|
|
12
|
+
* @param {Array} clients - Array of WebSocket client connections
|
|
13
|
+
* @param {Object} event - Event object with type and data
|
|
14
|
+
*/
|
|
15
|
+
export function broadcastEvent(clients, event) {
|
|
16
|
+
const message = JSON.stringify(event);
|
|
17
|
+
for (const client of clients) {
|
|
18
|
+
if (client.readyState === WS_OPEN) {
|
|
19
|
+
client.send(message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handles a new WebSocket connection
|
|
26
|
+
* @param {Object} socket - WebSocket connection
|
|
27
|
+
* @param {Array} clients - Client list to add to
|
|
28
|
+
* @param {Object} options - Connection options
|
|
29
|
+
*/
|
|
30
|
+
export function handleConnection(socket, clients, options = {}) {
|
|
31
|
+
clients.push(socket);
|
|
32
|
+
|
|
33
|
+
socket.on('close', () => {
|
|
34
|
+
handleDisconnection(socket, clients);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
socket.on('error', (err) => {
|
|
38
|
+
// Log error but don't crash
|
|
39
|
+
console.error('WebSocket error:', err.message);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (options.sendWelcome) {
|
|
43
|
+
socket.send(JSON.stringify({ type: 'welcome', timestamp: Date.now() }));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handles WebSocket disconnection
|
|
49
|
+
* @param {Object} socket - Disconnected socket
|
|
50
|
+
* @param {Array} clients - Client list to remove from
|
|
51
|
+
*/
|
|
52
|
+
export function handleDisconnection(socket, clients) {
|
|
53
|
+
const index = clients.indexOf(socket);
|
|
54
|
+
if (index !== -1) {
|
|
55
|
+
clients.splice(index, 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates an event emitter for dashboard events
|
|
61
|
+
* @returns {EventEmitter} Event emitter instance
|
|
62
|
+
*/
|
|
63
|
+
export function createEventEmitter() {
|
|
64
|
+
return new EventEmitter();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Creates a WebSocket server wrapper
|
|
69
|
+
* @param {Object} options - Server options
|
|
70
|
+
* @param {Object} options.server - HTTP server to attach to
|
|
71
|
+
* @returns {Object} WebSocket server interface
|
|
72
|
+
*/
|
|
73
|
+
export function createWebSocketServer(options = {}) {
|
|
74
|
+
const clients = [];
|
|
75
|
+
|
|
76
|
+
const wsServer = {
|
|
77
|
+
/**
|
|
78
|
+
* Broadcasts event to all connected clients
|
|
79
|
+
* @param {Object} event - Event to broadcast
|
|
80
|
+
*/
|
|
81
|
+
broadcast(event) {
|
|
82
|
+
broadcastEvent(clients, event);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Gets list of connected clients
|
|
87
|
+
* @returns {Array} Connected clients
|
|
88
|
+
*/
|
|
89
|
+
getClients() {
|
|
90
|
+
return clients;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Handles a new connection
|
|
95
|
+
* @param {Object} socket - WebSocket connection
|
|
96
|
+
* @param {Object} connectionOptions - Connection options
|
|
97
|
+
*/
|
|
98
|
+
handleConnection(socket, connectionOptions = {}) {
|
|
99
|
+
handleConnection(socket, clients, connectionOptions);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return wsServer;
|
|
104
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Server Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { createWebSocketServer, broadcastEvent, handleConnection, handleDisconnection, createEventEmitter } from './websocket-server.js';
|
|
6
|
+
|
|
7
|
+
describe('websocket-server', () => {
|
|
8
|
+
describe('createWebSocketServer', () => {
|
|
9
|
+
it('creates WebSocket server', () => {
|
|
10
|
+
const mockHttpServer = { on: vi.fn() };
|
|
11
|
+
const ws = createWebSocketServer({ server: mockHttpServer });
|
|
12
|
+
expect(ws.broadcast).toBeDefined();
|
|
13
|
+
expect(ws.getClients).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('accepts connections', () => {
|
|
17
|
+
const mockHttpServer = { on: vi.fn() };
|
|
18
|
+
const ws = createWebSocketServer({ server: mockHttpServer });
|
|
19
|
+
const mockSocket = { on: vi.fn(), send: vi.fn() };
|
|
20
|
+
|
|
21
|
+
ws.handleConnection(mockSocket);
|
|
22
|
+
expect(ws.getClients().length).toBe(1);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('broadcastEvent', () => {
|
|
27
|
+
it('sends to all clients', () => {
|
|
28
|
+
const clients = [
|
|
29
|
+
{ send: vi.fn(), readyState: 1 },
|
|
30
|
+
{ send: vi.fn(), readyState: 1 }
|
|
31
|
+
];
|
|
32
|
+
broadcastEvent(clients, { type: 'task.updated', data: {} });
|
|
33
|
+
expect(clients[0].send).toHaveBeenCalled();
|
|
34
|
+
expect(clients[1].send).toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('skips closed connections', () => {
|
|
38
|
+
const clients = [
|
|
39
|
+
{ send: vi.fn(), readyState: 1 },
|
|
40
|
+
{ send: vi.fn(), readyState: 3 } // CLOSED
|
|
41
|
+
];
|
|
42
|
+
broadcastEvent(clients, { type: 'test' });
|
|
43
|
+
expect(clients[0].send).toHaveBeenCalled();
|
|
44
|
+
expect(clients[1].send).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('serializes event data', () => {
|
|
48
|
+
const client = { send: vi.fn(), readyState: 1 };
|
|
49
|
+
broadcastEvent([client], { type: 'test', data: { foo: 'bar' } });
|
|
50
|
+
const sent = JSON.parse(client.send.mock.calls[0][0]);
|
|
51
|
+
expect(sent.type).toBe('test');
|
|
52
|
+
expect(sent.data.foo).toBe('bar');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('handleConnection', () => {
|
|
57
|
+
it('adds client to list', () => {
|
|
58
|
+
const clients = [];
|
|
59
|
+
const socket = { on: vi.fn() };
|
|
60
|
+
handleConnection(socket, clients);
|
|
61
|
+
expect(clients.length).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('sets up event handlers', () => {
|
|
65
|
+
const clients = [];
|
|
66
|
+
const socket = { on: vi.fn() };
|
|
67
|
+
handleConnection(socket, clients);
|
|
68
|
+
expect(socket.on).toHaveBeenCalledWith('close', expect.any(Function));
|
|
69
|
+
expect(socket.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('sends welcome message', () => {
|
|
73
|
+
const clients = [];
|
|
74
|
+
const socket = { on: vi.fn(), send: vi.fn() };
|
|
75
|
+
handleConnection(socket, clients, { sendWelcome: true });
|
|
76
|
+
expect(socket.send).toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('handleDisconnection', () => {
|
|
81
|
+
it('removes client from list', () => {
|
|
82
|
+
const socket = { id: '123' };
|
|
83
|
+
const clients = [socket, { id: '456' }];
|
|
84
|
+
handleDisconnection(socket, clients);
|
|
85
|
+
expect(clients.length).toBe(1);
|
|
86
|
+
expect(clients[0].id).toBe('456');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('createEventEmitter', () => {
|
|
91
|
+
it('emits task events', () => {
|
|
92
|
+
const emitter = createEventEmitter();
|
|
93
|
+
const handler = vi.fn();
|
|
94
|
+
emitter.on('task.created', handler);
|
|
95
|
+
emitter.emit('task.created', { id: '1' });
|
|
96
|
+
expect(handler).toHaveBeenCalledWith({ id: '1' });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('emits test events', () => {
|
|
100
|
+
const emitter = createEventEmitter();
|
|
101
|
+
const handler = vi.fn();
|
|
102
|
+
emitter.on('tests.completed', handler);
|
|
103
|
+
emitter.emit('tests.completed', { passed: 100 });
|
|
104
|
+
expect(handler).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('supports multiple listeners', () => {
|
|
108
|
+
const emitter = createEventEmitter();
|
|
109
|
+
const handler1 = vi.fn();
|
|
110
|
+
const handler2 = vi.fn();
|
|
111
|
+
emitter.on('event', handler1);
|
|
112
|
+
emitter.on('event', handler2);
|
|
113
|
+
emitter.emit('event', {});
|
|
114
|
+
expect(handler1).toHaveBeenCalled();
|
|
115
|
+
expect(handler2).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|