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.
Files changed (105) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
  14. package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
  15. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  16. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  17. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  18. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  19. package/package.json +1 -1
  20. package/server/lib/access-control-doc.js +541 -0
  21. package/server/lib/access-control-doc.test.js +672 -0
  22. package/server/lib/adr-generator.js +423 -0
  23. package/server/lib/adr-generator.test.js +586 -0
  24. package/server/lib/agent-progress-monitor.js +223 -0
  25. package/server/lib/agent-progress-monitor.test.js +202 -0
  26. package/server/lib/audit-attribution.js +191 -0
  27. package/server/lib/audit-attribution.test.js +359 -0
  28. package/server/lib/audit-classifier.js +202 -0
  29. package/server/lib/audit-classifier.test.js +209 -0
  30. package/server/lib/audit-command.js +275 -0
  31. package/server/lib/audit-command.test.js +325 -0
  32. package/server/lib/audit-exporter.js +380 -0
  33. package/server/lib/audit-exporter.test.js +464 -0
  34. package/server/lib/audit-logger.js +236 -0
  35. package/server/lib/audit-logger.test.js +364 -0
  36. package/server/lib/audit-query.js +257 -0
  37. package/server/lib/audit-query.test.js +352 -0
  38. package/server/lib/audit-storage.js +269 -0
  39. package/server/lib/audit-storage.test.js +272 -0
  40. package/server/lib/bulk-repo-init.js +342 -0
  41. package/server/lib/bulk-repo-init.test.js +388 -0
  42. package/server/lib/compliance-checklist.js +866 -0
  43. package/server/lib/compliance-checklist.test.js +476 -0
  44. package/server/lib/compliance-command.js +616 -0
  45. package/server/lib/compliance-command.test.js +551 -0
  46. package/server/lib/compliance-reporter.js +692 -0
  47. package/server/lib/compliance-reporter.test.js +707 -0
  48. package/server/lib/data-flow-doc.js +665 -0
  49. package/server/lib/data-flow-doc.test.js +659 -0
  50. package/server/lib/ephemeral-storage.js +249 -0
  51. package/server/lib/ephemeral-storage.test.js +254 -0
  52. package/server/lib/evidence-collector.js +627 -0
  53. package/server/lib/evidence-collector.test.js +901 -0
  54. package/server/lib/flow-diagram-generator.js +474 -0
  55. package/server/lib/flow-diagram-generator.test.js +446 -0
  56. package/server/lib/idp-manager.js +626 -0
  57. package/server/lib/idp-manager.test.js +587 -0
  58. package/server/lib/memory-exclusion.js +326 -0
  59. package/server/lib/memory-exclusion.test.js +241 -0
  60. package/server/lib/mfa-handler.js +452 -0
  61. package/server/lib/mfa-handler.test.js +490 -0
  62. package/server/lib/oauth-flow.js +375 -0
  63. package/server/lib/oauth-flow.test.js +487 -0
  64. package/server/lib/oauth-registry.js +190 -0
  65. package/server/lib/oauth-registry.test.js +306 -0
  66. package/server/lib/readme-generator.js +490 -0
  67. package/server/lib/readme-generator.test.js +493 -0
  68. package/server/lib/repo-dependency-tracker.js +261 -0
  69. package/server/lib/repo-dependency-tracker.test.js +350 -0
  70. package/server/lib/retention-policy.js +281 -0
  71. package/server/lib/retention-policy.test.js +486 -0
  72. package/server/lib/role-mapper.js +236 -0
  73. package/server/lib/role-mapper.test.js +395 -0
  74. package/server/lib/saml-provider.js +765 -0
  75. package/server/lib/saml-provider.test.js +643 -0
  76. package/server/lib/security-policy-generator.js +682 -0
  77. package/server/lib/security-policy-generator.test.js +544 -0
  78. package/server/lib/sensitive-detector.js +112 -0
  79. package/server/lib/sensitive-detector.test.js +209 -0
  80. package/server/lib/service-interaction-diagram.js +700 -0
  81. package/server/lib/service-interaction-diagram.test.js +638 -0
  82. package/server/lib/service-summary.js +553 -0
  83. package/server/lib/service-summary.test.js +619 -0
  84. package/server/lib/session-purge.js +460 -0
  85. package/server/lib/session-purge.test.js +312 -0
  86. package/server/lib/sso-command.js +544 -0
  87. package/server/lib/sso-command.test.js +552 -0
  88. package/server/lib/sso-session.js +492 -0
  89. package/server/lib/sso-session.test.js +670 -0
  90. package/server/lib/workspace-command.js +249 -0
  91. package/server/lib/workspace-command.test.js +264 -0
  92. package/server/lib/workspace-config.js +270 -0
  93. package/server/lib/workspace-config.test.js +312 -0
  94. package/server/lib/workspace-docs-command.js +547 -0
  95. package/server/lib/workspace-docs-command.test.js +692 -0
  96. package/server/lib/workspace-memory.js +451 -0
  97. package/server/lib/workspace-memory.test.js +403 -0
  98. package/server/lib/workspace-scanner.js +452 -0
  99. package/server/lib/workspace-scanner.test.js +677 -0
  100. package/server/lib/workspace-test-runner.js +315 -0
  101. package/server/lib/workspace-test-runner.test.js +294 -0
  102. package/server/lib/zero-retention-command.js +439 -0
  103. package/server/lib/zero-retention-command.test.js +448 -0
  104. package/server/lib/zero-retention.js +322 -0
  105. 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
+ });