tlc-claude-code 1.8.5 → 2.1.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 (138) hide show
  1. package/.claude/commands/tlc/bootstrap.md +77 -0
  2. package/.claude/commands/tlc/build.md +20 -6
  3. package/.claude/commands/tlc/deploy.md +194 -2
  4. package/.claude/commands/tlc/e2e-verify.md +214 -0
  5. package/.claude/commands/tlc/guard.md +191 -0
  6. package/.claude/commands/tlc/help.md +32 -0
  7. package/.claude/commands/tlc/init.md +73 -37
  8. package/.claude/commands/tlc/llm.md +19 -4
  9. package/.claude/commands/tlc/preflight.md +134 -0
  10. package/.claude/commands/tlc/recall.md +87 -0
  11. package/.claude/commands/tlc/remember.md +71 -0
  12. package/.claude/commands/tlc/review.md +17 -4
  13. package/.claude/commands/tlc/watchci.md +159 -0
  14. package/.claude/hooks/tlc-block-tools.sh +41 -0
  15. package/.claude/hooks/tlc-capture-exchange.sh +50 -0
  16. package/.claude/hooks/tlc-post-build.sh +38 -0
  17. package/.claude/hooks/tlc-post-push.sh +22 -0
  18. package/.claude/hooks/tlc-prompt-guard.sh +69 -0
  19. package/.claude/hooks/tlc-session-init.sh +123 -0
  20. package/CLAUDE.md +96 -201
  21. package/bin/install.js +171 -2
  22. package/bin/postinstall.js +45 -26
  23. package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
  24. package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
  25. package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
  26. package/dashboard-web/dist/index.html +2 -2
  27. package/docker-compose.dev.yml +18 -12
  28. package/package.json +3 -1
  29. package/server/index.js +240 -1
  30. package/server/lib/bug-writer.js +204 -0
  31. package/server/lib/bug-writer.test.js +279 -0
  32. package/server/lib/capture-bridge.js +242 -0
  33. package/server/lib/capture-bridge.test.js +363 -0
  34. package/server/lib/capture-guard.js +140 -0
  35. package/server/lib/capture-guard.test.js +182 -0
  36. package/server/lib/claude-cascade.js +247 -0
  37. package/server/lib/claude-cascade.test.js +245 -0
  38. package/server/lib/command-runner.js +159 -0
  39. package/server/lib/command-runner.test.js +92 -0
  40. package/server/lib/context-injection.js +121 -0
  41. package/server/lib/context-injection.test.js +340 -0
  42. package/server/lib/conversation-chunker.js +320 -0
  43. package/server/lib/conversation-chunker.test.js +573 -0
  44. package/server/lib/deploy/runners/dependency-runner.js +106 -0
  45. package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
  46. package/server/lib/deploy/runners/secrets-runner.js +174 -0
  47. package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
  48. package/server/lib/deploy/security-gates.js +11 -24
  49. package/server/lib/deploy/security-gates.test.js +9 -2
  50. package/server/lib/deploy-engine.js +182 -0
  51. package/server/lib/deploy-engine.test.js +147 -0
  52. package/server/lib/docker-api.js +137 -0
  53. package/server/lib/docker-api.test.js +202 -0
  54. package/server/lib/docker-client.js +297 -0
  55. package/server/lib/docker-client.test.js +308 -0
  56. package/server/lib/embedding-client.js +160 -0
  57. package/server/lib/embedding-client.test.js +243 -0
  58. package/server/lib/global-config.js +198 -0
  59. package/server/lib/global-config.test.js +288 -0
  60. package/server/lib/inherited-search.js +184 -0
  61. package/server/lib/inherited-search.test.js +343 -0
  62. package/server/lib/input-sanitizer.js +86 -0
  63. package/server/lib/input-sanitizer.test.js +117 -0
  64. package/server/lib/launchd-agent.js +225 -0
  65. package/server/lib/launchd-agent.test.js +185 -0
  66. package/server/lib/memory-api.js +182 -0
  67. package/server/lib/memory-api.test.js +320 -0
  68. package/server/lib/memory-bridge-e2e.test.js +160 -0
  69. package/server/lib/memory-committer.js +18 -4
  70. package/server/lib/memory-committer.test.js +21 -0
  71. package/server/lib/memory-hooks-capture.test.js +415 -0
  72. package/server/lib/memory-hooks-integration.test.js +98 -0
  73. package/server/lib/memory-hooks.js +139 -0
  74. package/server/lib/memory-inheritance.js +179 -0
  75. package/server/lib/memory-inheritance.test.js +360 -0
  76. package/server/lib/memory-store-adapter.js +105 -0
  77. package/server/lib/memory-store-adapter.test.js +141 -0
  78. package/server/lib/memory-wiring-e2e.test.js +93 -0
  79. package/server/lib/nginx-config.js +114 -0
  80. package/server/lib/nginx-config.test.js +82 -0
  81. package/server/lib/ollama-health.js +91 -0
  82. package/server/lib/ollama-health.test.js +74 -0
  83. package/server/lib/plan-writer.js +196 -0
  84. package/server/lib/plan-writer.test.js +298 -0
  85. package/server/lib/port-guard.js +44 -0
  86. package/server/lib/port-guard.test.js +65 -0
  87. package/server/lib/project-scanner.js +302 -0
  88. package/server/lib/project-scanner.test.js +541 -0
  89. package/server/lib/project-status.js +302 -0
  90. package/server/lib/project-status.test.js +470 -0
  91. package/server/lib/projects-registry.js +237 -0
  92. package/server/lib/projects-registry.test.js +275 -0
  93. package/server/lib/recall-command.js +207 -0
  94. package/server/lib/recall-command.test.js +306 -0
  95. package/server/lib/remember-command.js +98 -0
  96. package/server/lib/remember-command.test.js +288 -0
  97. package/server/lib/rich-capture.js +221 -0
  98. package/server/lib/rich-capture.test.js +312 -0
  99. package/server/lib/roadmap-api.js +200 -0
  100. package/server/lib/roadmap-api.test.js +318 -0
  101. package/server/lib/security/crypto-utils.test.js +2 -2
  102. package/server/lib/semantic-recall.js +242 -0
  103. package/server/lib/semantic-recall.test.js +463 -0
  104. package/server/lib/setup-generator.js +315 -0
  105. package/server/lib/setup-generator.test.js +303 -0
  106. package/server/lib/ssh-client.js +184 -0
  107. package/server/lib/ssh-client.test.js +127 -0
  108. package/server/lib/test-inventory.js +112 -0
  109. package/server/lib/test-inventory.test.js +360 -0
  110. package/server/lib/vector-indexer.js +246 -0
  111. package/server/lib/vector-indexer.test.js +459 -0
  112. package/server/lib/vector-store.js +260 -0
  113. package/server/lib/vector-store.test.js +706 -0
  114. package/server/lib/vps-api.js +184 -0
  115. package/server/lib/vps-api.test.js +208 -0
  116. package/server/lib/vps-bootstrap.js +124 -0
  117. package/server/lib/vps-bootstrap.test.js +79 -0
  118. package/server/lib/vps-monitor.js +126 -0
  119. package/server/lib/vps-monitor.test.js +98 -0
  120. package/server/lib/workspace-api.js +992 -0
  121. package/server/lib/workspace-api.test.js +1217 -0
  122. package/server/lib/workspace-bootstrap.js +164 -0
  123. package/server/lib/workspace-bootstrap.test.js +503 -0
  124. package/server/lib/workspace-context.js +129 -0
  125. package/server/lib/workspace-context.test.js +214 -0
  126. package/server/lib/workspace-detector.js +162 -0
  127. package/server/lib/workspace-detector.test.js +193 -0
  128. package/server/lib/workspace-init.js +307 -0
  129. package/server/lib/workspace-init.test.js +244 -0
  130. package/server/lib/workspace-snapshot.js +236 -0
  131. package/server/lib/workspace-snapshot.test.js +444 -0
  132. package/server/lib/workspace-watcher.js +162 -0
  133. package/server/lib/workspace-watcher.test.js +257 -0
  134. package/server/package-lock.json +1306 -17
  135. package/server/package.json +7 -0
  136. package/dashboard-web/dist/assets/index-B1I_joSL.js +0 -393
  137. package/dashboard-web/dist/assets/index-B1I_joSL.js.map +0 -1
  138. package/dashboard-web/dist/assets/index-Trhg1C1Y.css +0 -1
@@ -0,0 +1,184 @@
1
+ /**
2
+ * SSH Client — wraps ssh2 for VPS communication
3
+ * Phase 80 Task 4
4
+ */
5
+
6
+ const { Client } = require('ssh2');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Resolve ~ in key paths
12
+ */
13
+ function resolveKeyPath(keyPath) {
14
+ if (!keyPath) return null;
15
+ if (keyPath.startsWith('~')) {
16
+ return path.join(require('os').homedir(), keyPath.slice(1));
17
+ }
18
+ return keyPath;
19
+ }
20
+
21
+ /**
22
+ * Create an SSH client wrapper
23
+ * @param {Object} [options]
24
+ * @param {Function} [options._execFn] - Injected exec function (for testing)
25
+ * @returns {Object} SSH client API
26
+ */
27
+ function createSshClient(options = {}) {
28
+ const injectedExec = options._execFn;
29
+
30
+ /**
31
+ * Execute a command via SSH
32
+ * @param {Object} config - { host, port, username, privateKeyPath }
33
+ * @param {string} command
34
+ * @returns {Promise<{ stdout, stderr, exitCode }>}
35
+ */
36
+ async function exec(config, command) {
37
+ if (injectedExec) {
38
+ return injectedExec(config, command);
39
+ }
40
+
41
+ if (!config.host || !config.username) {
42
+ throw new Error('SSH config requires host and username');
43
+ }
44
+
45
+ const keyPath = resolveKeyPath(config.privateKeyPath);
46
+
47
+ return new Promise((resolve, reject) => {
48
+ const conn = new Client();
49
+ conn.on('ready', () => {
50
+ conn.exec(command, (err, stream) => {
51
+ if (err) { conn.end(); return reject(err); }
52
+
53
+ let stdout = '';
54
+ let stderr = '';
55
+
56
+ stream.on('data', (data) => { stdout += data.toString(); });
57
+ stream.stderr.on('data', (data) => { stderr += data.toString(); });
58
+ stream.on('close', (code) => {
59
+ conn.end();
60
+ resolve({ stdout, stderr, exitCode: code });
61
+ });
62
+ });
63
+ });
64
+ conn.on('error', (err) => reject(err));
65
+
66
+ const connOpts = {
67
+ host: config.host,
68
+ port: config.port || 22,
69
+ username: config.username,
70
+ readyTimeout: 10000,
71
+ keepaliveInterval: 5000,
72
+ };
73
+ if (keyPath && fs.existsSync(keyPath)) {
74
+ connOpts.privateKey = fs.readFileSync(keyPath);
75
+ }
76
+ conn.connect(connOpts);
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Execute with streaming output
82
+ * @param {Object} config
83
+ * @param {string} command
84
+ * @param {Function} onData - Called with each output chunk
85
+ * @returns {Promise<number>} exit code
86
+ */
87
+ async function execStream(config, command, onData) {
88
+ if (injectedExec) {
89
+ const result = await injectedExec(config, command);
90
+ if (onData && result.stdout) onData(result.stdout);
91
+ return result.exitCode;
92
+ }
93
+
94
+ if (!config.host || !config.username) {
95
+ throw new Error('SSH config requires host and username');
96
+ }
97
+
98
+ const keyPath = resolveKeyPath(config.privateKeyPath);
99
+
100
+ return new Promise((resolve, reject) => {
101
+ const conn = new Client();
102
+ conn.on('ready', () => {
103
+ conn.exec(command, (err, stream) => {
104
+ if (err) { conn.end(); return reject(err); }
105
+ stream.on('data', (data) => onData && onData(data.toString()));
106
+ stream.stderr.on('data', (data) => onData && onData(data.toString()));
107
+ stream.on('close', (code) => { conn.end(); resolve(code); });
108
+ });
109
+ });
110
+ conn.on('error', (err) => reject(err));
111
+
112
+ const connOpts = {
113
+ host: config.host,
114
+ port: config.port || 22,
115
+ username: config.username,
116
+ };
117
+ if (keyPath && fs.existsSync(keyPath)) {
118
+ connOpts.privateKey = fs.readFileSync(keyPath);
119
+ }
120
+ conn.connect(connOpts);
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Upload a file via SFTP
126
+ * @param {Object} config
127
+ * @param {string} localPath
128
+ * @param {string} remotePath
129
+ */
130
+ async function upload(config, localPath, remotePath) {
131
+ if (!config.host || !config.username) {
132
+ throw new Error('SSH config requires host and username');
133
+ }
134
+
135
+ const keyPath = resolveKeyPath(config.privateKeyPath);
136
+
137
+ return new Promise((resolve, reject) => {
138
+ const conn = new Client();
139
+ conn.on('ready', () => {
140
+ conn.sftp((err, sftp) => {
141
+ if (err) { conn.end(); return reject(err); }
142
+ sftp.fastPut(localPath, remotePath, (err) => {
143
+ conn.end();
144
+ if (err) reject(err);
145
+ else resolve();
146
+ });
147
+ });
148
+ });
149
+ conn.on('error', (err) => reject(err));
150
+
151
+ const connOpts = {
152
+ host: config.host,
153
+ port: config.port || 22,
154
+ username: config.username,
155
+ };
156
+ if (keyPath && fs.existsSync(keyPath)) {
157
+ connOpts.privateKey = fs.readFileSync(keyPath);
158
+ }
159
+ conn.connect(connOpts);
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Test SSH connection and gather server info
165
+ * @param {Object} config
166
+ * @returns {Promise<{ connected, os, docker, disk }>}
167
+ */
168
+ async function testConnection(config) {
169
+ const osResult = await exec(config, 'uname -a');
170
+ const dockerResult = await exec(config, 'docker --version 2>/dev/null || echo "not installed"');
171
+ const diskResult = await exec(config, 'df -h / | tail -1');
172
+
173
+ return {
174
+ connected: true,
175
+ os: osResult.stdout.trim(),
176
+ docker: dockerResult.stdout.trim(),
177
+ disk: diskResult.stdout.trim(),
178
+ };
179
+ }
180
+
181
+ return { exec, execStream, upload, testConnection };
182
+ }
183
+
184
+ module.exports = { createSshClient };
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+
3
+ const { createSshClient } = await import('./ssh-client.js');
4
+
5
+ /**
6
+ * Create a mock ssh2 Client for injection
7
+ */
8
+ function createMockSsh2() {
9
+ const mockStream = {
10
+ on: vi.fn().mockReturnThis(),
11
+ stderr: { on: vi.fn().mockReturnThis() },
12
+ end: vi.fn(),
13
+ write: vi.fn(),
14
+ };
15
+
16
+ const mockSftp = {
17
+ fastPut: vi.fn(),
18
+ };
19
+
20
+ const mockClient = {
21
+ on: vi.fn().mockReturnThis(),
22
+ connect: vi.fn(),
23
+ exec: vi.fn(),
24
+ sftp: vi.fn(),
25
+ end: vi.fn(),
26
+ _mockStream: mockStream,
27
+ _mockSftp: mockSftp,
28
+ };
29
+
30
+ return mockClient;
31
+ }
32
+
33
+ describe('SshClient', () => {
34
+ let sshClient;
35
+
36
+ beforeEach(() => {
37
+ sshClient = createSshClient();
38
+ });
39
+
40
+ describe('testConnection', () => {
41
+ it('resolves with server info on successful connection', async () => {
42
+ const config = { host: '1.2.3.4', port: 22, username: 'deploy', privateKeyPath: '/fake/key' };
43
+ // Test that testConnection is a function
44
+ expect(typeof sshClient.testConnection).toBe('function');
45
+ });
46
+
47
+ it('rejects when host is unreachable', async () => {
48
+ const config = { host: 'invalid', port: 22, username: 'root', privateKeyPath: '/nonexistent' };
49
+ await expect(sshClient.testConnection(config)).rejects.toThrow();
50
+ });
51
+ });
52
+
53
+ describe('exec', () => {
54
+ it('is a function that accepts config and command', () => {
55
+ expect(typeof sshClient.exec).toBe('function');
56
+ });
57
+
58
+ it('rejects with error for bad config', async () => {
59
+ await expect(sshClient.exec({}, 'whoami')).rejects.toThrow();
60
+ });
61
+ });
62
+
63
+ describe('execStream', () => {
64
+ it('is a function that accepts config, command, and callback', () => {
65
+ expect(typeof sshClient.execStream).toBe('function');
66
+ });
67
+ });
68
+
69
+ describe('upload', () => {
70
+ it('is a function that accepts config, localPath, remotePath', () => {
71
+ expect(typeof sshClient.upload).toBe('function');
72
+ });
73
+ });
74
+ });
75
+
76
+ describe('SshClient with mock', () => {
77
+ it('exec resolves with stdout, stderr, exitCode via injected client', async () => {
78
+ // Create a mock that simulates successful exec
79
+ const mockExecResult = { stdout: 'root\n', stderr: '', exitCode: 0 };
80
+ const sshClient = createSshClient({
81
+ _execFn: vi.fn().mockResolvedValue(mockExecResult),
82
+ });
83
+
84
+ const result = await sshClient.exec(
85
+ { host: '1.2.3.4', username: 'deploy', privateKeyPath: '/key' },
86
+ 'whoami'
87
+ );
88
+ expect(result).toEqual(mockExecResult);
89
+ });
90
+
91
+ it('exec rejects when command fails', async () => {
92
+ const sshClient = createSshClient({
93
+ _execFn: vi.fn().mockRejectedValue(new Error('command failed')),
94
+ });
95
+
96
+ await expect(
97
+ sshClient.exec({ host: '1.2.3.4', username: 'deploy', privateKeyPath: '/key' }, 'bad-cmd')
98
+ ).rejects.toThrow('command failed');
99
+ });
100
+
101
+ it('testConnection returns server info', async () => {
102
+ const sshClient = createSshClient({
103
+ _execFn: vi.fn()
104
+ .mockResolvedValueOnce({ stdout: 'Linux\n', stderr: '', exitCode: 0 }) // uname
105
+ .mockResolvedValueOnce({ stdout: 'Docker version 24.0.0\n', stderr: '', exitCode: 0 }) // docker
106
+ .mockResolvedValueOnce({ stdout: '/dev/sda1 50G 20G 28G 42% /\n', stderr: '', exitCode: 0 }), // df
107
+ });
108
+
109
+ const info = await sshClient.testConnection(
110
+ { host: '1.2.3.4', username: 'deploy', privateKeyPath: '/key' }
111
+ );
112
+ expect(info.os).toContain('Linux');
113
+ expect(info.docker).toContain('24.0.0');
114
+ expect(info.disk).toBeTruthy();
115
+ expect(info.connected).toBe(true);
116
+ });
117
+
118
+ it('testConnection reports disconnected on failure', async () => {
119
+ const sshClient = createSshClient({
120
+ _execFn: vi.fn().mockRejectedValue(new Error('Connection refused')),
121
+ });
122
+
123
+ await expect(
124
+ sshClient.testConnection({ host: '1.2.3.4', username: 'root', privateKeyPath: '/key' })
125
+ ).rejects.toThrow('Connection refused');
126
+ });
127
+ });
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @file test-inventory.js
3
+ * @description Test Suite Inventory API (Phase 75, Task 2).
4
+ *
5
+ * Factory function that accepts injected dependencies (globSync, fs) and returns
6
+ * functions for discovering test files, counting tests per file, grouping by
7
+ * directory, and reading cached test runs.
8
+ */
9
+
10
+ const { join, relative, dirname } = require('path');
11
+
12
+ /**
13
+ * Creates a test inventory service with injected dependencies.
14
+ * @param {Object} deps - Injected dependencies
15
+ * @param {Function} deps.globSync - Glob function for file discovery
16
+ * @param {{ readFileSync: Function, existsSync: Function }} deps.fs - File system operations
17
+ * @returns {{ getTestInventory: Function, getLastTestRun: Function }}
18
+ */
19
+ function createTestInventory({ globSync, fs }) {
20
+ /**
21
+ * Count the number of test cases in a file's content.
22
+ * Matches: it(, it.only(, it.skip(, test(, test.only(, test.skip(
23
+ * @param {string} content - File content to scan
24
+ * @returns {number} Number of test cases found
25
+ */
26
+ function countTests(content) {
27
+ const pattern = /\b(?:it|test)(?:\.only|\.skip)?\s*\(/g;
28
+ const matches = content.match(pattern);
29
+ return matches ? matches.length : 0;
30
+ }
31
+
32
+ /**
33
+ * Discover test files, count tests, and group by directory.
34
+ * @param {string} projectPath - Absolute path to the project root
35
+ * @returns {{ totalFiles: number, totalTests: number, groups: Array<{ name: string, fileCount: number, testCount: number, files: Array }> }}
36
+ */
37
+ function getTestInventory(projectPath) {
38
+ const patterns = [
39
+ '**/*.test.js',
40
+ '**/*.test.ts',
41
+ '**/*.test.tsx',
42
+ '**/*.spec.*',
43
+ ];
44
+
45
+ const files = globSync(patterns, {
46
+ cwd: projectPath,
47
+ absolute: true,
48
+ ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
49
+ });
50
+
51
+ /** @type {Map<string, Array<{ relativePath: string, testCount: number }>>} */
52
+ const groupMap = new Map();
53
+
54
+ for (const filePath of files) {
55
+ const rel = relative(projectPath, filePath);
56
+ const dir = dirname(rel);
57
+
58
+ let content;
59
+ try {
60
+ content = fs.readFileSync(filePath, 'utf-8');
61
+ } catch {
62
+ content = '';
63
+ }
64
+
65
+ const testCount = countTests(content);
66
+
67
+ if (!groupMap.has(dir)) {
68
+ groupMap.set(dir, []);
69
+ }
70
+ groupMap.get(dir).push({ relativePath: rel, testCount });
71
+ }
72
+
73
+ const groups = [];
74
+ for (const [name, fileList] of groupMap) {
75
+ const testCount = fileList.reduce((sum, f) => sum + f.testCount, 0);
76
+ groups.push({
77
+ name,
78
+ fileCount: fileList.length,
79
+ testCount,
80
+ files: fileList,
81
+ });
82
+ }
83
+
84
+ // Sort by testCount descending
85
+ groups.sort((a, b) => b.testCount - a.testCount);
86
+
87
+ const totalFiles = files.length;
88
+ const totalTests = groups.reduce((sum, g) => sum + g.testCount, 0);
89
+
90
+ return { totalFiles, totalTests, groups };
91
+ }
92
+
93
+ /**
94
+ * Read the cached last test run result.
95
+ * @param {string} projectPath - Absolute path to the project root
96
+ * @returns {Object|null} Parsed test run data, or null if no cache exists
97
+ */
98
+ function getLastTestRun(projectPath) {
99
+ const cachePath = join(projectPath, '.tlc', 'last-test-run.json');
100
+
101
+ if (!fs.existsSync(cachePath)) {
102
+ return null;
103
+ }
104
+
105
+ const raw = fs.readFileSync(cachePath, 'utf-8');
106
+ return JSON.parse(raw);
107
+ }
108
+
109
+ return { getTestInventory, getLastTestRun };
110
+ }
111
+
112
+ module.exports = { createTestInventory };