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,315 @@
1
+ /**
2
+ * Setup Generator — produces setup.md and setup.sh for workspace rebuild
3
+ *
4
+ * Generates human-readable instructions (setup.md) and an automated bash
5
+ * script (setup.sh) from the projects registry. Detects project types
6
+ * (Node, Python, Go) and includes appropriate install commands, version
7
+ * requirements, and TLC-specific setup steps.
8
+ *
9
+ * Factory function `createSetupGenerator` accepts dependencies:
10
+ * - registry — projects registry (listProjects)
11
+ *
12
+ * The returned object exposes:
13
+ * - generateSetupMd(workspaceRoot) — markdown instructions
14
+ * - generateSetupSh(workspaceRoot) — bash script
15
+ *
16
+ * @module setup-generator
17
+ */
18
+
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+
22
+ // -------------------------------------------------------------------------
23
+ // Project type detection helpers
24
+ // -------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Detect the project type from marker files in a directory.
28
+ * @param {string} projectDir - Absolute path to project directory
29
+ * @returns {'node'|'python'|'go'|'unknown'}
30
+ */
31
+ function detectProjectType(projectDir) {
32
+ try {
33
+ if (fs.existsSync(path.join(projectDir, 'package.json'))) return 'node';
34
+ if (fs.existsSync(path.join(projectDir, 'requirements.txt'))) return 'python';
35
+ if (fs.existsSync(path.join(projectDir, 'pyproject.toml'))) return 'python';
36
+ if (fs.existsSync(path.join(projectDir, 'setup.py'))) return 'python';
37
+ if (fs.existsSync(path.join(projectDir, 'go.mod'))) return 'go';
38
+ } catch {
39
+ // Ignore FS errors
40
+ }
41
+ return 'unknown';
42
+ }
43
+
44
+ /**
45
+ * Try to detect the Node.js version required by a project.
46
+ * Checks .nvmrc first, then package.json engines.node.
47
+ * @param {string} projectDir - Absolute path to project directory
48
+ * @returns {string|null} Version string or null
49
+ */
50
+ function detectNodeVersion(projectDir) {
51
+ // Check .nvmrc
52
+ try {
53
+ const nvmrc = path.join(projectDir, '.nvmrc');
54
+ if (fs.existsSync(nvmrc)) {
55
+ const version = fs.readFileSync(nvmrc, 'utf-8').trim();
56
+ if (version) return version;
57
+ }
58
+ } catch {
59
+ // Ignore
60
+ }
61
+
62
+ // Check package.json engines.node
63
+ try {
64
+ const pkgPath = path.join(projectDir, 'package.json');
65
+ if (fs.existsSync(pkgPath)) {
66
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
67
+ if (pkg.engines && pkg.engines.node) {
68
+ return pkg.engines.node;
69
+ }
70
+ }
71
+ } catch {
72
+ // Ignore
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Build an enriched project list with detected types and versions.
80
+ * @param {string} workspaceRoot - Absolute path to workspace
81
+ * @param {Array} projects - Project entries from registry
82
+ * @returns {Array} Projects enriched with type and nodeVersion fields
83
+ */
84
+ function enrichProjects(workspaceRoot, projects) {
85
+ return projects.map((project) => {
86
+ const dir = path.join(workspaceRoot, project.localPath);
87
+ const type = detectProjectType(dir);
88
+ const nodeVersion = type === 'node' ? detectNodeVersion(dir) : null;
89
+ return { ...project, type, nodeVersion };
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Collect unique prerequisite tool names from project types.
95
+ * @param {Array} enriched - Enriched project entries
96
+ * @returns {Set<string>} Set of prerequisite names
97
+ */
98
+ function collectPrerequisites(enriched) {
99
+ const prereqs = new Set();
100
+ prereqs.add('Git');
101
+ prereqs.add('Ollama');
102
+
103
+ for (const p of enriched) {
104
+ if (p.type === 'node') prereqs.add('Node.js');
105
+ if (p.type === 'python') prereqs.add('Python');
106
+ if (p.type === 'go') prereqs.add('Go');
107
+ }
108
+
109
+ return prereqs;
110
+ }
111
+
112
+ // -------------------------------------------------------------------------
113
+ // Factory
114
+ // -------------------------------------------------------------------------
115
+
116
+ /**
117
+ * Creates a setup generator instance.
118
+ *
119
+ * @param {object} deps
120
+ * @param {object} deps.registry - Projects registry with listProjects()
121
+ * @returns {{ generateSetupMd: Function, generateSetupSh: Function }}
122
+ */
123
+ export function createSetupGenerator({ registry } = {}) {
124
+ if (!registry) {
125
+ throw new Error('registry dependency is required');
126
+ }
127
+
128
+ /**
129
+ * Generate a setup.md with human-readable workspace setup instructions.
130
+ * @param {string} workspaceRoot - Absolute path to workspace root
131
+ * @returns {Promise<string>} Markdown content
132
+ */
133
+ async function generateSetupMd(workspaceRoot) {
134
+ const projects = await registry.listProjects();
135
+ const enriched = enrichProjects(workspaceRoot, projects);
136
+ const prereqs = collectPrerequisites(enriched);
137
+
138
+ const lines = [];
139
+
140
+ // Title
141
+ lines.push('# Workspace Setup');
142
+ lines.push('');
143
+ lines.push('Follow these instructions to set up the workspace on a new machine.');
144
+ lines.push('This document can be read by humans and by Claude (paste into a new session).');
145
+ lines.push('');
146
+
147
+ // Prerequisites
148
+ lines.push('## Prerequisites');
149
+ lines.push('');
150
+ lines.push('Ensure the following tools are installed before proceeding:');
151
+ lines.push('');
152
+
153
+ for (const prereq of prereqs) {
154
+ if (prereq === 'Node.js') {
155
+ // Include detected versions if available
156
+ const nodeVersions = enriched
157
+ .filter((p) => p.type === 'node' && p.nodeVersion)
158
+ .map((p) => p.nodeVersion);
159
+ if (nodeVersions.length > 0) {
160
+ const unique = [...new Set(nodeVersions)];
161
+ lines.push(`- **Node.js** (${unique.join(', ')})`);
162
+ } else {
163
+ lines.push('- **Node.js**');
164
+ }
165
+ } else if (prereq === 'Python') {
166
+ lines.push('- **Python** (3.x recommended)');
167
+ } else if (prereq === 'Go') {
168
+ lines.push('- **Go** (1.21+ recommended)');
169
+ } else if (prereq === 'Ollama') {
170
+ lines.push('- **Ollama** (for semantic memory embeddings)');
171
+ } else {
172
+ lines.push(`- **${prereq}**`);
173
+ }
174
+ }
175
+ lines.push('');
176
+
177
+ // Clone & install steps
178
+ lines.push('## Clone Projects');
179
+ lines.push('');
180
+
181
+ for (const project of enriched) {
182
+ lines.push(`### ${project.name}`);
183
+ lines.push('');
184
+ if (project.description) {
185
+ lines.push(project.description);
186
+ lines.push('');
187
+ }
188
+ lines.push('```bash');
189
+ lines.push(`git clone ${project.gitUrl} ${project.localPath}`);
190
+ lines.push(`cd ${project.localPath}`);
191
+ lines.push(`git checkout ${project.defaultBranch || 'main'}`);
192
+
193
+ if (project.type === 'node') {
194
+ lines.push('npm install');
195
+ } else if (project.type === 'python') {
196
+ lines.push('pip install -r requirements.txt');
197
+ }
198
+
199
+ lines.push('cd ..');
200
+ lines.push('```');
201
+ lines.push('');
202
+ }
203
+
204
+ // TLC-specific setup
205
+ lines.push('## TLC Setup');
206
+ lines.push('');
207
+ lines.push('### Pull Ollama embedding model');
208
+ lines.push('');
209
+ lines.push('```bash');
210
+ lines.push('ollama pull mxbai-embed-large');
211
+ lines.push('```');
212
+ lines.push('');
213
+ lines.push('### Rebuild vector index');
214
+ lines.push('');
215
+ lines.push('```bash');
216
+ lines.push('npx tlc-server rebuild-vectors');
217
+ lines.push('```');
218
+ lines.push('');
219
+ lines.push('### Start TLC dashboard');
220
+ lines.push('');
221
+ lines.push('```bash');
222
+ lines.push('npx tlc-server start');
223
+ lines.push('```');
224
+ lines.push('');
225
+ lines.push('The TLC dashboard will be available at http://localhost:5174');
226
+ lines.push('');
227
+
228
+ return lines.join('\n');
229
+ }
230
+
231
+ /**
232
+ * Generate a setup.sh bash script for automated workspace rebuild.
233
+ * @param {string} workspaceRoot - Absolute path to workspace root
234
+ * @returns {Promise<string>} Bash script content
235
+ */
236
+ async function generateSetupSh(workspaceRoot) {
237
+ const projects = await registry.listProjects();
238
+ const enriched = enrichProjects(workspaceRoot, projects);
239
+
240
+ const lines = [];
241
+
242
+ // Shebang and header
243
+ lines.push('#!/bin/bash');
244
+ lines.push('# Workspace setup script — generated by TLC');
245
+ lines.push('# Safe to run multiple times (idempotent)');
246
+ lines.push('set -e');
247
+ lines.push('');
248
+
249
+ // Determine workspace root dir (script location)
250
+ lines.push('SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"');
251
+ lines.push('cd "$SCRIPT_DIR"');
252
+ lines.push('');
253
+ lines.push('echo "=== TLC Workspace Setup ==="');
254
+ lines.push('echo ""');
255
+ lines.push('');
256
+
257
+ // Clone each project with idempotency guard
258
+ for (const project of enriched) {
259
+ lines.push(`# --- ${project.name} ---`);
260
+ lines.push(`if [ ! -d "${project.localPath}/.git" ]; then`);
261
+ lines.push(` echo "Cloning ${project.name}..."`);
262
+ lines.push(` git clone ${project.gitUrl} ${project.localPath}`);
263
+ lines.push(` cd ${project.localPath}`);
264
+ lines.push(` git checkout ${project.defaultBranch || 'main'}`);
265
+ lines.push(' cd "$SCRIPT_DIR"');
266
+ lines.push('else');
267
+ lines.push(` echo "${project.name} already cloned, skipping."`);
268
+ lines.push('fi');
269
+ lines.push('');
270
+
271
+ // Install dependencies based on project type
272
+ if (project.type === 'node') {
273
+ lines.push(`# Install Node dependencies for ${project.name}`);
274
+ lines.push(`if [ -f "${project.localPath}/package.json" ]; then`);
275
+ lines.push(` echo "Running npm install for ${project.name}..."`);
276
+ lines.push(` cd ${project.localPath} && npm install && cd "$SCRIPT_DIR"`);
277
+ lines.push('fi');
278
+ lines.push('');
279
+ } else if (project.type === 'python') {
280
+ lines.push(`# Install Python dependencies for ${project.name}`);
281
+ lines.push(`if [ -f "${project.localPath}/requirements.txt" ]; then`);
282
+ lines.push(` echo "Running pip install for ${project.name}..."`);
283
+ lines.push(` pip install -r ${project.localPath}/requirements.txt`);
284
+ lines.push(`elif [ -f "${project.localPath}/pyproject.toml" ]; then`);
285
+ lines.push(` echo "Running pip install for ${project.name}..."`);
286
+ lines.push(` pip install -e ${project.localPath}`);
287
+ lines.push('fi');
288
+ lines.push('');
289
+ }
290
+ }
291
+
292
+ // Ollama model pull
293
+ lines.push('# --- Ollama embedding model ---');
294
+ lines.push('echo ""');
295
+ lines.push('echo "Pulling Ollama embedding model..."');
296
+ lines.push('ollama pull mxbai-embed-large');
297
+ lines.push('');
298
+
299
+ // Vector rebuild
300
+ lines.push('# --- Rebuild vector index ---');
301
+ lines.push('echo ""');
302
+ lines.push('echo "Rebuilding vector index..."');
303
+ lines.push('npx tlc-server rebuild-vectors');
304
+ lines.push('');
305
+
306
+ // Done
307
+ lines.push('echo ""');
308
+ lines.push('echo "=== Setup complete ==="');
309
+ lines.push('');
310
+
311
+ return lines.join('\n');
312
+ }
313
+
314
+ return { generateSetupMd, generateSetupSh };
315
+ }
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Setup Generator Tests
3
+ *
4
+ * Tests for generating setup.md (human-readable instructions) and
5
+ * setup.sh (automated workspace rebuild script) from the projects registry.
6
+ *
7
+ * The setup generator:
8
+ * - Reads the projects registry to discover repos
9
+ * - Detects project types (Node, Python, Go) from marker files
10
+ * - Generates a setup.md with prerequisites and step-by-step instructions
11
+ * - Generates a setup.sh bash script with clone, install, and rebuild commands
12
+ * - Produces idempotent scripts (safe to run multiple times)
13
+ *
14
+ * These tests are written BEFORE the implementation (Red phase).
15
+ */
16
+
17
+ import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import os from 'os';
21
+ import { createSetupGenerator } from './setup-generator.js';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Mock factories
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Creates a mock projects registry that returns a configurable list of
29
+ * projects. Mirrors the shape returned by createProjectsRegistry().
30
+ * @param {Array} projects - Array of project entries
31
+ * @returns {object} Mock registry with listProjects stub
32
+ */
33
+ function createMockRegistry(projects = []) {
34
+ return {
35
+ load: vi.fn().mockResolvedValue({ version: 1, projects }),
36
+ listProjects: vi.fn().mockResolvedValue(projects),
37
+ save: vi.fn().mockResolvedValue(undefined),
38
+ addProject: vi.fn().mockResolvedValue(undefined),
39
+ removeProject: vi.fn().mockResolvedValue(undefined),
40
+ };
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Sample project data
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const sampleNodeProject = {
48
+ name: 'api-service',
49
+ gitUrl: 'git@github.com:myorg/api-service.git',
50
+ localPath: 'api-service',
51
+ defaultBranch: 'main',
52
+ hasTlc: true,
53
+ description: 'REST API service',
54
+ };
55
+
56
+ const samplePythonProject = {
57
+ name: 'ml-pipeline',
58
+ gitUrl: 'https://github.com/myorg/ml-pipeline.git',
59
+ localPath: 'ml-pipeline',
60
+ defaultBranch: 'main',
61
+ hasTlc: false,
62
+ description: 'ML training pipeline',
63
+ };
64
+
65
+ const sampleGoProject = {
66
+ name: 'gateway',
67
+ gitUrl: 'git@github.com:myorg/gateway.git',
68
+ localPath: 'gateway',
69
+ defaultBranch: 'main',
70
+ hasTlc: false,
71
+ description: 'API gateway in Go',
72
+ };
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Helpers
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /**
79
+ * Create a fake project directory with the right marker files for
80
+ * project-type detection.
81
+ * @param {string} root - Workspace root
82
+ * @param {string} localPath - Relative path to project dir
83
+ * @param {'node'|'python'|'go'|'unknown'} type - Project type to simulate
84
+ * @param {object} [options] - Extra options
85
+ * @param {string} [options.nodeVersion] - Node.js version for .nvmrc
86
+ * @param {object} [options.engines] - package.json engines field
87
+ */
88
+ function createFakeProject(root, localPath, type, options = {}) {
89
+ const dir = path.join(root, localPath);
90
+ fs.mkdirSync(dir, { recursive: true });
91
+
92
+ if (type === 'node') {
93
+ const pkg = { name: localPath, version: '1.0.0' };
94
+ if (options.engines) {
95
+ pkg.engines = options.engines;
96
+ }
97
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2));
98
+ if (options.nodeVersion) {
99
+ fs.writeFileSync(path.join(dir, '.nvmrc'), options.nodeVersion);
100
+ }
101
+ } else if (type === 'python') {
102
+ fs.writeFileSync(
103
+ path.join(dir, 'requirements.txt'),
104
+ 'flask==3.0.0\nnumpy==1.26.0\n'
105
+ );
106
+ } else if (type === 'go') {
107
+ fs.writeFileSync(
108
+ path.join(dir, 'go.mod'),
109
+ 'module example.com/gateway\n\ngo 1.21\n'
110
+ );
111
+ }
112
+ // 'unknown' — no marker files
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Tests
117
+ // ---------------------------------------------------------------------------
118
+
119
+ describe('setup-generator', () => {
120
+ let tempDir;
121
+
122
+ beforeEach(() => {
123
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-generator-test-'));
124
+ });
125
+
126
+ afterEach(() => {
127
+ fs.rmSync(tempDir, { recursive: true, force: true });
128
+ });
129
+
130
+ // -------------------------------------------------------------------------
131
+ // 1. Generates setup.md with prerequisites section
132
+ // -------------------------------------------------------------------------
133
+ it('generates setup.md with prerequisites section', async () => {
134
+ const registry = createMockRegistry([sampleNodeProject]);
135
+ createFakeProject(tempDir, 'api-service', 'node');
136
+
137
+ const generator = createSetupGenerator({ registry });
138
+ const md = await generator.generateSetupMd(tempDir);
139
+
140
+ expect(md).toContain('# '); // Has a markdown heading
141
+ expect(md).toContain('Prerequisites'); // Has prerequisites section
142
+ expect(md).toContain('Node'); // Mentions Node as a prerequisite
143
+ });
144
+
145
+ // -------------------------------------------------------------------------
146
+ // 2. Generates setup.sh with clone commands
147
+ // -------------------------------------------------------------------------
148
+ it('generates setup.sh with clone commands for each project', async () => {
149
+ const registry = createMockRegistry([sampleNodeProject, samplePythonProject]);
150
+ createFakeProject(tempDir, 'api-service', 'node');
151
+ createFakeProject(tempDir, 'ml-pipeline', 'python');
152
+
153
+ const generator = createSetupGenerator({ registry });
154
+ const sh = await generator.generateSetupSh(tempDir);
155
+
156
+ expect(sh).toContain('#!/bin/bash');
157
+ expect(sh).toContain('git clone');
158
+ expect(sh).toContain('git@github.com:myorg/api-service.git');
159
+ expect(sh).toContain('https://github.com/myorg/ml-pipeline.git');
160
+ });
161
+
162
+ // -------------------------------------------------------------------------
163
+ // 3. Setup.sh includes npm install for Node projects
164
+ // -------------------------------------------------------------------------
165
+ it('setup.sh includes npm install for Node projects', async () => {
166
+ const registry = createMockRegistry([sampleNodeProject]);
167
+ createFakeProject(tempDir, 'api-service', 'node');
168
+
169
+ const generator = createSetupGenerator({ registry });
170
+ const sh = await generator.generateSetupSh(tempDir);
171
+
172
+ expect(sh).toContain('npm install');
173
+ });
174
+
175
+ // -------------------------------------------------------------------------
176
+ // 4. Setup.sh includes pip install for Python projects
177
+ // -------------------------------------------------------------------------
178
+ it('setup.sh includes pip install for Python projects', async () => {
179
+ const registry = createMockRegistry([samplePythonProject]);
180
+ createFakeProject(tempDir, 'ml-pipeline', 'python');
181
+
182
+ const generator = createSetupGenerator({ registry });
183
+ const sh = await generator.generateSetupSh(tempDir);
184
+
185
+ expect(sh).toContain('pip install');
186
+ });
187
+
188
+ // -------------------------------------------------------------------------
189
+ // 5. Detects Node.js version from .nvmrc or package.json engines
190
+ // -------------------------------------------------------------------------
191
+ it('detects Node.js version from .nvmrc or package.json engines', async () => {
192
+ const registry = createMockRegistry([sampleNodeProject]);
193
+ createFakeProject(tempDir, 'api-service', 'node', { nodeVersion: '20.11.0' });
194
+
195
+ const generator = createSetupGenerator({ registry });
196
+ const md = await generator.generateSetupMd(tempDir);
197
+
198
+ expect(md).toContain('20.11.0');
199
+ });
200
+
201
+ // -------------------------------------------------------------------------
202
+ // 5b. Detects Node.js version from package.json engines field
203
+ // -------------------------------------------------------------------------
204
+ it('detects Node.js version from package.json engines field', async () => {
205
+ const registry = createMockRegistry([sampleNodeProject]);
206
+ createFakeProject(tempDir, 'api-service', 'node', {
207
+ engines: { node: '>=18.0.0' },
208
+ });
209
+
210
+ const generator = createSetupGenerator({ registry });
211
+ const md = await generator.generateSetupMd(tempDir);
212
+
213
+ expect(md).toContain('>=18.0.0');
214
+ });
215
+
216
+ // -------------------------------------------------------------------------
217
+ // 6. Includes Ollama model pull command
218
+ // -------------------------------------------------------------------------
219
+ it('includes Ollama model pull command', async () => {
220
+ const registry = createMockRegistry([sampleNodeProject]);
221
+ createFakeProject(tempDir, 'api-service', 'node');
222
+
223
+ const generator = createSetupGenerator({ registry });
224
+ const sh = await generator.generateSetupSh(tempDir);
225
+
226
+ expect(sh).toContain('ollama pull mxbai-embed-large');
227
+ });
228
+
229
+ // -------------------------------------------------------------------------
230
+ // 7. Includes vector rebuild mention
231
+ // -------------------------------------------------------------------------
232
+ it('includes vector rebuild mention in setup.sh', async () => {
233
+ const registry = createMockRegistry([sampleNodeProject]);
234
+ createFakeProject(tempDir, 'api-service', 'node');
235
+
236
+ const generator = createSetupGenerator({ registry });
237
+ const sh = await generator.generateSetupSh(tempDir);
238
+
239
+ // Should mention vector rebuild in some form
240
+ expect(sh).toMatch(/vector|rebuild|index/i);
241
+ });
242
+
243
+ // -------------------------------------------------------------------------
244
+ // 8. Script is idempotent (contains if [ ! -d checks before clone)
245
+ // -------------------------------------------------------------------------
246
+ it('script is idempotent with directory existence checks before clone', async () => {
247
+ const registry = createMockRegistry([sampleNodeProject]);
248
+ createFakeProject(tempDir, 'api-service', 'node');
249
+
250
+ const generator = createSetupGenerator({ registry });
251
+ const sh = await generator.generateSetupSh(tempDir);
252
+
253
+ // Should contain idempotency guards — check for directory before cloning
254
+ expect(sh).toContain('if [ ! -d');
255
+ });
256
+
257
+ // -------------------------------------------------------------------------
258
+ // 9. Setup.md includes TLC dashboard start instructions
259
+ // -------------------------------------------------------------------------
260
+ it('setup.md includes TLC dashboard start instructions', async () => {
261
+ const registry = createMockRegistry([sampleNodeProject]);
262
+ createFakeProject(tempDir, 'api-service', 'node');
263
+
264
+ const generator = createSetupGenerator({ registry });
265
+ const md = await generator.generateSetupMd(tempDir);
266
+
267
+ // Should mention starting the TLC dashboard
268
+ expect(md).toMatch(/dashboard|tlc.*start|tlc-server/i);
269
+ });
270
+
271
+ // -------------------------------------------------------------------------
272
+ // 10. Handles workspace with mixed project types (Node + Python)
273
+ // -------------------------------------------------------------------------
274
+ it('handles workspace with mixed project types (Node, Python, Go)', async () => {
275
+ const registry = createMockRegistry([
276
+ sampleNodeProject,
277
+ samplePythonProject,
278
+ sampleGoProject,
279
+ ]);
280
+ createFakeProject(tempDir, 'api-service', 'node');
281
+ createFakeProject(tempDir, 'ml-pipeline', 'python');
282
+ createFakeProject(tempDir, 'gateway', 'go');
283
+
284
+ const generator = createSetupGenerator({ registry });
285
+
286
+ const sh = await generator.generateSetupSh(tempDir);
287
+ const md = await generator.generateSetupMd(tempDir);
288
+
289
+ // sh should have npm install for Node project
290
+ expect(sh).toContain('npm install');
291
+ // sh should have pip install for Python project
292
+ expect(sh).toContain('pip install');
293
+ // sh should clone all three repos
294
+ expect(sh).toContain('api-service');
295
+ expect(sh).toContain('ml-pipeline');
296
+ expect(sh).toContain('gateway');
297
+
298
+ // md should mention multiple prerequisites
299
+ expect(md).toContain('Node');
300
+ expect(md).toContain('Python');
301
+ expect(md).toContain('Go');
302
+ });
303
+ });