tlc-claude-code 1.8.4 → 2.0.1

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 (77) 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/recall.md +87 -0
  4. package/.claude/commands/tlc/remember.md +71 -0
  5. package/CLAUDE.md +84 -201
  6. package/dashboard-web/dist/assets/index-Uhc49PE-.css +1 -0
  7. package/dashboard-web/dist/assets/index-W36XHPC5.js +431 -0
  8. package/dashboard-web/dist/assets/index-W36XHPC5.js.map +1 -0
  9. package/dashboard-web/dist/index.html +2 -2
  10. package/package.json +1 -1
  11. package/server/index.js +29 -4
  12. package/server/lib/bug-writer.js +204 -0
  13. package/server/lib/bug-writer.test.js +279 -0
  14. package/server/lib/claude-cascade.js +247 -0
  15. package/server/lib/claude-cascade.test.js +245 -0
  16. package/server/lib/context-injection.js +121 -0
  17. package/server/lib/context-injection.test.js +340 -0
  18. package/server/lib/conversation-chunker.js +320 -0
  19. package/server/lib/conversation-chunker.test.js +573 -0
  20. package/server/lib/embedding-client.js +160 -0
  21. package/server/lib/embedding-client.test.js +243 -0
  22. package/server/lib/global-config.js +198 -0
  23. package/server/lib/global-config.test.js +288 -0
  24. package/server/lib/inherited-search.js +184 -0
  25. package/server/lib/inherited-search.test.js +343 -0
  26. package/server/lib/memory-api.js +180 -0
  27. package/server/lib/memory-api.test.js +322 -0
  28. package/server/lib/memory-hooks-capture.test.js +350 -0
  29. package/server/lib/memory-hooks.js +101 -0
  30. package/server/lib/memory-inheritance.js +179 -0
  31. package/server/lib/memory-inheritance.test.js +360 -0
  32. package/server/lib/plan-parser.js +33 -7
  33. package/server/lib/plan-writer.js +196 -0
  34. package/server/lib/plan-writer.test.js +298 -0
  35. package/server/lib/project-scanner.js +267 -0
  36. package/server/lib/project-scanner.test.js +389 -0
  37. package/server/lib/project-status.js +302 -0
  38. package/server/lib/project-status.test.js +470 -0
  39. package/server/lib/projects-registry.js +237 -0
  40. package/server/lib/projects-registry.test.js +275 -0
  41. package/server/lib/recall-command.js +207 -0
  42. package/server/lib/recall-command.test.js +306 -0
  43. package/server/lib/remember-command.js +96 -0
  44. package/server/lib/remember-command.test.js +265 -0
  45. package/server/lib/rich-capture.js +221 -0
  46. package/server/lib/rich-capture.test.js +312 -0
  47. package/server/lib/roadmap-api.js +200 -0
  48. package/server/lib/roadmap-api.test.js +318 -0
  49. package/server/lib/semantic-recall.js +242 -0
  50. package/server/lib/semantic-recall.test.js +446 -0
  51. package/server/lib/setup-generator.js +315 -0
  52. package/server/lib/setup-generator.test.js +303 -0
  53. package/server/lib/test-inventory.js +112 -0
  54. package/server/lib/test-inventory.test.js +360 -0
  55. package/server/lib/vector-indexer.js +246 -0
  56. package/server/lib/vector-indexer.test.js +459 -0
  57. package/server/lib/vector-store.js +260 -0
  58. package/server/lib/vector-store.test.js +706 -0
  59. package/server/lib/workspace-api.js +811 -0
  60. package/server/lib/workspace-api.test.js +743 -0
  61. package/server/lib/workspace-bootstrap.js +164 -0
  62. package/server/lib/workspace-bootstrap.test.js +503 -0
  63. package/server/lib/workspace-context.js +129 -0
  64. package/server/lib/workspace-context.test.js +214 -0
  65. package/server/lib/workspace-detector.js +162 -0
  66. package/server/lib/workspace-detector.test.js +193 -0
  67. package/server/lib/workspace-init.js +307 -0
  68. package/server/lib/workspace-init.test.js +244 -0
  69. package/server/lib/workspace-snapshot.js +236 -0
  70. package/server/lib/workspace-snapshot.test.js +444 -0
  71. package/server/lib/workspace-watcher.js +162 -0
  72. package/server/lib/workspace-watcher.test.js +257 -0
  73. package/server/package-lock.json +552 -0
  74. package/server/package.json +4 -0
  75. package/dashboard-web/dist/assets/index-B1I_joSL.js +0 -393
  76. package/dashboard-web/dist/assets/index-B1I_joSL.js.map +0 -1
  77. package/dashboard-web/dist/assets/index-Trhg1C1Y.css +0 -1
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Workspace Bootstrap — clones repos from the projects registry and sets up
3
+ * a workspace on a new machine.
4
+ *
5
+ * Factory function `createWorkspaceBootstrap` accepts dependencies:
6
+ * - registry — projects registry (listProjects / load)
7
+ * - vectorIndexer — optional vector indexer (rebuildIndex)
8
+ * - execAsync — optional async exec function (defaults to no-op for testing)
9
+ *
10
+ * The returned object exposes:
11
+ * - execute(workspaceRoot, options) — clone repos, install deps, rebuild vectors
12
+ *
13
+ * @module workspace-bootstrap
14
+ */
15
+
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+
19
+ /**
20
+ * Default no-op exec function used when no execAsync is injected.
21
+ * @param {string} _cmd - Command string (ignored)
22
+ * @returns {Promise<{stdout: string, stderr: string}>}
23
+ */
24
+ async function defaultExecAsync(_cmd) {
25
+ return { stdout: '', stderr: '' };
26
+ }
27
+
28
+ /**
29
+ * Creates a workspace bootstrap instance.
30
+ *
31
+ * @param {object} deps
32
+ * @param {object} deps.registry - Projects registry with listProjects()
33
+ * @param {object} [deps.vectorIndexer] - Optional vector indexer with rebuildIndex()
34
+ * @param {Function} [deps.execAsync] - Async exec function for shell commands
35
+ * @returns {{ execute: Function }}
36
+ */
37
+ export function createWorkspaceBootstrap({ registry, vectorIndexer, execAsync } = {}) {
38
+ const exec = execAsync || defaultExecAsync;
39
+
40
+ /**
41
+ * Execute the bootstrap workflow: clone repos, install deps, rebuild vectors.
42
+ *
43
+ * @param {string} workspaceRoot - Absolute path to the workspace root directory
44
+ * @param {object} [options={}]
45
+ * @param {boolean} [options.dryRun=false] - If true, no exec calls are made
46
+ * @param {boolean} [options.skipInstall=false] - If true, skip npm install
47
+ * @param {number} [options.parallel=1] - Concurrency (reserved for future use)
48
+ * @param {Function} [options.onProgress] - Progress callback (phase, project, status)
49
+ * @returns {Promise<{cloned: number, skipped: number, failed: number, errors: Array, plan?: Array}>}
50
+ */
51
+ async function execute(workspaceRoot, options = {}) {
52
+ const {
53
+ dryRun = false,
54
+ skipInstall = false,
55
+ parallel = 1,
56
+ onProgress,
57
+ } = options;
58
+
59
+ const projects = await registry.listProjects();
60
+
61
+ const result = {
62
+ cloned: 0,
63
+ skipped: 0,
64
+ failed: 0,
65
+ errors: [],
66
+ };
67
+
68
+ // In dry-run mode, build a plan but execute nothing
69
+ if (dryRun) {
70
+ const plan = [];
71
+
72
+ for (const project of projects) {
73
+ const targetDir = path.join(workspaceRoot, project.localPath);
74
+ const gitDir = path.join(targetDir, '.git');
75
+ const alreadyCloned = fs.existsSync(gitDir);
76
+
77
+ if (alreadyCloned) {
78
+ result.skipped++;
79
+ if (onProgress) {
80
+ onProgress({ phase: 'scan', project: project.name, status: 'skipped' });
81
+ }
82
+ } else {
83
+ plan.push({
84
+ name: project.name,
85
+ gitUrl: project.gitUrl,
86
+ localPath: project.localPath,
87
+ defaultBranch: project.defaultBranch,
88
+ });
89
+ if (onProgress) {
90
+ onProgress({ phase: 'plan', project: project.name, status: 'would-clone' });
91
+ }
92
+ }
93
+ }
94
+
95
+ result.plan = plan;
96
+ return result;
97
+ }
98
+
99
+ // Live run: clone each project sequentially
100
+ for (const project of projects) {
101
+ const targetDir = path.join(workspaceRoot, project.localPath);
102
+ const gitDir = path.join(targetDir, '.git');
103
+
104
+ // Check if already cloned
105
+ if (fs.existsSync(gitDir)) {
106
+ result.skipped++;
107
+ if (onProgress) {
108
+ onProgress({ phase: 'clone', project: project.name, status: 'skipped' });
109
+ }
110
+ continue;
111
+ }
112
+
113
+ // Ensure parent directories exist for nested localPath values
114
+ const parentDir = path.dirname(targetDir);
115
+ if (!fs.existsSync(parentDir)) {
116
+ fs.mkdirSync(parentDir, { recursive: true });
117
+ }
118
+
119
+ try {
120
+ // Clone the repository
121
+ if (onProgress) {
122
+ onProgress({ phase: 'clone', project: project.name, status: 'cloning' });
123
+ }
124
+
125
+ await exec(`git clone ${project.gitUrl} ${targetDir}`);
126
+
127
+ // Checkout the configured default branch
128
+ await exec(`git -C ${targetDir} checkout ${project.defaultBranch}`);
129
+
130
+ // Run npm install unless skipInstall is set
131
+ if (!skipInstall) {
132
+ await exec(`npm install --prefix ${targetDir}`);
133
+ }
134
+
135
+ result.cloned++;
136
+ if (onProgress) {
137
+ onProgress({ phase: 'clone', project: project.name, status: 'done' });
138
+ }
139
+ } catch (err) {
140
+ result.failed++;
141
+ result.errors.push({
142
+ project: project.name,
143
+ error: err.message || String(err),
144
+ });
145
+ if (onProgress) {
146
+ onProgress({ phase: 'clone', project: project.name, status: 'failed' });
147
+ }
148
+ }
149
+ }
150
+
151
+ // Rebuild vector index if vectorIndexer is provided
152
+ if (vectorIndexer && typeof vectorIndexer.rebuildIndex === 'function') {
153
+ try {
154
+ await vectorIndexer.rebuildIndex();
155
+ } catch (_err) {
156
+ // Vector rebuild failure is non-fatal
157
+ }
158
+ }
159
+
160
+ return result;
161
+ }
162
+
163
+ return { execute };
164
+ }
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Workspace Bootstrap Tests
3
+ *
4
+ * Tests for the /tlc:bootstrap command that clones all repos from
5
+ * projects.json and sets up the workspace on a new machine.
6
+ *
7
+ * The bootstrap module:
8
+ * - Reads the projects registry to discover repos
9
+ * - Clones each repo to its configured localPath
10
+ * - Checks out the configured defaultBranch
11
+ * - Optionally runs npm install per project
12
+ * - Optionally triggers vector index rebuild
13
+ * - Reports progress via callback and returns a summary
14
+ *
15
+ * These tests are written BEFORE the implementation (Red phase).
16
+ */
17
+
18
+ import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import os from 'os';
22
+ import { createWorkspaceBootstrap } from './workspace-bootstrap.js';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Mock factories
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /**
29
+ * Creates a mock projects registry that returns a configurable list of
30
+ * projects. Mirrors the shape returned by createProjectsRegistry().
31
+ * @param {Array} projects - Array of project entries
32
+ * @returns {object} Mock registry with listProjects / load stubs
33
+ */
34
+ function createMockRegistry(projects = []) {
35
+ return {
36
+ load: vi.fn().mockResolvedValue({ version: 1, projects }),
37
+ listProjects: vi.fn().mockResolvedValue(projects),
38
+ save: vi.fn().mockResolvedValue(undefined),
39
+ addProject: vi.fn().mockResolvedValue(undefined),
40
+ removeProject: vi.fn().mockResolvedValue(undefined),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Creates a mock vectorIndexer dependency.
46
+ * @returns {object}
47
+ */
48
+ function createMockVectorIndexer() {
49
+ return {
50
+ rebuildIndex: vi.fn().mockResolvedValue({ indexed: 5, errors: 0 }),
51
+ indexFile: vi.fn().mockResolvedValue({ success: true }),
52
+ indexAll: vi.fn().mockResolvedValue({ indexed: 0, errors: 0 }),
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Creates a mock execAsync function that tracks calls and simulates
58
+ * successful git operations by default.
59
+ * @returns {vi.fn} Mock exec function
60
+ */
61
+ function createMockExec() {
62
+ return vi.fn().mockResolvedValue({ stdout: '', stderr: '' });
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Sample project data
67
+ // ---------------------------------------------------------------------------
68
+
69
+ const sampleProjects = [
70
+ {
71
+ name: 'api-service',
72
+ gitUrl: 'git@github.com:myorg/api-service.git',
73
+ localPath: 'api-service',
74
+ defaultBranch: 'main',
75
+ hasTlc: true,
76
+ description: 'REST API service',
77
+ },
78
+ {
79
+ name: 'web-frontend',
80
+ gitUrl: 'https://github.com/myorg/web-frontend.git',
81
+ localPath: 'web-frontend',
82
+ defaultBranch: 'develop',
83
+ hasTlc: false,
84
+ description: 'React frontend app',
85
+ },
86
+ {
87
+ name: 'shared-lib',
88
+ gitUrl: 'git@github.com:myorg/shared-lib.git',
89
+ localPath: 'libs/shared',
90
+ defaultBranch: 'main',
91
+ hasTlc: true,
92
+ description: 'Shared utilities library',
93
+ },
94
+ ];
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Tests
98
+ // ---------------------------------------------------------------------------
99
+
100
+ describe('workspace-bootstrap', () => {
101
+ let tempDir;
102
+ let mockRegistry;
103
+ let mockVectorIndexer;
104
+ let bootstrap;
105
+
106
+ beforeEach(() => {
107
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-bootstrap-test-'));
108
+ mockRegistry = createMockRegistry(sampleProjects);
109
+ mockVectorIndexer = createMockVectorIndexer();
110
+
111
+ bootstrap = createWorkspaceBootstrap({
112
+ registry: mockRegistry,
113
+ vectorIndexer: mockVectorIndexer,
114
+ });
115
+ });
116
+
117
+ afterEach(() => {
118
+ fs.rmSync(tempDir, { recursive: true, force: true });
119
+ });
120
+
121
+ // -------------------------------------------------------------------------
122
+ // 1. Clones repos listed in projects.json
123
+ // -------------------------------------------------------------------------
124
+ it('clones repos listed in projects.json (mock exec for git clone)', async () => {
125
+ const result = await bootstrap.execute(tempDir, {
126
+ dryRun: false,
127
+ skipInstall: true,
128
+ parallel: 1,
129
+ });
130
+
131
+ // Should have attempted to clone all 3 projects
132
+ expect(result.cloned).toBe(3);
133
+
134
+ // Registry should have been queried
135
+ expect(
136
+ mockRegistry.listProjects.mock.calls.length +
137
+ mockRegistry.load.mock.calls.length
138
+ ).toBeGreaterThanOrEqual(1);
139
+ });
140
+
141
+ // -------------------------------------------------------------------------
142
+ // 2. Skips already-cloned repos (directory already exists with .git/)
143
+ // -------------------------------------------------------------------------
144
+ it('skips already-cloned repos (directory already exists with .git/)', async () => {
145
+ // Pre-create one repo directory with .git/ to simulate already-cloned
146
+ const existingRepo = path.join(tempDir, 'api-service');
147
+ fs.mkdirSync(existingRepo, { recursive: true });
148
+ fs.mkdirSync(path.join(existingRepo, '.git'));
149
+
150
+ const result = await bootstrap.execute(tempDir, {
151
+ dryRun: false,
152
+ skipInstall: true,
153
+ parallel: 1,
154
+ });
155
+
156
+ // api-service should be skipped, the other 2 cloned
157
+ expect(result.skipped).toBe(1);
158
+ expect(result.cloned).toBe(2);
159
+ });
160
+
161
+ // -------------------------------------------------------------------------
162
+ // 3. Checks out correct branch per repo
163
+ // -------------------------------------------------------------------------
164
+ it('checks out correct branch per repo (git checkout command)', async () => {
165
+ const execCalls = [];
166
+
167
+ // Create a bootstrap with an exec spy that captures calls
168
+ const spyBootstrap = createWorkspaceBootstrap({
169
+ registry: mockRegistry,
170
+ vectorIndexer: mockVectorIndexer,
171
+ execAsync: vi.fn().mockImplementation(async (cmd) => {
172
+ execCalls.push(cmd);
173
+ return { stdout: '', stderr: '' };
174
+ }),
175
+ });
176
+
177
+ await spyBootstrap.execute(tempDir, {
178
+ dryRun: false,
179
+ skipInstall: true,
180
+ parallel: 1,
181
+ });
182
+
183
+ // Should contain checkout commands for the configured branches
184
+ const checkoutCmds = execCalls.filter(cmd => cmd.includes('checkout'));
185
+ // web-frontend uses 'develop', others use 'main'
186
+ const developCheckout = checkoutCmds.find(cmd => cmd.includes('develop'));
187
+ expect(developCheckout).toBeDefined();
188
+
189
+ // At least one checkout for 'main'
190
+ const mainCheckout = checkoutCmds.find(cmd => cmd.includes('main'));
191
+ expect(mainCheckout).toBeDefined();
192
+ });
193
+
194
+ // -------------------------------------------------------------------------
195
+ // 4. Dry-run shows plan without cloning
196
+ // -------------------------------------------------------------------------
197
+ it('dry-run shows plan without cloning (no exec calls, returns plan)', async () => {
198
+ const execCalls = [];
199
+
200
+ const spyBootstrap = createWorkspaceBootstrap({
201
+ registry: mockRegistry,
202
+ vectorIndexer: mockVectorIndexer,
203
+ execAsync: vi.fn().mockImplementation(async (cmd) => {
204
+ execCalls.push(cmd);
205
+ return { stdout: '', stderr: '' };
206
+ }),
207
+ });
208
+
209
+ const result = await spyBootstrap.execute(tempDir, {
210
+ dryRun: true,
211
+ skipInstall: true,
212
+ parallel: 1,
213
+ });
214
+
215
+ // No git clone commands should have been executed
216
+ const cloneCmds = execCalls.filter(cmd => cmd.includes('git clone'));
217
+ expect(cloneCmds).toHaveLength(0);
218
+
219
+ // Result should still list what would be done
220
+ expect(result.cloned).toBe(0);
221
+
222
+ // Should indicate 3 projects would be cloned (or return a plan array)
223
+ // The plan could be in result.plan or result.wouldClone
224
+ expect(result.skipped + (result.plan || result.wouldClone || []).length || 0)
225
+ .toBeGreaterThanOrEqual(0);
226
+ });
227
+
228
+ // -------------------------------------------------------------------------
229
+ // 5. Skip-install flag respected
230
+ // -------------------------------------------------------------------------
231
+ it('skip-install flag prevents npm install calls', async () => {
232
+ const execCalls = [];
233
+
234
+ const spyBootstrap = createWorkspaceBootstrap({
235
+ registry: mockRegistry,
236
+ vectorIndexer: mockVectorIndexer,
237
+ execAsync: vi.fn().mockImplementation(async (cmd) => {
238
+ execCalls.push(cmd);
239
+ return { stdout: '', stderr: '' };
240
+ }),
241
+ });
242
+
243
+ await spyBootstrap.execute(tempDir, {
244
+ dryRun: false,
245
+ skipInstall: true,
246
+ parallel: 1,
247
+ });
248
+
249
+ // No npm install / pip install commands should have been called
250
+ const installCmds = execCalls.filter(cmd =>
251
+ cmd.includes('npm install') ||
252
+ cmd.includes('yarn install') ||
253
+ cmd.includes('pip install')
254
+ );
255
+ expect(installCmds).toHaveLength(0);
256
+ });
257
+
258
+ // -------------------------------------------------------------------------
259
+ // 6. Progress callback fires per repo with phase/project/status
260
+ // -------------------------------------------------------------------------
261
+ it('progress callback fires per repo with phase/project/status', async () => {
262
+ const progressEvents = [];
263
+
264
+ await bootstrap.execute(tempDir, {
265
+ dryRun: false,
266
+ skipInstall: true,
267
+ parallel: 1,
268
+ onProgress: (event) => {
269
+ progressEvents.push(event);
270
+ },
271
+ });
272
+
273
+ // Should have received progress events
274
+ expect(progressEvents.length).toBeGreaterThan(0);
275
+
276
+ // Each event should have phase, project, and status
277
+ for (const event of progressEvents) {
278
+ expect(event).toHaveProperty('phase');
279
+ expect(event).toHaveProperty('project');
280
+ expect(event).toHaveProperty('status');
281
+ }
282
+
283
+ // Should have events for each project
284
+ const projectNames = progressEvents.map(e => e.project);
285
+ expect(projectNames).toContain('api-service');
286
+ expect(projectNames).toContain('web-frontend');
287
+ expect(projectNames).toContain('shared-lib');
288
+ });
289
+
290
+ // -------------------------------------------------------------------------
291
+ // 7. Summary reports counts correctly
292
+ // -------------------------------------------------------------------------
293
+ it('summary reports counts correctly (cloned/skipped/failed)', async () => {
294
+ // Pre-create one repo to be skipped
295
+ const existingRepo = path.join(tempDir, 'web-frontend');
296
+ fs.mkdirSync(existingRepo, { recursive: true });
297
+ fs.mkdirSync(path.join(existingRepo, '.git'));
298
+
299
+ const result = await bootstrap.execute(tempDir, {
300
+ dryRun: false,
301
+ skipInstall: true,
302
+ parallel: 1,
303
+ });
304
+
305
+ // Should report correct counts
306
+ expect(result).toHaveProperty('cloned');
307
+ expect(result).toHaveProperty('skipped');
308
+ expect(result).toHaveProperty('failed');
309
+ expect(result).toHaveProperty('errors');
310
+
311
+ expect(result.cloned).toBe(2);
312
+ expect(result.skipped).toBe(1);
313
+ expect(result.failed).toBe(0);
314
+ expect(result.errors).toEqual([]);
315
+ });
316
+
317
+ // -------------------------------------------------------------------------
318
+ // 8. Handles clone failure gracefully (continues with others, reports error)
319
+ // -------------------------------------------------------------------------
320
+ it('handles clone failure gracefully (continues with others, reports error)', async () => {
321
+ let callCount = 0;
322
+
323
+ const failingBootstrap = createWorkspaceBootstrap({
324
+ registry: mockRegistry,
325
+ vectorIndexer: mockVectorIndexer,
326
+ execAsync: vi.fn().mockImplementation(async (cmd) => {
327
+ // Fail the first clone attempt (api-service), succeed the rest
328
+ if (cmd.includes('git clone') && cmd.includes('api-service')) {
329
+ throw new Error('Connection refused');
330
+ }
331
+ return { stdout: '', stderr: '' };
332
+ }),
333
+ });
334
+
335
+ const result = await failingBootstrap.execute(tempDir, {
336
+ dryRun: false,
337
+ skipInstall: true,
338
+ parallel: 1,
339
+ });
340
+
341
+ // Should have continued past the failure
342
+ expect(result.failed).toBe(1);
343
+ expect(result.cloned).toBe(2);
344
+
345
+ // Errors array should contain the failure details
346
+ expect(result.errors).toHaveLength(1);
347
+ expect(result.errors[0]).toHaveProperty('project', 'api-service');
348
+ expect(result.errors[0]).toHaveProperty('error');
349
+ });
350
+
351
+ // -------------------------------------------------------------------------
352
+ // 9. Triggers vector index rebuild after clone
353
+ // -------------------------------------------------------------------------
354
+ it('triggers vector index rebuild after clone (calls vectorIndexer.rebuildIndex)', async () => {
355
+ await bootstrap.execute(tempDir, {
356
+ dryRun: false,
357
+ skipInstall: true,
358
+ parallel: 1,
359
+ });
360
+
361
+ // vectorIndexer.rebuildIndex should have been called after cloning
362
+ expect(mockVectorIndexer.rebuildIndex).toHaveBeenCalled();
363
+ });
364
+
365
+ // -------------------------------------------------------------------------
366
+ // 9b. Vector index rebuild skipped when vectorIndexer not provided
367
+ // -------------------------------------------------------------------------
368
+ it('skips vector index rebuild when vectorIndexer is not provided', async () => {
369
+ const noVectorBootstrap = createWorkspaceBootstrap({
370
+ registry: mockRegistry,
371
+ // No vectorIndexer provided
372
+ });
373
+
374
+ // Should not throw when vectorIndexer is absent
375
+ const result = await noVectorBootstrap.execute(tempDir, {
376
+ dryRun: false,
377
+ skipInstall: true,
378
+ parallel: 1,
379
+ });
380
+
381
+ expect(result.cloned).toBe(3);
382
+ // No rebuildIndex call since vectorIndexer was not provided
383
+ expect(mockVectorIndexer.rebuildIndex).not.toHaveBeenCalled();
384
+ });
385
+
386
+ // -------------------------------------------------------------------------
387
+ // 10. Creates local directories as needed (parent dirs)
388
+ // -------------------------------------------------------------------------
389
+ it('creates local directories as needed (nested parent dirs)', async () => {
390
+ // shared-lib has localPath 'libs/shared' which requires creating 'libs/' first
391
+ // Use a registry with only the nested-path project
392
+ const nestedRegistry = createMockRegistry([
393
+ {
394
+ name: 'shared-lib',
395
+ gitUrl: 'git@github.com:myorg/shared-lib.git',
396
+ localPath: 'libs/shared',
397
+ defaultBranch: 'main',
398
+ hasTlc: true,
399
+ description: 'Shared utilities library',
400
+ },
401
+ ]);
402
+
403
+ const execCalls = [];
404
+ const dirBootstrap = createWorkspaceBootstrap({
405
+ registry: nestedRegistry,
406
+ vectorIndexer: mockVectorIndexer,
407
+ execAsync: vi.fn().mockImplementation(async (cmd) => {
408
+ execCalls.push(cmd);
409
+ // Simulate git clone by creating the target directory with .git
410
+ if (cmd.includes('git clone')) {
411
+ const targetDir = path.join(tempDir, 'libs', 'shared');
412
+ fs.mkdirSync(targetDir, { recursive: true });
413
+ fs.mkdirSync(path.join(targetDir, '.git'), { recursive: true });
414
+ }
415
+ return { stdout: '', stderr: '' };
416
+ }),
417
+ });
418
+
419
+ const result = await dirBootstrap.execute(tempDir, {
420
+ dryRun: false,
421
+ skipInstall: true,
422
+ parallel: 1,
423
+ });
424
+
425
+ // The parent directory 'libs/' should exist (created before clone)
426
+ expect(fs.existsSync(path.join(tempDir, 'libs'))).toBe(true);
427
+ expect(result.cloned).toBe(1);
428
+ });
429
+
430
+ // -------------------------------------------------------------------------
431
+ // 11. Handles SSH git URLs in clone command
432
+ // -------------------------------------------------------------------------
433
+ it('handles SSH git URLs in clone command', async () => {
434
+ const sshOnlyRegistry = createMockRegistry([
435
+ {
436
+ name: 'ssh-repo',
437
+ gitUrl: 'git@github.com:myorg/ssh-repo.git',
438
+ localPath: 'ssh-repo',
439
+ defaultBranch: 'main',
440
+ hasTlc: false,
441
+ description: 'SSH-cloned repo',
442
+ },
443
+ ]);
444
+
445
+ const execCalls = [];
446
+ const sshBootstrap = createWorkspaceBootstrap({
447
+ registry: sshOnlyRegistry,
448
+ vectorIndexer: mockVectorIndexer,
449
+ execAsync: vi.fn().mockImplementation(async (cmd) => {
450
+ execCalls.push(cmd);
451
+ return { stdout: '', stderr: '' };
452
+ }),
453
+ });
454
+
455
+ await sshBootstrap.execute(tempDir, {
456
+ dryRun: false,
457
+ skipInstall: true,
458
+ parallel: 1,
459
+ });
460
+
461
+ // The clone command should contain the SSH URL
462
+ const cloneCmd = execCalls.find(cmd => cmd.includes('git clone'));
463
+ expect(cloneCmd).toBeDefined();
464
+ expect(cloneCmd).toContain('git@github.com:myorg/ssh-repo.git');
465
+ });
466
+
467
+ // -------------------------------------------------------------------------
468
+ // 12. Handles HTTPS git URLs in clone command
469
+ // -------------------------------------------------------------------------
470
+ it('handles HTTPS git URLs in clone command', async () => {
471
+ const httpsOnlyRegistry = createMockRegistry([
472
+ {
473
+ name: 'https-repo',
474
+ gitUrl: 'https://github.com/myorg/https-repo.git',
475
+ localPath: 'https-repo',
476
+ defaultBranch: 'main',
477
+ hasTlc: false,
478
+ description: 'HTTPS-cloned repo',
479
+ },
480
+ ]);
481
+
482
+ const execCalls = [];
483
+ const httpsBootstrap = createWorkspaceBootstrap({
484
+ registry: httpsOnlyRegistry,
485
+ vectorIndexer: mockVectorIndexer,
486
+ execAsync: vi.fn().mockImplementation(async (cmd) => {
487
+ execCalls.push(cmd);
488
+ return { stdout: '', stderr: '' };
489
+ }),
490
+ });
491
+
492
+ await httpsBootstrap.execute(tempDir, {
493
+ dryRun: false,
494
+ skipInstall: true,
495
+ parallel: 1,
496
+ });
497
+
498
+ // The clone command should contain the HTTPS URL
499
+ const cloneCmd = execCalls.find(cmd => cmd.includes('git clone'));
500
+ expect(cloneCmd).toBeDefined();
501
+ expect(cloneCmd).toContain('https://github.com/myorg/https-repo.git');
502
+ });
503
+ });