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,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
|
+
});
|