tlc-claude-code 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Command - CLI interface for workspace operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { WorkspaceConfig } = require('./workspace-config.js');
|
|
8
|
+
const { BulkRepoInit } = require('./bulk-repo-init.js');
|
|
9
|
+
const { WorkspaceTestRunner } = require('./workspace-test-runner.js');
|
|
10
|
+
const { RepoDependencyTracker } = require('./repo-dependency-tracker.js');
|
|
11
|
+
|
|
12
|
+
const CONFIG_FILENAME = '.tlc-workspace.json';
|
|
13
|
+
|
|
14
|
+
class WorkspaceCommand {
|
|
15
|
+
constructor(rootDir) {
|
|
16
|
+
this.rootDir = rootDir;
|
|
17
|
+
this.configPath = path.join(rootDir, CONFIG_FILENAME);
|
|
18
|
+
this.config = null;
|
|
19
|
+
this._loadConfig();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load existing workspace config
|
|
24
|
+
*/
|
|
25
|
+
_loadConfig() {
|
|
26
|
+
if (fs.existsSync(this.configPath)) {
|
|
27
|
+
try {
|
|
28
|
+
this.config = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
|
|
29
|
+
} catch (err) {
|
|
30
|
+
this.config = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Save workspace config
|
|
37
|
+
*/
|
|
38
|
+
_saveConfig() {
|
|
39
|
+
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if workspace is initialized
|
|
44
|
+
*/
|
|
45
|
+
isInitialized() {
|
|
46
|
+
return this.config !== null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get current config
|
|
51
|
+
*/
|
|
52
|
+
getConfig() {
|
|
53
|
+
return this.config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize workspace - scan, detect, and optionally bulk-init repos
|
|
58
|
+
*/
|
|
59
|
+
async init(options = {}) {
|
|
60
|
+
const { dryRun = false, confirm = false } = options;
|
|
61
|
+
|
|
62
|
+
const workspaceConfig = new WorkspaceConfig(this.rootDir);
|
|
63
|
+
const bulkInit = new BulkRepoInit(this.rootDir);
|
|
64
|
+
|
|
65
|
+
// Discover repos
|
|
66
|
+
const discovered = workspaceConfig.discoverRepos();
|
|
67
|
+
|
|
68
|
+
// Find uninitialized repos
|
|
69
|
+
const needsInit = bulkInit.findUninitializedRepos();
|
|
70
|
+
|
|
71
|
+
const result = {
|
|
72
|
+
discovered,
|
|
73
|
+
needsInit,
|
|
74
|
+
initialized: 0,
|
|
75
|
+
skipped: discovered.length - needsInit.length,
|
|
76
|
+
errors: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (dryRun) {
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (confirm) {
|
|
84
|
+
// Initialize all uninitialized repos
|
|
85
|
+
if (needsInit.length > 0) {
|
|
86
|
+
const initResult = bulkInit.initializeAll();
|
|
87
|
+
result.initialized = initResult.initialized;
|
|
88
|
+
result.errors = initResult.errors;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create workspace config
|
|
92
|
+
this.config = {
|
|
93
|
+
root: this.rootDir,
|
|
94
|
+
repos: discovered,
|
|
95
|
+
createdAt: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
this._saveConfig();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Add a repo to the workspace
|
|
105
|
+
*/
|
|
106
|
+
async add(repoPath) {
|
|
107
|
+
if (!this.isInitialized()) {
|
|
108
|
+
throw new Error('Workspace not initialized. Run init first.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const absolutePath = path.isAbsolute(repoPath)
|
|
112
|
+
? repoPath
|
|
113
|
+
: path.join(this.rootDir, repoPath);
|
|
114
|
+
|
|
115
|
+
const relativePath = path.relative(this.rootDir, absolutePath);
|
|
116
|
+
|
|
117
|
+
// Check repo exists
|
|
118
|
+
if (!fs.existsSync(absolutePath)) {
|
|
119
|
+
throw new Error(`Repository not found: ${repoPath}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Auto-initialize if no .tlc.json
|
|
123
|
+
const tlcPath = path.join(absolutePath, '.tlc.json');
|
|
124
|
+
if (!fs.existsSync(tlcPath)) {
|
|
125
|
+
const bulkInit = new BulkRepoInit(this.rootDir);
|
|
126
|
+
bulkInit.initializeRepo(relativePath);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Add to config if not already present
|
|
130
|
+
if (!this.config.repos.includes(relativePath)) {
|
|
131
|
+
this.config.repos.push(relativePath);
|
|
132
|
+
this._saveConfig();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Remove a repo from the workspace (does not delete files)
|
|
138
|
+
*/
|
|
139
|
+
async remove(repoPath) {
|
|
140
|
+
if (!this.isInitialized()) {
|
|
141
|
+
throw new Error('Workspace not initialized. Run init first.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const relativePath = path.isAbsolute(repoPath)
|
|
145
|
+
? path.relative(this.rootDir, repoPath)
|
|
146
|
+
: repoPath;
|
|
147
|
+
|
|
148
|
+
this.config.repos = this.config.repos.filter(r => r !== relativePath);
|
|
149
|
+
this._saveConfig();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* List all repos with status
|
|
154
|
+
*/
|
|
155
|
+
async list() {
|
|
156
|
+
if (!this.isInitialized()) {
|
|
157
|
+
throw new Error('Workspace not initialized. Run init first.');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const repos = [];
|
|
161
|
+
|
|
162
|
+
for (const repoName of this.config.repos) {
|
|
163
|
+
const repoPath = path.join(this.rootDir, repoName);
|
|
164
|
+
const info = {
|
|
165
|
+
name: repoName,
|
|
166
|
+
path: repoName,
|
|
167
|
+
status: 'ready',
|
|
168
|
+
hasTlc: false,
|
|
169
|
+
packageName: repoName,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Check .tlc.json
|
|
173
|
+
const tlcPath = path.join(repoPath, '.tlc.json');
|
|
174
|
+
info.hasTlc = fs.existsSync(tlcPath);
|
|
175
|
+
info.status = info.hasTlc ? 'ready' : 'needs-init';
|
|
176
|
+
|
|
177
|
+
// Get package name
|
|
178
|
+
const pkgPath = path.join(repoPath, 'package.json');
|
|
179
|
+
if (fs.existsSync(pkgPath)) {
|
|
180
|
+
try {
|
|
181
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
182
|
+
if (pkg.name) {
|
|
183
|
+
info.packageName = pkg.name;
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
// Ignore
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
repos.push(info);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return repos;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Run tests across workspace
|
|
198
|
+
*/
|
|
199
|
+
async test(options = {}) {
|
|
200
|
+
if (!this.isInitialized()) {
|
|
201
|
+
throw new Error('Workspace not initialized. Run init first.');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const runner = new WorkspaceTestRunner(this.rootDir, this.config.repos);
|
|
205
|
+
return runner.runTests(options);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate dependency graph (Mermaid)
|
|
210
|
+
*/
|
|
211
|
+
async graph() {
|
|
212
|
+
if (!this.isInitialized()) {
|
|
213
|
+
throw new Error('Workspace not initialized. Run init first.');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const tracker = new RepoDependencyTracker(this.rootDir, this.config.repos);
|
|
217
|
+
return tracker.generateMermaidDiagram();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get workspace status overview
|
|
222
|
+
*/
|
|
223
|
+
async status() {
|
|
224
|
+
if (!this.isInitialized()) {
|
|
225
|
+
throw new Error('Workspace not initialized. Run init first.');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const repoList = await this.list();
|
|
229
|
+
const tracker = new RepoDependencyTracker(this.rootDir, this.config.repos);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
root: this.rootDir,
|
|
233
|
+
repoCount: this.config.repos.length,
|
|
234
|
+
repos: repoList.map(r => ({
|
|
235
|
+
name: r.name,
|
|
236
|
+
hasTlc: r.hasTlc,
|
|
237
|
+
status: r.status,
|
|
238
|
+
dependencies: tracker.getDependencies(r.name),
|
|
239
|
+
dependents: tracker.getDependents(r.name),
|
|
240
|
+
})),
|
|
241
|
+
dependencyGraph: tracker.getDependencyGraph(),
|
|
242
|
+
cycles: tracker.detectCircularDependencies(),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = {
|
|
248
|
+
WorkspaceCommand,
|
|
249
|
+
};
|
|
@@ -0,0 +1,264 @@
|
|
|
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 { WorkspaceCommand } = await import('./workspace-command.js');
|
|
7
|
+
|
|
8
|
+
describe('WorkspaceCommand', () => {
|
|
9
|
+
let tempDir;
|
|
10
|
+
let command;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-cmd-test-'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function createRepo(name, options = {}) {
|
|
21
|
+
const repoPath = path.join(tempDir, name);
|
|
22
|
+
fs.mkdirSync(repoPath, { recursive: true });
|
|
23
|
+
|
|
24
|
+
fs.writeFileSync(
|
|
25
|
+
path.join(repoPath, 'package.json'),
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
name: options.packageName || name,
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
scripts: { test: 'echo "pass"' },
|
|
30
|
+
dependencies: options.dependencies || {},
|
|
31
|
+
}, null, 2)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (options.hasTlc) {
|
|
35
|
+
fs.writeFileSync(
|
|
36
|
+
path.join(repoPath, '.tlc.json'),
|
|
37
|
+
JSON.stringify({ project: name }, null, 2)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return repoPath;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('init', () => {
|
|
45
|
+
it('scans subdirectories for repos', async () => {
|
|
46
|
+
createRepo('repo-a');
|
|
47
|
+
createRepo('repo-b');
|
|
48
|
+
|
|
49
|
+
command = new WorkspaceCommand(tempDir);
|
|
50
|
+
const result = await command.init({ dryRun: true });
|
|
51
|
+
|
|
52
|
+
expect(result.discovered).toContain('repo-a');
|
|
53
|
+
expect(result.discovered).toContain('repo-b');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('shows which repos need TLC setup', async () => {
|
|
57
|
+
createRepo('initialized', { hasTlc: true });
|
|
58
|
+
createRepo('uninitialized', { hasTlc: false });
|
|
59
|
+
|
|
60
|
+
command = new WorkspaceCommand(tempDir);
|
|
61
|
+
const result = await command.init({ dryRun: true });
|
|
62
|
+
|
|
63
|
+
expect(result.needsInit).toContain('uninitialized');
|
|
64
|
+
expect(result.needsInit).not.toContain('initialized');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('bulk-initializes repos on confirm', async () => {
|
|
68
|
+
createRepo('new-repo-1');
|
|
69
|
+
createRepo('new-repo-2');
|
|
70
|
+
|
|
71
|
+
command = new WorkspaceCommand(tempDir);
|
|
72
|
+
const result = await command.init({ confirm: true });
|
|
73
|
+
|
|
74
|
+
expect(fs.existsSync(path.join(tempDir, 'new-repo-1', '.tlc.json'))).toBe(true);
|
|
75
|
+
expect(fs.existsSync(path.join(tempDir, 'new-repo-2', '.tlc.json'))).toBe(true);
|
|
76
|
+
expect(result.initialized).toBe(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('creates .tlc-workspace.json', async () => {
|
|
80
|
+
createRepo('my-repo');
|
|
81
|
+
|
|
82
|
+
command = new WorkspaceCommand(tempDir);
|
|
83
|
+
await command.init({ confirm: true });
|
|
84
|
+
|
|
85
|
+
const configPath = path.join(tempDir, '.tlc-workspace.json');
|
|
86
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
87
|
+
|
|
88
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
89
|
+
expect(config.repos).toContain('my-repo');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('skips hidden directories and node_modules', async () => {
|
|
93
|
+
createRepo('valid-repo');
|
|
94
|
+
fs.mkdirSync(path.join(tempDir, '.hidden'));
|
|
95
|
+
fs.writeFileSync(path.join(tempDir, '.hidden', 'package.json'), '{}');
|
|
96
|
+
fs.mkdirSync(path.join(tempDir, 'node_modules'));
|
|
97
|
+
fs.writeFileSync(path.join(tempDir, 'node_modules', 'package.json'), '{}');
|
|
98
|
+
|
|
99
|
+
command = new WorkspaceCommand(tempDir);
|
|
100
|
+
const result = await command.init({ dryRun: true });
|
|
101
|
+
|
|
102
|
+
expect(result.discovered).toContain('valid-repo');
|
|
103
|
+
expect(result.discovered).not.toContain('.hidden');
|
|
104
|
+
expect(result.discovered).not.toContain('node_modules');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('add', () => {
|
|
109
|
+
it('adds repo to workspace', async () => {
|
|
110
|
+
createRepo('existing-repo', { hasTlc: true });
|
|
111
|
+
|
|
112
|
+
command = new WorkspaceCommand(tempDir);
|
|
113
|
+
await command.init({ confirm: true });
|
|
114
|
+
await command.add('existing-repo');
|
|
115
|
+
|
|
116
|
+
const config = command.getConfig();
|
|
117
|
+
expect(config.repos).toContain('existing-repo');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('auto-initializes repo if no .tlc.json', async () => {
|
|
121
|
+
createRepo('uninit-repo');
|
|
122
|
+
|
|
123
|
+
command = new WorkspaceCommand(tempDir);
|
|
124
|
+
await command.init({ confirm: true });
|
|
125
|
+
await command.add('uninit-repo');
|
|
126
|
+
|
|
127
|
+
expect(fs.existsSync(path.join(tempDir, 'uninit-repo', '.tlc.json'))).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('throws if repo does not exist', async () => {
|
|
131
|
+
command = new WorkspaceCommand(tempDir);
|
|
132
|
+
await command.init({ confirm: true });
|
|
133
|
+
|
|
134
|
+
await expect(command.add('nonexistent')).rejects.toThrow(/not found/i);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('remove', () => {
|
|
139
|
+
it('removes repo from config', async () => {
|
|
140
|
+
createRepo('to-remove', { hasTlc: true });
|
|
141
|
+
|
|
142
|
+
command = new WorkspaceCommand(tempDir);
|
|
143
|
+
await command.init({ confirm: true });
|
|
144
|
+
await command.remove('to-remove');
|
|
145
|
+
|
|
146
|
+
const config = command.getConfig();
|
|
147
|
+
expect(config.repos).not.toContain('to-remove');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('does not delete repo files', async () => {
|
|
151
|
+
createRepo('keep-files', { hasTlc: true });
|
|
152
|
+
|
|
153
|
+
command = new WorkspaceCommand(tempDir);
|
|
154
|
+
await command.init({ confirm: true });
|
|
155
|
+
await command.remove('keep-files');
|
|
156
|
+
|
|
157
|
+
// Files should still exist
|
|
158
|
+
expect(fs.existsSync(path.join(tempDir, 'keep-files', 'package.json'))).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('list', () => {
|
|
163
|
+
it('shows repos with status (ready/needs-init)', async () => {
|
|
164
|
+
createRepo('ready-repo', { hasTlc: true });
|
|
165
|
+
createRepo('needs-init-repo', { hasTlc: false });
|
|
166
|
+
|
|
167
|
+
command = new WorkspaceCommand(tempDir);
|
|
168
|
+
await command.init({ confirm: true });
|
|
169
|
+
const list = await command.list();
|
|
170
|
+
|
|
171
|
+
const readyRepo = list.find(r => r.name === 'ready-repo');
|
|
172
|
+
const needsInitRepo = list.find(r => r.name === 'needs-init-repo');
|
|
173
|
+
|
|
174
|
+
expect(readyRepo.status).toBe('ready');
|
|
175
|
+
// After init with confirm, all repos should be ready
|
|
176
|
+
expect(needsInitRepo.status).toBe('ready');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('shows repo names and paths', async () => {
|
|
180
|
+
createRepo('my-app', { packageName: '@org/my-app' });
|
|
181
|
+
|
|
182
|
+
command = new WorkspaceCommand(tempDir);
|
|
183
|
+
await command.init({ confirm: true });
|
|
184
|
+
const list = await command.list();
|
|
185
|
+
|
|
186
|
+
const repo = list.find(r => r.name === 'my-app');
|
|
187
|
+
expect(repo.packageName).toBe('@org/my-app');
|
|
188
|
+
expect(repo.path).toBe('my-app');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('test', () => {
|
|
193
|
+
it('runs unified test runner', async () => {
|
|
194
|
+
createRepo('test-repo');
|
|
195
|
+
|
|
196
|
+
command = new WorkspaceCommand(tempDir);
|
|
197
|
+
await command.init({ confirm: true });
|
|
198
|
+
const result = await command.test();
|
|
199
|
+
|
|
200
|
+
expect(result.summary.total).toBe(1);
|
|
201
|
+
expect(result.summary.passed).toBe(1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('passes options to test runner', async () => {
|
|
205
|
+
createRepo('repo-1');
|
|
206
|
+
createRepo('repo-2');
|
|
207
|
+
|
|
208
|
+
command = new WorkspaceCommand(tempDir);
|
|
209
|
+
await command.init({ confirm: true });
|
|
210
|
+
const result = await command.test({ filter: ['repo-1'] });
|
|
211
|
+
|
|
212
|
+
expect(Object.keys(result.repos)).toEqual(['repo-1']);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('graph', () => {
|
|
217
|
+
it('outputs Mermaid diagram', async () => {
|
|
218
|
+
createRepo('core', { packageName: 'core' });
|
|
219
|
+
createRepo('api', {
|
|
220
|
+
packageName: 'api',
|
|
221
|
+
dependencies: { core: 'workspace:*' },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
command = new WorkspaceCommand(tempDir);
|
|
225
|
+
await command.init({ confirm: true });
|
|
226
|
+
const diagram = await command.graph();
|
|
227
|
+
|
|
228
|
+
expect(diagram).toContain('graph TD');
|
|
229
|
+
expect(diagram).toContain('api');
|
|
230
|
+
expect(diagram).toContain('core');
|
|
231
|
+
expect(diagram).toContain('-->');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('status', () => {
|
|
236
|
+
it('shows repo health overview', async () => {
|
|
237
|
+
createRepo('healthy-repo', { hasTlc: true });
|
|
238
|
+
|
|
239
|
+
command = new WorkspaceCommand(tempDir);
|
|
240
|
+
await command.init({ confirm: true });
|
|
241
|
+
const status = await command.status();
|
|
242
|
+
|
|
243
|
+
expect(status.repos).toHaveLength(1);
|
|
244
|
+
expect(status.repos[0].name).toBe('healthy-repo');
|
|
245
|
+
expect(status.repos[0].hasTlc).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('error handling', () => {
|
|
250
|
+
it('handles workspace not initialized error', async () => {
|
|
251
|
+
command = new WorkspaceCommand(tempDir);
|
|
252
|
+
|
|
253
|
+
await expect(command.list()).rejects.toThrow(/not initialized/i);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('handles empty workspace', async () => {
|
|
257
|
+
command = new WorkspaceCommand(tempDir);
|
|
258
|
+
const result = await command.init({ confirm: true });
|
|
259
|
+
|
|
260
|
+
expect(result.discovered).toEqual([]);
|
|
261
|
+
expect(result.initialized).toBe(0);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|