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.
- package/.claude/commands/tlc/bootstrap.md +77 -0
- package/.claude/commands/tlc/build.md +20 -6
- package/.claude/commands/tlc/deploy.md +194 -2
- package/.claude/commands/tlc/e2e-verify.md +214 -0
- package/.claude/commands/tlc/guard.md +191 -0
- package/.claude/commands/tlc/help.md +32 -0
- package/.claude/commands/tlc/init.md +73 -37
- package/.claude/commands/tlc/llm.md +19 -4
- package/.claude/commands/tlc/preflight.md +134 -0
- package/.claude/commands/tlc/recall.md +87 -0
- package/.claude/commands/tlc/remember.md +71 -0
- package/.claude/commands/tlc/review.md +17 -4
- package/.claude/commands/tlc/watchci.md +159 -0
- package/.claude/hooks/tlc-block-tools.sh +41 -0
- package/.claude/hooks/tlc-capture-exchange.sh +50 -0
- package/.claude/hooks/tlc-post-build.sh +38 -0
- package/.claude/hooks/tlc-post-push.sh +22 -0
- package/.claude/hooks/tlc-prompt-guard.sh +69 -0
- package/.claude/hooks/tlc-session-init.sh +123 -0
- package/CLAUDE.md +96 -201
- package/bin/install.js +171 -2
- package/bin/postinstall.js +45 -26
- package/dashboard-web/dist/assets/index-CdS5CHqu.css +1 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js +483 -0
- package/dashboard-web/dist/assets/index-CwNPPVpg.js.map +1 -0
- package/dashboard-web/dist/index.html +2 -2
- package/docker-compose.dev.yml +18 -12
- package/package.json +3 -1
- package/server/index.js +240 -1
- package/server/lib/bug-writer.js +204 -0
- package/server/lib/bug-writer.test.js +279 -0
- package/server/lib/capture-bridge.js +242 -0
- package/server/lib/capture-bridge.test.js +363 -0
- package/server/lib/capture-guard.js +140 -0
- package/server/lib/capture-guard.test.js +182 -0
- package/server/lib/claude-cascade.js +247 -0
- package/server/lib/claude-cascade.test.js +245 -0
- package/server/lib/command-runner.js +159 -0
- package/server/lib/command-runner.test.js +92 -0
- package/server/lib/context-injection.js +121 -0
- package/server/lib/context-injection.test.js +340 -0
- package/server/lib/conversation-chunker.js +320 -0
- package/server/lib/conversation-chunker.test.js +573 -0
- package/server/lib/deploy/runners/dependency-runner.js +106 -0
- package/server/lib/deploy/runners/dependency-runner.test.js +148 -0
- package/server/lib/deploy/runners/secrets-runner.js +174 -0
- package/server/lib/deploy/runners/secrets-runner.test.js +127 -0
- package/server/lib/deploy/security-gates.js +11 -24
- package/server/lib/deploy/security-gates.test.js +9 -2
- package/server/lib/deploy-engine.js +182 -0
- package/server/lib/deploy-engine.test.js +147 -0
- package/server/lib/docker-api.js +137 -0
- package/server/lib/docker-api.test.js +202 -0
- package/server/lib/docker-client.js +297 -0
- package/server/lib/docker-client.test.js +308 -0
- package/server/lib/embedding-client.js +160 -0
- package/server/lib/embedding-client.test.js +243 -0
- package/server/lib/global-config.js +198 -0
- package/server/lib/global-config.test.js +288 -0
- package/server/lib/inherited-search.js +184 -0
- package/server/lib/inherited-search.test.js +343 -0
- package/server/lib/input-sanitizer.js +86 -0
- package/server/lib/input-sanitizer.test.js +117 -0
- package/server/lib/launchd-agent.js +225 -0
- package/server/lib/launchd-agent.test.js +185 -0
- package/server/lib/memory-api.js +182 -0
- package/server/lib/memory-api.test.js +320 -0
- package/server/lib/memory-bridge-e2e.test.js +160 -0
- package/server/lib/memory-committer.js +18 -4
- package/server/lib/memory-committer.test.js +21 -0
- package/server/lib/memory-hooks-capture.test.js +415 -0
- package/server/lib/memory-hooks-integration.test.js +98 -0
- package/server/lib/memory-hooks.js +139 -0
- package/server/lib/memory-inheritance.js +179 -0
- package/server/lib/memory-inheritance.test.js +360 -0
- package/server/lib/memory-store-adapter.js +105 -0
- package/server/lib/memory-store-adapter.test.js +141 -0
- package/server/lib/memory-wiring-e2e.test.js +93 -0
- package/server/lib/nginx-config.js +114 -0
- package/server/lib/nginx-config.test.js +82 -0
- package/server/lib/ollama-health.js +91 -0
- package/server/lib/ollama-health.test.js +74 -0
- package/server/lib/plan-writer.js +196 -0
- package/server/lib/plan-writer.test.js +298 -0
- package/server/lib/port-guard.js +44 -0
- package/server/lib/port-guard.test.js +65 -0
- package/server/lib/project-scanner.js +302 -0
- package/server/lib/project-scanner.test.js +541 -0
- package/server/lib/project-status.js +302 -0
- package/server/lib/project-status.test.js +470 -0
- package/server/lib/projects-registry.js +237 -0
- package/server/lib/projects-registry.test.js +275 -0
- package/server/lib/recall-command.js +207 -0
- package/server/lib/recall-command.test.js +306 -0
- package/server/lib/remember-command.js +98 -0
- package/server/lib/remember-command.test.js +288 -0
- package/server/lib/rich-capture.js +221 -0
- package/server/lib/rich-capture.test.js +312 -0
- package/server/lib/roadmap-api.js +200 -0
- package/server/lib/roadmap-api.test.js +318 -0
- package/server/lib/security/crypto-utils.test.js +2 -2
- package/server/lib/semantic-recall.js +242 -0
- package/server/lib/semantic-recall.test.js +463 -0
- package/server/lib/setup-generator.js +315 -0
- package/server/lib/setup-generator.test.js +303 -0
- package/server/lib/ssh-client.js +184 -0
- package/server/lib/ssh-client.test.js +127 -0
- package/server/lib/test-inventory.js +112 -0
- package/server/lib/test-inventory.test.js +360 -0
- package/server/lib/vector-indexer.js +246 -0
- package/server/lib/vector-indexer.test.js +459 -0
- package/server/lib/vector-store.js +260 -0
- package/server/lib/vector-store.test.js +706 -0
- package/server/lib/vps-api.js +184 -0
- package/server/lib/vps-api.test.js +208 -0
- package/server/lib/vps-bootstrap.js +124 -0
- package/server/lib/vps-bootstrap.test.js +79 -0
- package/server/lib/vps-monitor.js +126 -0
- package/server/lib/vps-monitor.test.js +98 -0
- package/server/lib/workspace-api.js +992 -0
- package/server/lib/workspace-api.test.js +1217 -0
- package/server/lib/workspace-bootstrap.js +164 -0
- package/server/lib/workspace-bootstrap.test.js +503 -0
- package/server/lib/workspace-context.js +129 -0
- package/server/lib/workspace-context.test.js +214 -0
- package/server/lib/workspace-detector.js +162 -0
- package/server/lib/workspace-detector.test.js +193 -0
- package/server/lib/workspace-init.js +307 -0
- package/server/lib/workspace-init.test.js +244 -0
- package/server/lib/workspace-snapshot.js +236 -0
- package/server/lib/workspace-snapshot.test.js +444 -0
- package/server/lib/workspace-watcher.js +162 -0
- package/server/lib/workspace-watcher.test.js +257 -0
- package/server/package-lock.json +1306 -17
- package/server/package.json +7 -0
- package/dashboard-web/dist/assets/index-B1I_joSL.js +0 -393
- package/dashboard-web/dist/assets/index-B1I_joSL.js.map +0 -1
- package/dashboard-web/dist/assets/index-Trhg1C1Y.css +0 -1
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
const { ProjectScanner } = await import('./project-scanner.js');
|
|
7
|
+
|
|
8
|
+
describe('ProjectScanner', () => {
|
|
9
|
+
let tempDir;
|
|
10
|
+
let scanner;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'project-scanner-test-'));
|
|
14
|
+
scanner = new ProjectScanner();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Helper: create a minimal TLC project directory structure
|
|
23
|
+
*/
|
|
24
|
+
function createTlcProject(parentDir, name, options = {}) {
|
|
25
|
+
const projectDir = path.join(parentDir, name);
|
|
26
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
// .tlc.json
|
|
29
|
+
if (options.tlcJson !== false) {
|
|
30
|
+
fs.writeFileSync(
|
|
31
|
+
path.join(projectDir, '.tlc.json'),
|
|
32
|
+
JSON.stringify(options.tlcJson || { name })
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// package.json
|
|
37
|
+
if (options.packageJson !== false) {
|
|
38
|
+
const pkg = options.packageJson || { name, version: '1.0.0' };
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
path.join(projectDir, 'package.json'),
|
|
41
|
+
JSON.stringify(pkg)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// .planning directory
|
|
46
|
+
if (options.planning !== false) {
|
|
47
|
+
fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// .git directory
|
|
51
|
+
if (options.git !== false) {
|
|
52
|
+
fs.mkdirSync(path.join(projectDir, '.git'), { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ROADMAP.md
|
|
56
|
+
if (options.roadmap) {
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
path.join(projectDir, '.planning', 'ROADMAP.md'),
|
|
59
|
+
options.roadmap
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return projectDir;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Test 1: Discovers project with .tlc.json
|
|
67
|
+
it('discovers project with .tlc.json', () => {
|
|
68
|
+
createTlcProject(tempDir, 'alpha-project');
|
|
69
|
+
|
|
70
|
+
const results = scanner.scan([tempDir]);
|
|
71
|
+
|
|
72
|
+
expect(results).toHaveLength(1);
|
|
73
|
+
expect(results[0].name).toBe('alpha-project');
|
|
74
|
+
expect(results[0].hasTlc).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Test 2: Discovers project with .planning/ directory (no .tlc.json)
|
|
78
|
+
it('discovers project with .planning/ directory (no .tlc.json)', () => {
|
|
79
|
+
createTlcProject(tempDir, 'planning-only', {
|
|
80
|
+
tlcJson: false,
|
|
81
|
+
packageJson: { name: 'planning-only', version: '2.0.0' },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const results = scanner.scan([tempDir]);
|
|
85
|
+
|
|
86
|
+
expect(results).toHaveLength(1);
|
|
87
|
+
expect(results[0].name).toBe('planning-only');
|
|
88
|
+
expect(results[0].hasTlc).toBe(false);
|
|
89
|
+
expect(results[0].hasPlanning).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Test 3: Discovers candidate project with package.json + .git/ (marked hasTlc: false)
|
|
93
|
+
it('discovers candidate project with package.json + .git/ (marked hasTlc: false)', () => {
|
|
94
|
+
createTlcProject(tempDir, 'candidate-project', {
|
|
95
|
+
tlcJson: false,
|
|
96
|
+
planning: false,
|
|
97
|
+
packageJson: { name: 'candidate-project', version: '0.1.0' },
|
|
98
|
+
git: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const results = scanner.scan([tempDir]);
|
|
102
|
+
|
|
103
|
+
expect(results).toHaveLength(1);
|
|
104
|
+
expect(results[0].name).toBe('candidate-project');
|
|
105
|
+
expect(results[0].hasTlc).toBe(false);
|
|
106
|
+
expect(results[0].hasPlanning).toBe(false);
|
|
107
|
+
expect(results[0].version).toBe('0.1.0');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Test 4: Skips node_modules directories
|
|
111
|
+
it('skips node_modules directories', () => {
|
|
112
|
+
// Create a project inside node_modules — should be ignored
|
|
113
|
+
const nmDir = path.join(tempDir, 'node_modules');
|
|
114
|
+
fs.mkdirSync(nmDir, { recursive: true });
|
|
115
|
+
createTlcProject(nmDir, 'hidden-project');
|
|
116
|
+
|
|
117
|
+
// Create a valid project at the top level
|
|
118
|
+
createTlcProject(tempDir, 'visible-project');
|
|
119
|
+
|
|
120
|
+
const results = scanner.scan([tempDir]);
|
|
121
|
+
|
|
122
|
+
expect(results).toHaveLength(1);
|
|
123
|
+
expect(results[0].name).toBe('visible-project');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Test 5: Skips all ignored directories (dist, build, coverage, etc.)
|
|
127
|
+
it('skips all ignored directories (dist, build, coverage, vendor, .next, .nuxt)', () => {
|
|
128
|
+
const ignoreDirs = ['dist', 'build', 'coverage', 'vendor', '.next', '.nuxt'];
|
|
129
|
+
|
|
130
|
+
for (const dir of ignoreDirs) {
|
|
131
|
+
const ignored = path.join(tempDir, dir);
|
|
132
|
+
fs.mkdirSync(ignored, { recursive: true });
|
|
133
|
+
createTlcProject(ignored, `project-in-${dir}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// One valid project
|
|
137
|
+
createTlcProject(tempDir, 'valid-project');
|
|
138
|
+
|
|
139
|
+
const results = scanner.scan([tempDir]);
|
|
140
|
+
|
|
141
|
+
expect(results).toHaveLength(1);
|
|
142
|
+
expect(results[0].name).toBe('valid-project');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Test 6: Respects depth limit (default 5)
|
|
146
|
+
it('respects depth limit (default 5)', () => {
|
|
147
|
+
// Create a project nested 4 levels deep — within default depth of 5
|
|
148
|
+
let nested = tempDir;
|
|
149
|
+
for (let i = 0; i < 4; i++) {
|
|
150
|
+
nested = path.join(nested, `level${i}`);
|
|
151
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
createTlcProject(nested, 'deep-project');
|
|
154
|
+
|
|
155
|
+
const results = scanner.scan([tempDir]);
|
|
156
|
+
|
|
157
|
+
expect(results).toHaveLength(1);
|
|
158
|
+
expect(results[0].name).toBe('deep-project');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Test 7: Depth 1 only scans immediate children
|
|
162
|
+
it('depth 1 only scans immediate children', () => {
|
|
163
|
+
const shallowScanner = new ProjectScanner({ scanDepth: 1 });
|
|
164
|
+
|
|
165
|
+
// Immediate child — should be found
|
|
166
|
+
createTlcProject(tempDir, 'shallow-project');
|
|
167
|
+
|
|
168
|
+
// Nested one level deeper — should NOT be found
|
|
169
|
+
const subDir = path.join(tempDir, 'subdir');
|
|
170
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
171
|
+
createTlcProject(subDir, 'nested-project');
|
|
172
|
+
|
|
173
|
+
const results = shallowScanner.scan([tempDir]);
|
|
174
|
+
|
|
175
|
+
expect(results).toHaveLength(1);
|
|
176
|
+
expect(results[0].name).toBe('shallow-project');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Test 8: Returns empty array for empty root directory
|
|
180
|
+
it('returns empty array for empty root directory', () => {
|
|
181
|
+
const results = scanner.scan([tempDir]);
|
|
182
|
+
|
|
183
|
+
expect(results).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Test 9: Handles multiple root paths
|
|
187
|
+
it('handles multiple root paths', () => {
|
|
188
|
+
const root1 = fs.mkdtempSync(path.join(os.tmpdir(), 'root1-'));
|
|
189
|
+
const root2 = fs.mkdtempSync(path.join(os.tmpdir(), 'root2-'));
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
createTlcProject(root1, 'project-a');
|
|
193
|
+
createTlcProject(root2, 'project-b');
|
|
194
|
+
|
|
195
|
+
const results = scanner.scan([root1, root2]);
|
|
196
|
+
|
|
197
|
+
expect(results).toHaveLength(2);
|
|
198
|
+
const names = results.map(r => r.name);
|
|
199
|
+
expect(names).toContain('project-a');
|
|
200
|
+
expect(names).toContain('project-b');
|
|
201
|
+
} finally {
|
|
202
|
+
fs.rmSync(root1, { recursive: true, force: true });
|
|
203
|
+
fs.rmSync(root2, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Test 10: Deduplicates projects found in overlapping roots
|
|
208
|
+
it('deduplicates projects found in overlapping roots', () => {
|
|
209
|
+
createTlcProject(tempDir, 'unique-project');
|
|
210
|
+
|
|
211
|
+
// Pass the same root twice
|
|
212
|
+
const results = scanner.scan([tempDir, tempDir]);
|
|
213
|
+
|
|
214
|
+
expect(results).toHaveLength(1);
|
|
215
|
+
expect(results[0].name).toBe('unique-project');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Test 11: Caches results on repeated calls within TTL
|
|
219
|
+
it('caches results on repeated calls within TTL', () => {
|
|
220
|
+
createTlcProject(tempDir, 'cached-project');
|
|
221
|
+
|
|
222
|
+
const results1 = scanner.scan([tempDir]);
|
|
223
|
+
expect(results1).toHaveLength(1);
|
|
224
|
+
|
|
225
|
+
// Remove the project on disk
|
|
226
|
+
fs.rmSync(path.join(tempDir, 'cached-project'), { recursive: true, force: true });
|
|
227
|
+
|
|
228
|
+
// Second scan within TTL should return cached results
|
|
229
|
+
const results2 = scanner.scan([tempDir]);
|
|
230
|
+
expect(results2).toHaveLength(1);
|
|
231
|
+
expect(results2[0].name).toBe('cached-project');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Test 12: Force re-scan bypasses cache
|
|
235
|
+
it('force re-scan bypasses cache', () => {
|
|
236
|
+
createTlcProject(tempDir, 'cached-project');
|
|
237
|
+
|
|
238
|
+
const results1 = scanner.scan([tempDir]);
|
|
239
|
+
expect(results1).toHaveLength(1);
|
|
240
|
+
|
|
241
|
+
// Remove the project on disk
|
|
242
|
+
fs.rmSync(path.join(tempDir, 'cached-project'), { recursive: true, force: true });
|
|
243
|
+
|
|
244
|
+
// Force re-scan should see the project is gone
|
|
245
|
+
const results2 = scanner.scan([tempDir], { force: true });
|
|
246
|
+
expect(results2).toHaveLength(0);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Test 13: Handles permission denied errors gracefully
|
|
250
|
+
it('handles permission denied errors gracefully', () => {
|
|
251
|
+
createTlcProject(tempDir, 'accessible-project');
|
|
252
|
+
|
|
253
|
+
// Create a directory that can't be read
|
|
254
|
+
const restrictedDir = path.join(tempDir, 'restricted');
|
|
255
|
+
fs.mkdirSync(restrictedDir, { recursive: true });
|
|
256
|
+
fs.chmodSync(restrictedDir, 0o000);
|
|
257
|
+
|
|
258
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const results = scanner.scan([tempDir]);
|
|
262
|
+
|
|
263
|
+
// Should still find the accessible project
|
|
264
|
+
expect(results).toHaveLength(1);
|
|
265
|
+
expect(results[0].name).toBe('accessible-project');
|
|
266
|
+
|
|
267
|
+
// Should have logged a warning about the restricted dir
|
|
268
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
269
|
+
} finally {
|
|
270
|
+
warnSpy.mockRestore();
|
|
271
|
+
// Restore permissions for cleanup
|
|
272
|
+
fs.chmodSync(restrictedDir, 0o755);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Test 14: Returns project metadata (name, version from package.json)
|
|
277
|
+
it('returns project metadata (name, version from package.json)', () => {
|
|
278
|
+
createTlcProject(tempDir, 'meta-project', {
|
|
279
|
+
packageJson: { name: 'meta-project', version: '3.2.1' },
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const results = scanner.scan([tempDir]);
|
|
283
|
+
|
|
284
|
+
expect(results).toHaveLength(1);
|
|
285
|
+
expect(results[0].name).toBe('meta-project');
|
|
286
|
+
expect(results[0].path).toBe(path.join(tempDir, 'meta-project'));
|
|
287
|
+
expect(results[0].version).toBe('3.2.1');
|
|
288
|
+
expect(results[0].hasTlc).toBe(true);
|
|
289
|
+
expect(results[0].hasPlanning).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Test 15: Reads phase info from ROADMAP.md (heading format)
|
|
293
|
+
it('reads phase info from ROADMAP.md (heading format)', () => {
|
|
294
|
+
const roadmap = [
|
|
295
|
+
'# Roadmap',
|
|
296
|
+
'',
|
|
297
|
+
'### Phase 1: Core Infrastructure [x]',
|
|
298
|
+
'',
|
|
299
|
+
'### Phase 2: Test Quality [x]',
|
|
300
|
+
'',
|
|
301
|
+
'### Phase 3: Dev Server [>]',
|
|
302
|
+
'',
|
|
303
|
+
'### Phase 4: API Docs [ ]',
|
|
304
|
+
'',
|
|
305
|
+
'### Phase 5: CI/CD [ ]',
|
|
306
|
+
].join('\n');
|
|
307
|
+
|
|
308
|
+
createTlcProject(tempDir, 'roadmap-heading-project', { roadmap });
|
|
309
|
+
|
|
310
|
+
const results = scanner.scan([tempDir]);
|
|
311
|
+
|
|
312
|
+
expect(results).toHaveLength(1);
|
|
313
|
+
expect(results[0].phase).toBe(3);
|
|
314
|
+
expect(results[0].phaseName).toBe('Dev Server');
|
|
315
|
+
expect(results[0].totalPhases).toBe(5);
|
|
316
|
+
expect(results[0].completedPhases).toBe(2);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Test 16: Reads phase info from ROADMAP.md (table format)
|
|
320
|
+
it('reads phase info from ROADMAP.md (table format)', () => {
|
|
321
|
+
const roadmap = [
|
|
322
|
+
'# Roadmap',
|
|
323
|
+
'',
|
|
324
|
+
'| # | Phase | Status |',
|
|
325
|
+
'|---|-------|--------|',
|
|
326
|
+
'| 1 | [Auth](./phases/1-PLAN.md) | complete |',
|
|
327
|
+
'| 2 | [API](./phases/2-PLAN.md) | done |',
|
|
328
|
+
'| 3 | [UI](./phases/3-PLAN.md) | active |',
|
|
329
|
+
'| 4 | [Deploy](./phases/4-PLAN.md) | pending |',
|
|
330
|
+
].join('\n');
|
|
331
|
+
|
|
332
|
+
createTlcProject(tempDir, 'roadmap-table-project', { roadmap });
|
|
333
|
+
|
|
334
|
+
const results = scanner.scan([tempDir]);
|
|
335
|
+
|
|
336
|
+
expect(results).toHaveLength(1);
|
|
337
|
+
expect(results[0].phase).toBe(3);
|
|
338
|
+
expect(results[0].phaseName).toBe('UI');
|
|
339
|
+
expect(results[0].totalPhases).toBe(4);
|
|
340
|
+
expect(results[0].completedPhases).toBe(2);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Test 17: Projects sorted alphabetically by name
|
|
344
|
+
it('projects sorted alphabetically by name', () => {
|
|
345
|
+
createTlcProject(tempDir, 'zulu-project');
|
|
346
|
+
createTlcProject(tempDir, 'alpha-project');
|
|
347
|
+
createTlcProject(tempDir, 'mike-project');
|
|
348
|
+
|
|
349
|
+
const results = scanner.scan([tempDir]);
|
|
350
|
+
|
|
351
|
+
expect(results).toHaveLength(3);
|
|
352
|
+
expect(results[0].name).toBe('alpha-project');
|
|
353
|
+
expect(results[1].name).toBe('mike-project');
|
|
354
|
+
expect(results[2].name).toBe('zulu-project');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Test 18: Handles root path that doesn't exist (returns empty, logs warning)
|
|
358
|
+
it('handles root path that does not exist (returns empty, logs warning)', () => {
|
|
359
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const results = scanner.scan(['/tmp/nonexistent-root-path-xyz-123']);
|
|
363
|
+
|
|
364
|
+
expect(results).toEqual([]);
|
|
365
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
366
|
+
expect.stringContaining('/tmp/nonexistent-root-path-xyz-123')
|
|
367
|
+
);
|
|
368
|
+
} finally {
|
|
369
|
+
warnSpy.mockRestore();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Test 19: Scan progress callback reports discovered count
|
|
374
|
+
it('scan progress callback reports discovered count', () => {
|
|
375
|
+
createTlcProject(tempDir, 'project-one');
|
|
376
|
+
createTlcProject(tempDir, 'project-two');
|
|
377
|
+
|
|
378
|
+
const progressCounts = [];
|
|
379
|
+
const onProgress = (count) => progressCounts.push(count);
|
|
380
|
+
|
|
381
|
+
const results = scanner.scan([tempDir], { onProgress });
|
|
382
|
+
|
|
383
|
+
expect(results).toHaveLength(2);
|
|
384
|
+
// Progress callback should have been called at least once
|
|
385
|
+
expect(progressCounts.length).toBeGreaterThanOrEqual(1);
|
|
386
|
+
// The last reported count should match total found
|
|
387
|
+
expect(progressCounts[progressCounts.length - 1]).toBe(2);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// =========================================================================
|
|
391
|
+
// Phase 79 — Task 1: Stop recursion at project boundaries
|
|
392
|
+
// =========================================================================
|
|
393
|
+
|
|
394
|
+
// Test 20: Does NOT recurse into subdirectories of a detected project
|
|
395
|
+
it('does not recurse into subdirectories of a detected project', () => {
|
|
396
|
+
// Create a TLC project with a nested sub-package that also looks like a project
|
|
397
|
+
const projectDir = createTlcProject(tempDir, 'monorepo-project');
|
|
398
|
+
const subPkgDir = path.join(projectDir, 'packages', 'sub-package');
|
|
399
|
+
fs.mkdirSync(subPkgDir, { recursive: true });
|
|
400
|
+
fs.writeFileSync(path.join(subPkgDir, 'package.json'), JSON.stringify({ name: 'sub-package' }));
|
|
401
|
+
fs.mkdirSync(path.join(subPkgDir, '.git'), { recursive: true });
|
|
402
|
+
|
|
403
|
+
const results = scanner.scan([tempDir]);
|
|
404
|
+
|
|
405
|
+
// Should only find the parent project, not the sub-package
|
|
406
|
+
expect(results).toHaveLength(1);
|
|
407
|
+
expect(results[0].name).toBe('monorepo-project');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Test 21: TLC project's server/ subdirectory not listed separately
|
|
411
|
+
it('does not list subdirectories of a TLC project as separate projects', () => {
|
|
412
|
+
const projectDir = createTlcProject(tempDir, 'tlc-project');
|
|
413
|
+
// Create a server/ subdirectory with its own package.json + .git
|
|
414
|
+
const serverDir = path.join(projectDir, 'server');
|
|
415
|
+
fs.mkdirSync(serverDir, { recursive: true });
|
|
416
|
+
fs.writeFileSync(path.join(serverDir, 'package.json'), JSON.stringify({ name: 'tlc-server' }));
|
|
417
|
+
fs.mkdirSync(path.join(serverDir, '.git'), { recursive: true });
|
|
418
|
+
|
|
419
|
+
const results = scanner.scan([tempDir]);
|
|
420
|
+
|
|
421
|
+
expect(results).toHaveLength(1);
|
|
422
|
+
expect(results[0].name).toBe('tlc-project');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Test 22: Top-level non-project directories are still traversed
|
|
426
|
+
it('still traverses non-project directories to find nested projects', () => {
|
|
427
|
+
// Create a plain directory (not a project) with a project nested inside
|
|
428
|
+
const groupDir = path.join(tempDir, 'my-workspace');
|
|
429
|
+
fs.mkdirSync(groupDir, { recursive: true });
|
|
430
|
+
// No .tlc.json, no .planning, no package.json+.git — just a folder
|
|
431
|
+
createTlcProject(groupDir, 'nested-real-project');
|
|
432
|
+
|
|
433
|
+
const results = scanner.scan([tempDir]);
|
|
434
|
+
|
|
435
|
+
expect(results).toHaveLength(1);
|
|
436
|
+
expect(results[0].name).toBe('nested-real-project');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Test 23: Multiple projects at same level, none recurse into children
|
|
440
|
+
it('finds sibling projects but does not recurse into either', () => {
|
|
441
|
+
const projA = createTlcProject(tempDir, 'project-a');
|
|
442
|
+
const projB = createTlcProject(tempDir, 'project-b');
|
|
443
|
+
|
|
444
|
+
// Add nested sub-projects inside each
|
|
445
|
+
const nestedA = path.join(projA, 'nested');
|
|
446
|
+
fs.mkdirSync(nestedA, { recursive: true });
|
|
447
|
+
fs.writeFileSync(path.join(nestedA, '.tlc.json'), '{}');
|
|
448
|
+
|
|
449
|
+
const nestedB = path.join(projB, 'apps', 'frontend');
|
|
450
|
+
fs.mkdirSync(nestedB, { recursive: true });
|
|
451
|
+
fs.writeFileSync(path.join(nestedB, 'package.json'), JSON.stringify({ name: 'frontend' }));
|
|
452
|
+
fs.mkdirSync(path.join(nestedB, '.git'), { recursive: true });
|
|
453
|
+
|
|
454
|
+
const results = scanner.scan([tempDir]);
|
|
455
|
+
|
|
456
|
+
expect(results).toHaveLength(2);
|
|
457
|
+
const names = results.map(r => r.name);
|
|
458
|
+
expect(names).toContain('project-a');
|
|
459
|
+
expect(names).toContain('project-b');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// =========================================================================
|
|
463
|
+
// Phase 79 — Task 2: Monorepo sub-package metadata
|
|
464
|
+
// =========================================================================
|
|
465
|
+
|
|
466
|
+
// Test 24: Detects npm workspaces array format
|
|
467
|
+
it('detects npm workspaces and returns isMonorepo: true', () => {
|
|
468
|
+
createTlcProject(tempDir, 'npm-monorepo', {
|
|
469
|
+
packageJson: {
|
|
470
|
+
name: 'npm-monorepo',
|
|
471
|
+
version: '1.0.0',
|
|
472
|
+
workspaces: ['packages/*'],
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Create a matching sub-package directory
|
|
477
|
+
const pkgDir = path.join(tempDir, 'npm-monorepo', 'packages', 'core');
|
|
478
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
479
|
+
fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name: '@mono/core' }));
|
|
480
|
+
|
|
481
|
+
const results = scanner.scan([tempDir]);
|
|
482
|
+
|
|
483
|
+
expect(results).toHaveLength(1);
|
|
484
|
+
expect(results[0].isMonorepo).toBe(true);
|
|
485
|
+
expect(results[0].workspaces).toBeInstanceOf(Array);
|
|
486
|
+
expect(results[0].workspaces.length).toBeGreaterThanOrEqual(1);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Test 25: Detects yarn workspaces object format
|
|
490
|
+
it('detects yarn workspaces object format', () => {
|
|
491
|
+
createTlcProject(tempDir, 'yarn-monorepo', {
|
|
492
|
+
packageJson: {
|
|
493
|
+
name: 'yarn-monorepo',
|
|
494
|
+
version: '1.0.0',
|
|
495
|
+
workspaces: { packages: ['packages/*'] },
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const pkgDir = path.join(tempDir, 'yarn-monorepo', 'packages', 'utils');
|
|
500
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
501
|
+
fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name: '@mono/utils' }));
|
|
502
|
+
|
|
503
|
+
const results = scanner.scan([tempDir]);
|
|
504
|
+
|
|
505
|
+
expect(results).toHaveLength(1);
|
|
506
|
+
expect(results[0].isMonorepo).toBe(true);
|
|
507
|
+
expect(results[0].workspaces).toBeInstanceOf(Array);
|
|
508
|
+
expect(results[0].workspaces.length).toBeGreaterThanOrEqual(1);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Test 26: Non-monorepo returns isMonorepo: false and empty workspaces
|
|
512
|
+
it('returns isMonorepo false and empty workspaces for regular project', () => {
|
|
513
|
+
createTlcProject(tempDir, 'regular-project', {
|
|
514
|
+
packageJson: { name: 'regular-project', version: '1.0.0' },
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const results = scanner.scan([tempDir]);
|
|
518
|
+
|
|
519
|
+
expect(results).toHaveLength(1);
|
|
520
|
+
expect(results[0].isMonorepo).toBe(false);
|
|
521
|
+
expect(results[0].workspaces).toEqual([]);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Test 27: Monorepo with no matching workspace directories
|
|
525
|
+
it('returns empty workspaces when glob pattern matches nothing', () => {
|
|
526
|
+
createTlcProject(tempDir, 'empty-mono', {
|
|
527
|
+
packageJson: {
|
|
528
|
+
name: 'empty-mono',
|
|
529
|
+
version: '1.0.0',
|
|
530
|
+
workspaces: ['packages/*'],
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
// Don't create the packages/ directory at all
|
|
534
|
+
|
|
535
|
+
const results = scanner.scan([tempDir]);
|
|
536
|
+
|
|
537
|
+
expect(results).toHaveLength(1);
|
|
538
|
+
expect(results[0].isMonorepo).toBe(true);
|
|
539
|
+
expect(results[0].workspaces).toEqual([]);
|
|
540
|
+
});
|
|
541
|
+
});
|