tlc-claude-code 1.3.0 → 1.4.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/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
- package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Configuration - Define and persist multi-repo workspace structure
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILENAME = '.tlc-workspace.json';
|
|
9
|
+
const IGNORE_DIRS = ['node_modules', '.git', '.svn', '.hg', 'dist', 'build', 'coverage'];
|
|
10
|
+
|
|
11
|
+
class WorkspaceConfig {
|
|
12
|
+
constructor(rootDir) {
|
|
13
|
+
this.rootDir = rootDir;
|
|
14
|
+
this.configPath = path.join(rootDir, CONFIG_FILENAME);
|
|
15
|
+
this.config = this.load();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load existing config or return null
|
|
20
|
+
*/
|
|
21
|
+
load() {
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(this.configPath)) {
|
|
24
|
+
const data = fs.readFileSync(this.configPath, 'utf-8');
|
|
25
|
+
return JSON.parse(data);
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error('Failed to load workspace config:', err.message);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save config to file
|
|
35
|
+
*/
|
|
36
|
+
save() {
|
|
37
|
+
try {
|
|
38
|
+
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error('Failed to save workspace config:', err.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Initialize a new workspace
|
|
46
|
+
*/
|
|
47
|
+
init() {
|
|
48
|
+
this.config = {
|
|
49
|
+
root: this.rootDir,
|
|
50
|
+
repos: [],
|
|
51
|
+
createdAt: new Date().toISOString(),
|
|
52
|
+
};
|
|
53
|
+
this.save();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get current config
|
|
58
|
+
*/
|
|
59
|
+
getConfig() {
|
|
60
|
+
return this.config || { root: this.rootDir, repos: [] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add a repo to the workspace
|
|
65
|
+
* @param {string} repoPath - Relative or absolute path to repo
|
|
66
|
+
*/
|
|
67
|
+
addRepo(repoPath) {
|
|
68
|
+
if (!this.config) {
|
|
69
|
+
throw new Error('Workspace not initialized. Run init() first.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Convert to relative path
|
|
73
|
+
const relativePath = this.toRelativePath(repoPath);
|
|
74
|
+
const absolutePath = path.join(this.rootDir, relativePath);
|
|
75
|
+
|
|
76
|
+
// Validate repo exists
|
|
77
|
+
if (!fs.existsSync(absolutePath)) {
|
|
78
|
+
throw new Error(`Repository not found: ${repoPath}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add if not already present
|
|
82
|
+
if (!this.config.repos.includes(relativePath)) {
|
|
83
|
+
this.config.repos.push(relativePath);
|
|
84
|
+
this.save();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Remove a repo from the workspace
|
|
90
|
+
* @param {string} repoPath - Relative or absolute path to repo
|
|
91
|
+
*/
|
|
92
|
+
removeRepo(repoPath) {
|
|
93
|
+
if (!this.config) return;
|
|
94
|
+
|
|
95
|
+
const relativePath = this.toRelativePath(repoPath);
|
|
96
|
+
this.config.repos = this.config.repos.filter(r => r !== relativePath);
|
|
97
|
+
this.save();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Convert absolute path to relative
|
|
102
|
+
*/
|
|
103
|
+
toRelativePath(inputPath) {
|
|
104
|
+
if (path.isAbsolute(inputPath)) {
|
|
105
|
+
return path.relative(this.rootDir, inputPath);
|
|
106
|
+
}
|
|
107
|
+
return inputPath;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Discover repos in subdirectories
|
|
112
|
+
* @returns {string[]} Array of relative paths to discovered repos
|
|
113
|
+
*/
|
|
114
|
+
discoverRepos() {
|
|
115
|
+
const discovered = [];
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const entries = fs.readdirSync(this.rootDir, { withFileTypes: true });
|
|
119
|
+
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (!entry.isDirectory()) continue;
|
|
122
|
+
if (entry.name.startsWith('.')) continue; // Skip hidden
|
|
123
|
+
if (IGNORE_DIRS.includes(entry.name)) continue;
|
|
124
|
+
|
|
125
|
+
const subDir = path.join(this.rootDir, entry.name);
|
|
126
|
+
|
|
127
|
+
// Check if it has package.json (Node.js project)
|
|
128
|
+
if (fs.existsSync(path.join(subDir, 'package.json'))) {
|
|
129
|
+
discovered.push(entry.name);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if it has pyproject.toml or setup.py (Python project)
|
|
134
|
+
if (
|
|
135
|
+
fs.existsSync(path.join(subDir, 'pyproject.toml')) ||
|
|
136
|
+
fs.existsSync(path.join(subDir, 'setup.py'))
|
|
137
|
+
) {
|
|
138
|
+
discovered.push(entry.name);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if it has go.mod (Go project)
|
|
143
|
+
if (fs.existsSync(path.join(subDir, 'go.mod'))) {
|
|
144
|
+
discovered.push(entry.name);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error('Failed to discover repos:', err.message);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return discovered;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Expand glob pattern to matching paths
|
|
157
|
+
* @param {string} pattern - Glob pattern like "packages/*"
|
|
158
|
+
* @returns {string[]} Matching relative paths
|
|
159
|
+
*/
|
|
160
|
+
expandGlob(pattern) {
|
|
161
|
+
const results = [];
|
|
162
|
+
|
|
163
|
+
// Simple glob expansion for "dir/*" pattern
|
|
164
|
+
if (pattern.endsWith('/*')) {
|
|
165
|
+
const baseDir = pattern.slice(0, -2);
|
|
166
|
+
const absoluteBase = path.join(this.rootDir, baseDir);
|
|
167
|
+
|
|
168
|
+
if (fs.existsSync(absoluteBase)) {
|
|
169
|
+
try {
|
|
170
|
+
const entries = fs.readdirSync(absoluteBase, { withFileTypes: true });
|
|
171
|
+
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
if (!entry.isDirectory()) continue;
|
|
174
|
+
if (entry.name.startsWith('.')) continue;
|
|
175
|
+
|
|
176
|
+
const subPath = path.join(baseDir, entry.name);
|
|
177
|
+
const absolutePath = path.join(this.rootDir, subPath);
|
|
178
|
+
|
|
179
|
+
// Check if it's a valid project
|
|
180
|
+
if (
|
|
181
|
+
fs.existsSync(path.join(absolutePath, 'package.json')) ||
|
|
182
|
+
fs.existsSync(path.join(absolutePath, 'pyproject.toml')) ||
|
|
183
|
+
fs.existsSync(path.join(absolutePath, 'go.mod'))
|
|
184
|
+
) {
|
|
185
|
+
results.push(subPath);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error('Failed to expand glob:', err.message);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Detect npm/pnpm/yarn workspaces from root package.json
|
|
199
|
+
* @returns {string[]} Workspace package paths
|
|
200
|
+
*/
|
|
201
|
+
detectNpmWorkspaces() {
|
|
202
|
+
const packageJsonPath = path.join(this.rootDir, 'package.json');
|
|
203
|
+
|
|
204
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
210
|
+
const workspaces = packageJson.workspaces;
|
|
211
|
+
|
|
212
|
+
if (!workspaces) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Handle array of patterns
|
|
217
|
+
const patterns = Array.isArray(workspaces) ? workspaces : workspaces.packages || [];
|
|
218
|
+
const results = [];
|
|
219
|
+
|
|
220
|
+
for (const pattern of patterns) {
|
|
221
|
+
const expanded = this.expandGlob(pattern);
|
|
222
|
+
results.push(...expanded);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return results;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error('Failed to detect npm workspaces:', err.message);
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get info about a specific repo
|
|
234
|
+
* @param {string} repoPath - Relative path to repo
|
|
235
|
+
* @returns {Object} Repo info
|
|
236
|
+
*/
|
|
237
|
+
getRepoInfo(repoPath) {
|
|
238
|
+
const absolutePath = path.join(this.rootDir, repoPath);
|
|
239
|
+
const info = {
|
|
240
|
+
path: repoPath,
|
|
241
|
+
name: repoPath,
|
|
242
|
+
hasTlc: false,
|
|
243
|
+
hasPackageJson: false,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Check for package.json
|
|
247
|
+
const packageJsonPath = path.join(absolutePath, 'package.json');
|
|
248
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
249
|
+
info.hasPackageJson = true;
|
|
250
|
+
try {
|
|
251
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
252
|
+
info.name = pkg.name || repoPath;
|
|
253
|
+
info.version = pkg.version;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
// Ignore parse errors
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check for .tlc.json
|
|
260
|
+
const tlcPath = path.join(absolutePath, '.tlc.json');
|
|
261
|
+
info.hasTlc = fs.existsSync(tlcPath);
|
|
262
|
+
|
|
263
|
+
return info;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
WorkspaceConfig,
|
|
269
|
+
CONFIG_FILENAME,
|
|
270
|
+
};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
const { WorkspaceConfig } = await import('./workspace-config.js');
|
|
7
|
+
|
|
8
|
+
describe('WorkspaceConfig', () => {
|
|
9
|
+
let tempDir;
|
|
10
|
+
let workspaceConfig;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-config-test-'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('initialization', () => {
|
|
21
|
+
it('creates workspace config file', () => {
|
|
22
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
23
|
+
workspaceConfig.init();
|
|
24
|
+
|
|
25
|
+
const configPath = path.join(tempDir, '.tlc-workspace.json');
|
|
26
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('initializes with empty repos list', () => {
|
|
30
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
31
|
+
workspaceConfig.init();
|
|
32
|
+
|
|
33
|
+
const config = workspaceConfig.getConfig();
|
|
34
|
+
expect(config.repos).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('sets workspace root in config', () => {
|
|
38
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
39
|
+
workspaceConfig.init();
|
|
40
|
+
|
|
41
|
+
const config = workspaceConfig.getConfig();
|
|
42
|
+
expect(config.root).toBe(tempDir);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('addRepo', () => {
|
|
47
|
+
it('adds repo to workspace', () => {
|
|
48
|
+
// Create a mock repo directory
|
|
49
|
+
const repoPath = path.join(tempDir, 'my-repo');
|
|
50
|
+
fs.mkdirSync(repoPath);
|
|
51
|
+
fs.writeFileSync(path.join(repoPath, 'package.json'), '{"name": "my-repo"}');
|
|
52
|
+
|
|
53
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
54
|
+
workspaceConfig.init();
|
|
55
|
+
workspaceConfig.addRepo('my-repo');
|
|
56
|
+
|
|
57
|
+
const config = workspaceConfig.getConfig();
|
|
58
|
+
expect(config.repos).toContain('my-repo');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('stores relative paths only', () => {
|
|
62
|
+
const repoPath = path.join(tempDir, 'my-repo');
|
|
63
|
+
fs.mkdirSync(repoPath);
|
|
64
|
+
fs.writeFileSync(path.join(repoPath, 'package.json'), '{}');
|
|
65
|
+
|
|
66
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
67
|
+
workspaceConfig.init();
|
|
68
|
+
workspaceConfig.addRepo(repoPath); // absolute path
|
|
69
|
+
|
|
70
|
+
const config = workspaceConfig.getConfig();
|
|
71
|
+
expect(config.repos).toContain('my-repo'); // stored as relative
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('rejects non-existent repo path', () => {
|
|
75
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
76
|
+
workspaceConfig.init();
|
|
77
|
+
|
|
78
|
+
expect(() => workspaceConfig.addRepo('non-existent')).toThrow(/not found|does not exist/i);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('prevents duplicate repos', () => {
|
|
82
|
+
const repoPath = path.join(tempDir, 'my-repo');
|
|
83
|
+
fs.mkdirSync(repoPath);
|
|
84
|
+
fs.writeFileSync(path.join(repoPath, 'package.json'), '{}');
|
|
85
|
+
|
|
86
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
87
|
+
workspaceConfig.init();
|
|
88
|
+
workspaceConfig.addRepo('my-repo');
|
|
89
|
+
workspaceConfig.addRepo('my-repo'); // duplicate
|
|
90
|
+
|
|
91
|
+
const config = workspaceConfig.getConfig();
|
|
92
|
+
expect(config.repos.filter(r => r === 'my-repo').length).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('removeRepo', () => {
|
|
97
|
+
it('removes repo from workspace', () => {
|
|
98
|
+
const repoPath = path.join(tempDir, 'my-repo');
|
|
99
|
+
fs.mkdirSync(repoPath);
|
|
100
|
+
fs.writeFileSync(path.join(repoPath, 'package.json'), '{}');
|
|
101
|
+
|
|
102
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
103
|
+
workspaceConfig.init();
|
|
104
|
+
workspaceConfig.addRepo('my-repo');
|
|
105
|
+
workspaceConfig.removeRepo('my-repo');
|
|
106
|
+
|
|
107
|
+
const config = workspaceConfig.getConfig();
|
|
108
|
+
expect(config.repos).not.toContain('my-repo');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('handles removing non-existent repo gracefully', () => {
|
|
112
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
113
|
+
workspaceConfig.init();
|
|
114
|
+
|
|
115
|
+
expect(() => workspaceConfig.removeRepo('not-there')).not.toThrow();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('auto-discovery', () => {
|
|
120
|
+
it('auto-discovers repos in subdirectories', () => {
|
|
121
|
+
// Create subdirectories with package.json
|
|
122
|
+
fs.mkdirSync(path.join(tempDir, 'repo-a'));
|
|
123
|
+
fs.writeFileSync(path.join(tempDir, 'repo-a', 'package.json'), '{"name": "repo-a"}');
|
|
124
|
+
fs.mkdirSync(path.join(tempDir, 'repo-b'));
|
|
125
|
+
fs.writeFileSync(path.join(tempDir, 'repo-b', 'package.json'), '{"name": "repo-b"}');
|
|
126
|
+
|
|
127
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
128
|
+
const discovered = workspaceConfig.discoverRepos();
|
|
129
|
+
|
|
130
|
+
expect(discovered).toContain('repo-a');
|
|
131
|
+
expect(discovered).toContain('repo-b');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('detects package.json in subdirs', () => {
|
|
135
|
+
fs.mkdirSync(path.join(tempDir, 'my-app'));
|
|
136
|
+
fs.writeFileSync(path.join(tempDir, 'my-app', 'package.json'), '{}');
|
|
137
|
+
|
|
138
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
139
|
+
const discovered = workspaceConfig.discoverRepos();
|
|
140
|
+
|
|
141
|
+
expect(discovered).toContain('my-app');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('ignores node_modules directory', () => {
|
|
145
|
+
fs.mkdirSync(path.join(tempDir, 'node_modules'));
|
|
146
|
+
fs.mkdirSync(path.join(tempDir, 'node_modules', 'some-pkg'));
|
|
147
|
+
fs.writeFileSync(path.join(tempDir, 'node_modules', 'some-pkg', 'package.json'), '{}');
|
|
148
|
+
|
|
149
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
150
|
+
const discovered = workspaceConfig.discoverRepos();
|
|
151
|
+
|
|
152
|
+
expect(discovered).not.toContain('node_modules');
|
|
153
|
+
expect(discovered).not.toContain(path.join('node_modules', 'some-pkg'));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('ignores .git directory', () => {
|
|
157
|
+
fs.mkdirSync(path.join(tempDir, '.git'));
|
|
158
|
+
fs.writeFileSync(path.join(tempDir, '.git', 'config'), '');
|
|
159
|
+
|
|
160
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
161
|
+
const discovered = workspaceConfig.discoverRepos();
|
|
162
|
+
|
|
163
|
+
expect(discovered).not.toContain('.git');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('ignores hidden directories', () => {
|
|
167
|
+
fs.mkdirSync(path.join(tempDir, '.hidden-repo'));
|
|
168
|
+
fs.writeFileSync(path.join(tempDir, '.hidden-repo', 'package.json'), '{}');
|
|
169
|
+
|
|
170
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
171
|
+
const discovered = workspaceConfig.discoverRepos();
|
|
172
|
+
|
|
173
|
+
expect(discovered).not.toContain('.hidden-repo');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('glob patterns', () => {
|
|
178
|
+
it('expands glob patterns (packages/*)', () => {
|
|
179
|
+
fs.mkdirSync(path.join(tempDir, 'packages'));
|
|
180
|
+
fs.mkdirSync(path.join(tempDir, 'packages', 'core'));
|
|
181
|
+
fs.writeFileSync(path.join(tempDir, 'packages', 'core', 'package.json'), '{}');
|
|
182
|
+
fs.mkdirSync(path.join(tempDir, 'packages', 'utils'));
|
|
183
|
+
fs.writeFileSync(path.join(tempDir, 'packages', 'utils', 'package.json'), '{}');
|
|
184
|
+
|
|
185
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
186
|
+
const expanded = workspaceConfig.expandGlob('packages/*');
|
|
187
|
+
|
|
188
|
+
expect(expanded).toContain('packages/core');
|
|
189
|
+
expect(expanded).toContain('packages/utils');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('npm workspaces detection', () => {
|
|
194
|
+
it('detects npm workspaces from root package.json', () => {
|
|
195
|
+
fs.writeFileSync(
|
|
196
|
+
path.join(tempDir, 'package.json'),
|
|
197
|
+
JSON.stringify({
|
|
198
|
+
name: 'monorepo',
|
|
199
|
+
workspaces: ['packages/*'],
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
fs.mkdirSync(path.join(tempDir, 'packages'));
|
|
203
|
+
fs.mkdirSync(path.join(tempDir, 'packages', 'api'));
|
|
204
|
+
fs.writeFileSync(path.join(tempDir, 'packages', 'api', 'package.json'), '{}');
|
|
205
|
+
|
|
206
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
207
|
+
const workspaces = workspaceConfig.detectNpmWorkspaces();
|
|
208
|
+
|
|
209
|
+
expect(workspaces).toContain('packages/api');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('returns empty array if no workspaces defined', () => {
|
|
213
|
+
fs.writeFileSync(
|
|
214
|
+
path.join(tempDir, 'package.json'),
|
|
215
|
+
JSON.stringify({ name: 'simple-project' })
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
219
|
+
const workspaces = workspaceConfig.detectNpmWorkspaces();
|
|
220
|
+
|
|
221
|
+
expect(workspaces).toEqual([]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('handles missing root package.json', () => {
|
|
225
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
226
|
+
const workspaces = workspaceConfig.detectNpmWorkspaces();
|
|
227
|
+
|
|
228
|
+
expect(workspaces).toEqual([]);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('persistence', () => {
|
|
233
|
+
it('saves config to .tlc-workspace.json', () => {
|
|
234
|
+
const repoPath = path.join(tempDir, 'my-repo');
|
|
235
|
+
fs.mkdirSync(repoPath);
|
|
236
|
+
fs.writeFileSync(path.join(repoPath, 'package.json'), '{}');
|
|
237
|
+
|
|
238
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
239
|
+
workspaceConfig.init();
|
|
240
|
+
workspaceConfig.addRepo('my-repo');
|
|
241
|
+
|
|
242
|
+
// Read file directly
|
|
243
|
+
const configPath = path.join(tempDir, '.tlc-workspace.json');
|
|
244
|
+
const saved = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
245
|
+
|
|
246
|
+
expect(saved.repos).toContain('my-repo');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('loads existing config on construction', () => {
|
|
250
|
+
// Pre-create config file
|
|
251
|
+
const configPath = path.join(tempDir, '.tlc-workspace.json');
|
|
252
|
+
fs.writeFileSync(
|
|
253
|
+
configPath,
|
|
254
|
+
JSON.stringify({ root: tempDir, repos: ['existing-repo'] })
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
258
|
+
const config = workspaceConfig.getConfig();
|
|
259
|
+
|
|
260
|
+
expect(config.repos).toContain('existing-repo');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('getRepoInfo', () => {
|
|
265
|
+
it('returns info about a repo', () => {
|
|
266
|
+
const repoPath = path.join(tempDir, 'my-repo');
|
|
267
|
+
fs.mkdirSync(repoPath);
|
|
268
|
+
fs.writeFileSync(
|
|
269
|
+
path.join(repoPath, 'package.json'),
|
|
270
|
+
JSON.stringify({ name: 'my-repo', version: '1.0.0' })
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
274
|
+
workspaceConfig.init();
|
|
275
|
+
workspaceConfig.addRepo('my-repo');
|
|
276
|
+
|
|
277
|
+
const info = workspaceConfig.getRepoInfo('my-repo');
|
|
278
|
+
|
|
279
|
+
expect(info.name).toBe('my-repo');
|
|
280
|
+
expect(info.path).toBe('my-repo');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('detects if repo has TLC config', () => {
|
|
284
|
+
const repoPath = path.join(tempDir, 'my-repo');
|
|
285
|
+
fs.mkdirSync(repoPath);
|
|
286
|
+
fs.writeFileSync(path.join(repoPath, 'package.json'), '{}');
|
|
287
|
+
fs.writeFileSync(path.join(repoPath, '.tlc.json'), '{}');
|
|
288
|
+
|
|
289
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
290
|
+
workspaceConfig.init();
|
|
291
|
+
workspaceConfig.addRepo('my-repo');
|
|
292
|
+
|
|
293
|
+
const info = workspaceConfig.getRepoInfo('my-repo');
|
|
294
|
+
|
|
295
|
+
expect(info.hasTlc).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('detects if repo is missing TLC config', () => {
|
|
299
|
+
const repoPath = path.join(tempDir, 'my-repo');
|
|
300
|
+
fs.mkdirSync(repoPath);
|
|
301
|
+
fs.writeFileSync(path.join(repoPath, 'package.json'), '{}');
|
|
302
|
+
|
|
303
|
+
workspaceConfig = new WorkspaceConfig(tempDir);
|
|
304
|
+
workspaceConfig.init();
|
|
305
|
+
workspaceConfig.addRepo('my-repo');
|
|
306
|
+
|
|
307
|
+
const info = workspaceConfig.getRepoInfo('my-repo');
|
|
308
|
+
|
|
309
|
+
expect(info.hasTlc).toBe(false);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|