tlc-claude-code 1.4.8 → 1.4.9
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/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks API Module
|
|
3
|
+
* REST API for task CRUD operations
|
|
4
|
+
*/
|
|
5
|
+
import { promises as defaultFs } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { createTlcIntrospection } from './tlc-introspection.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse tasks from PLAN.md content
|
|
11
|
+
* @param {string} content - Plan file content
|
|
12
|
+
* @returns {Array} Parsed tasks
|
|
13
|
+
*/
|
|
14
|
+
export function parseTasksFromPlan(content) {
|
|
15
|
+
const tasks = [];
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i];
|
|
20
|
+
|
|
21
|
+
// Match: ### Task N: Name [status@owner] or ### Task N: Name [status]
|
|
22
|
+
const taskMatch = line.match(/^###\s+Task\s+(\d+):\s*(.+?)\s*\[(x|>|\s*)(?:@(\w+))?\]/);
|
|
23
|
+
if (taskMatch) {
|
|
24
|
+
const number = parseInt(taskMatch[1], 10);
|
|
25
|
+
const subject = taskMatch[2].trim();
|
|
26
|
+
const marker = taskMatch[3];
|
|
27
|
+
const owner = taskMatch[4] || null;
|
|
28
|
+
|
|
29
|
+
let status;
|
|
30
|
+
if (marker === 'x') {
|
|
31
|
+
status = 'completed';
|
|
32
|
+
} else if (marker === '>') {
|
|
33
|
+
status = 'in_progress';
|
|
34
|
+
} else {
|
|
35
|
+
status = 'pending';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Look for goal in next lines
|
|
39
|
+
let goal = null;
|
|
40
|
+
for (let j = i + 1; j < lines.length && j < i + 5; j++) {
|
|
41
|
+
const goalMatch = lines[j].match(/^\*\*Goal:\*\*\s*(.+)$/);
|
|
42
|
+
if (goalMatch) {
|
|
43
|
+
goal = goalMatch[1].trim();
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
// Stop if we hit another task
|
|
47
|
+
if (lines[j].match(/^###\s+Task/)) break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
tasks.push({
|
|
51
|
+
number,
|
|
52
|
+
subject,
|
|
53
|
+
status,
|
|
54
|
+
owner,
|
|
55
|
+
goal
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return tasks;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Format task for API response
|
|
65
|
+
* @param {Object} task - Raw task object
|
|
66
|
+
* @param {number} phase - Phase number
|
|
67
|
+
* @returns {Object} Formatted task
|
|
68
|
+
*/
|
|
69
|
+
export function formatTaskForApi(task, phase) {
|
|
70
|
+
return {
|
|
71
|
+
id: `phase-${phase}-task-${task.number}`,
|
|
72
|
+
phase,
|
|
73
|
+
number: task.number,
|
|
74
|
+
subject: task.subject,
|
|
75
|
+
status: task.status,
|
|
76
|
+
owner: task.owner,
|
|
77
|
+
goal: task.goal
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get tasks from current phase
|
|
83
|
+
* @param {Object} options - Options
|
|
84
|
+
* @returns {Promise<Array>} Tasks array
|
|
85
|
+
*/
|
|
86
|
+
export async function getTasks(options = {}) {
|
|
87
|
+
const { introspection, fs: fileSystem = defaultFs, basePath = process.cwd() } = options;
|
|
88
|
+
|
|
89
|
+
const currentPhase = introspection.getCurrentPhase();
|
|
90
|
+
const phaseNumber = currentPhase?.number || 1;
|
|
91
|
+
|
|
92
|
+
// Try to read the phase plan file
|
|
93
|
+
const planPath = path.join(basePath, '.planning', 'phases', `${phaseNumber}-PLAN.md`);
|
|
94
|
+
|
|
95
|
+
let content = '';
|
|
96
|
+
try {
|
|
97
|
+
content = await fileSystem.readFile(planPath, 'utf-8');
|
|
98
|
+
} catch {
|
|
99
|
+
content = '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const tasks = parseTasksFromPlan(content);
|
|
103
|
+
return tasks.map(task => formatTaskForApi(task, phaseNumber));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a new task
|
|
108
|
+
* @param {Object} taskData - Task data
|
|
109
|
+
* @param {Object} options - Options
|
|
110
|
+
* @returns {Promise<Object>} Created task
|
|
111
|
+
*/
|
|
112
|
+
export async function createTask(taskData, options = {}) {
|
|
113
|
+
if (!taskData.subject) {
|
|
114
|
+
throw new Error('Subject is required');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { fs: fileSystem = defaultFs, basePath = process.cwd() } = options;
|
|
118
|
+
const phase = taskData.phase || 1;
|
|
119
|
+
|
|
120
|
+
const planPath = path.join(basePath, '.planning', 'phases', `${phase}-PLAN.md`);
|
|
121
|
+
|
|
122
|
+
let content = '';
|
|
123
|
+
try {
|
|
124
|
+
content = await fileSystem.readFile(planPath, 'utf-8');
|
|
125
|
+
} catch {
|
|
126
|
+
content = '## Tasks\n';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Find highest task number
|
|
130
|
+
const existingTasks = parseTasksFromPlan(content);
|
|
131
|
+
const maxNumber = existingTasks.reduce((max, t) => Math.max(max, t.number), 0);
|
|
132
|
+
const newNumber = maxNumber + 1;
|
|
133
|
+
|
|
134
|
+
// Create task markdown
|
|
135
|
+
const taskMd = `\n### Task ${newNumber}: ${taskData.subject} [ ]\n**Goal:** ${taskData.description || 'TBD'}\n`;
|
|
136
|
+
|
|
137
|
+
// Append to content
|
|
138
|
+
const newContent = content + taskMd;
|
|
139
|
+
await fileSystem.writeFile(planPath, newContent);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
id: `phase-${phase}-task-${newNumber}`,
|
|
143
|
+
phase,
|
|
144
|
+
number: newNumber,
|
|
145
|
+
subject: taskData.subject,
|
|
146
|
+
status: 'pending',
|
|
147
|
+
owner: null,
|
|
148
|
+
goal: taskData.description || 'TBD'
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Update an existing task
|
|
154
|
+
* @param {string} taskId - Task ID
|
|
155
|
+
* @param {Object} updates - Updates to apply
|
|
156
|
+
* @param {Object} options - Options
|
|
157
|
+
* @returns {Promise<Object>} Updated task
|
|
158
|
+
*/
|
|
159
|
+
export async function updateTask(taskId, updates, options = {}) {
|
|
160
|
+
const { fs: fileSystem = defaultFs, basePath = process.cwd() } = options;
|
|
161
|
+
|
|
162
|
+
// Parse task ID
|
|
163
|
+
const idMatch = taskId.match(/^(?:phase-(\d+)-)?task-(\d+)$/);
|
|
164
|
+
const phase = idMatch?.[1] ? parseInt(idMatch[1], 10) : 1;
|
|
165
|
+
const taskNumber = idMatch?.[2] ? parseInt(idMatch[2], 10) : parseInt(taskId.replace('task-', ''), 10);
|
|
166
|
+
|
|
167
|
+
const planPath = path.join(basePath, '.planning', 'phases', `${phase}-PLAN.md`);
|
|
168
|
+
|
|
169
|
+
let content = '';
|
|
170
|
+
try {
|
|
171
|
+
content = await fileSystem.readFile(planPath, 'utf-8');
|
|
172
|
+
} catch {
|
|
173
|
+
throw new Error(`Task ${taskId} not found`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const tasks = parseTasksFromPlan(content);
|
|
177
|
+
const task = tasks.find(t => t.number === taskNumber);
|
|
178
|
+
|
|
179
|
+
if (!task) {
|
|
180
|
+
throw new Error(`Task ${taskId} not found`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Apply updates
|
|
184
|
+
const updatedTask = { ...task };
|
|
185
|
+
if (updates.subject !== undefined) {
|
|
186
|
+
updatedTask.subject = updates.subject;
|
|
187
|
+
}
|
|
188
|
+
if (updates.status !== undefined) {
|
|
189
|
+
updatedTask.status = updates.status;
|
|
190
|
+
}
|
|
191
|
+
if (updates.owner !== undefined) {
|
|
192
|
+
updatedTask.owner = updates.owner;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Build new marker
|
|
196
|
+
let marker = ' ';
|
|
197
|
+
if (updatedTask.status === 'completed') marker = 'x';
|
|
198
|
+
else if (updatedTask.status === 'in_progress') marker = '>';
|
|
199
|
+
|
|
200
|
+
const ownerPart = updatedTask.owner ? `@${updatedTask.owner}` : '';
|
|
201
|
+
const newMarker = `[${marker}${ownerPart}]`;
|
|
202
|
+
|
|
203
|
+
// Replace in content
|
|
204
|
+
const oldPattern = new RegExp(
|
|
205
|
+
`(###\\s+Task\\s+${taskNumber}:\\s*)${task.subject.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s*\\[)[^\\]]*\\]`,
|
|
206
|
+
'g'
|
|
207
|
+
);
|
|
208
|
+
const newContent = content.replace(oldPattern, `$1${updatedTask.subject}$2${marker}${ownerPart}]`);
|
|
209
|
+
|
|
210
|
+
await fileSystem.writeFile(planPath, newContent);
|
|
211
|
+
|
|
212
|
+
return formatTaskForApi(updatedTask, phase);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Delete a task
|
|
217
|
+
* @param {string} taskId - Task ID
|
|
218
|
+
* @param {Object} options - Options
|
|
219
|
+
* @returns {Promise<void>}
|
|
220
|
+
*/
|
|
221
|
+
export async function deleteTask(taskId, options = {}) {
|
|
222
|
+
const { fs: fileSystem = defaultFs, basePath = process.cwd() } = options;
|
|
223
|
+
|
|
224
|
+
// Parse task ID
|
|
225
|
+
const idMatch = taskId.match(/^(?:phase-(\d+)-)?task-(\d+)$/);
|
|
226
|
+
const phase = idMatch?.[1] ? parseInt(idMatch[1], 10) : 1;
|
|
227
|
+
const taskNumber = idMatch?.[2] ? parseInt(idMatch[2], 10) : parseInt(taskId.replace('task-', ''), 10);
|
|
228
|
+
|
|
229
|
+
const planPath = path.join(basePath, '.planning', 'phases', `${phase}-PLAN.md`);
|
|
230
|
+
|
|
231
|
+
let content = '';
|
|
232
|
+
try {
|
|
233
|
+
content = await fileSystem.readFile(planPath, 'utf-8');
|
|
234
|
+
} catch {
|
|
235
|
+
throw new Error(`Task ${taskId} not found`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Remove the task section (from ### Task N to next ### or end)
|
|
239
|
+
const lines = content.split('\n');
|
|
240
|
+
const newLines = [];
|
|
241
|
+
let skipping = false;
|
|
242
|
+
|
|
243
|
+
for (const line of lines) {
|
|
244
|
+
const taskMatch = line.match(/^###\s+Task\s+(\d+):/);
|
|
245
|
+
if (taskMatch) {
|
|
246
|
+
const num = parseInt(taskMatch[1], 10);
|
|
247
|
+
if (num === taskNumber) {
|
|
248
|
+
skipping = true;
|
|
249
|
+
continue;
|
|
250
|
+
} else {
|
|
251
|
+
skipping = false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!skipping) {
|
|
256
|
+
newLines.push(line);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await fileSystem.writeFile(planPath, newLines.join('\n'));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Create Tasks API handlers
|
|
265
|
+
* @param {Object} options - Options
|
|
266
|
+
* @returns {Object} API handlers
|
|
267
|
+
*/
|
|
268
|
+
export function createTasksApi(options = {}) {
|
|
269
|
+
const { basePath = process.cwd(), fs: fileSystem = defaultFs } = options;
|
|
270
|
+
const introspection = options.introspection || createTlcIntrospection({ basePath, fs: fileSystem });
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
async get(query = {}) {
|
|
274
|
+
return getTasks({ introspection, fs: fileSystem, basePath });
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
async post(taskData) {
|
|
278
|
+
return createTask(taskData, { fs: fileSystem, basePath });
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async patch(taskId, updates) {
|
|
282
|
+
return updateTask(taskId, updates, { fs: fileSystem, basePath });
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
async delete(taskId) {
|
|
286
|
+
return deleteTask(taskId, { fs: fileSystem, basePath });
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks API Module Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { getTasks, createTask, updateTask, deleteTask, parseTasksFromPlan, formatTaskForApi, createTasksApi } from './tasks-api.js';
|
|
6
|
+
|
|
7
|
+
describe('tasks-api', () => {
|
|
8
|
+
describe('getTasks', () => {
|
|
9
|
+
it('returns tasks from current phase', async () => {
|
|
10
|
+
const mockIntrospection = {
|
|
11
|
+
getCurrentPhase: vi.fn().mockReturnValue({ number: 1 })
|
|
12
|
+
};
|
|
13
|
+
const mockFs = {
|
|
14
|
+
readFile: vi.fn().mockResolvedValue(`
|
|
15
|
+
### Task 1: Test task [ ]
|
|
16
|
+
**Goal:** Do something
|
|
17
|
+
### Task 2: Done task [x]
|
|
18
|
+
**Goal:** Did something
|
|
19
|
+
`)
|
|
20
|
+
};
|
|
21
|
+
const tasks = await getTasks({ introspection: mockIntrospection, fs: mockFs });
|
|
22
|
+
expect(tasks.length).toBe(2);
|
|
23
|
+
expect(tasks[0].subject).toBe('Test task');
|
|
24
|
+
expect(tasks[0].status).toBe('pending');
|
|
25
|
+
expect(tasks[1].status).toBe('completed');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns flat array format', async () => {
|
|
29
|
+
const mockIntrospection = {
|
|
30
|
+
getCurrentPhase: vi.fn().mockReturnValue({ number: 1 })
|
|
31
|
+
};
|
|
32
|
+
const mockFs = {
|
|
33
|
+
readFile: vi.fn().mockResolvedValue('### Task 1: Test [ ]')
|
|
34
|
+
};
|
|
35
|
+
const tasks = await getTasks({ introspection: mockIntrospection, fs: mockFs });
|
|
36
|
+
expect(Array.isArray(tasks)).toBe(true);
|
|
37
|
+
expect(tasks[0].id).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('createTask', () => {
|
|
42
|
+
it('creates task with required fields', async () => {
|
|
43
|
+
const mockFs = {
|
|
44
|
+
readFile: vi.fn().mockResolvedValue('## Tasks\n'),
|
|
45
|
+
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
46
|
+
};
|
|
47
|
+
const task = await createTask({
|
|
48
|
+
subject: 'New task',
|
|
49
|
+
description: 'Task description',
|
|
50
|
+
phase: 1
|
|
51
|
+
}, { fs: mockFs });
|
|
52
|
+
expect(task.id).toBeDefined();
|
|
53
|
+
expect(task.subject).toBe('New task');
|
|
54
|
+
expect(task.status).toBe('pending');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('validates required fields', async () => {
|
|
58
|
+
await expect(createTask({ description: 'No subject' }, {}))
|
|
59
|
+
.rejects.toThrow(/subject.*required/i);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('appends to PLAN.md', async () => {
|
|
63
|
+
const mockFs = {
|
|
64
|
+
readFile: vi.fn().mockResolvedValue('## Tasks\n'),
|
|
65
|
+
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
66
|
+
};
|
|
67
|
+
await createTask({ subject: 'Test', phase: 1 }, { fs: mockFs });
|
|
68
|
+
expect(mockFs.writeFile).toHaveBeenCalled();
|
|
69
|
+
const content = mockFs.writeFile.mock.calls[0][1];
|
|
70
|
+
expect(content).toContain('Test');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('updateTask', () => {
|
|
75
|
+
it('updates task status', async () => {
|
|
76
|
+
const mockFs = {
|
|
77
|
+
readFile: vi.fn().mockResolvedValue('### Task 1: Test [ ]'),
|
|
78
|
+
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
79
|
+
};
|
|
80
|
+
const task = await updateTask('task-1', { status: 'completed' }, { fs: mockFs });
|
|
81
|
+
expect(task.status).toBe('completed');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('updates task subject', async () => {
|
|
85
|
+
const mockFs = {
|
|
86
|
+
readFile: vi.fn().mockResolvedValue('### Task 1: Test [ ]'),
|
|
87
|
+
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
88
|
+
};
|
|
89
|
+
const task = await updateTask('task-1', { subject: 'Updated' }, { fs: mockFs });
|
|
90
|
+
expect(task.subject).toBe('Updated');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('throws for non-existent task', async () => {
|
|
94
|
+
const mockFs = {
|
|
95
|
+
readFile: vi.fn().mockResolvedValue('### Task 1: Test [ ]')
|
|
96
|
+
};
|
|
97
|
+
await expect(updateTask('task-99', {}, { fs: mockFs }))
|
|
98
|
+
.rejects.toThrow(/not found/i);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('deleteTask', () => {
|
|
103
|
+
it('removes task from plan', async () => {
|
|
104
|
+
const mockFs = {
|
|
105
|
+
readFile: vi.fn().mockResolvedValue('### Task 1: Test [ ]\n### Task 2: Keep [ ]'),
|
|
106
|
+
writeFile: vi.fn().mockResolvedValue(undefined)
|
|
107
|
+
};
|
|
108
|
+
await deleteTask('task-1', { fs: mockFs });
|
|
109
|
+
const content = mockFs.writeFile.mock.calls[0][1];
|
|
110
|
+
expect(content).not.toContain('Task 1');
|
|
111
|
+
expect(content).toContain('Task 2');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('parseTasksFromPlan', () => {
|
|
116
|
+
it('parses task format', () => {
|
|
117
|
+
const content = `
|
|
118
|
+
### Task 1: First task [ ]
|
|
119
|
+
**Goal:** Do first thing
|
|
120
|
+
### Task 2: Second task [x@alice]
|
|
121
|
+
**Goal:** Do second thing
|
|
122
|
+
`;
|
|
123
|
+
const tasks = parseTasksFromPlan(content);
|
|
124
|
+
expect(tasks.length).toBe(2);
|
|
125
|
+
expect(tasks[0].subject).toBe('First task');
|
|
126
|
+
expect(tasks[1].owner).toBe('alice');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('handles in-progress marker', () => {
|
|
130
|
+
const content = '### Task 1: Working [>@bob]';
|
|
131
|
+
const tasks = parseTasksFromPlan(content);
|
|
132
|
+
expect(tasks[0].status).toBe('in_progress');
|
|
133
|
+
expect(tasks[0].owner).toBe('bob');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('formatTaskForApi', () => {
|
|
138
|
+
it('formats task with all fields', () => {
|
|
139
|
+
const task = {
|
|
140
|
+
number: 1,
|
|
141
|
+
subject: 'Test',
|
|
142
|
+
status: 'pending',
|
|
143
|
+
goal: 'Do something',
|
|
144
|
+
owner: null
|
|
145
|
+
};
|
|
146
|
+
const formatted = formatTaskForApi(task, 5);
|
|
147
|
+
expect(formatted.id).toBe('phase-5-task-1');
|
|
148
|
+
expect(formatted.phase).toBe(5);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('createTasksApi', () => {
|
|
153
|
+
it('creates API handlers', () => {
|
|
154
|
+
const api = createTasksApi({ basePath: '/test' });
|
|
155
|
+
expect(api.get).toBeDefined();
|
|
156
|
+
expect(api.post).toBeDefined();
|
|
157
|
+
expect(api.patch).toBeDefined();
|
|
158
|
+
expect(api.delete).toBeDefined();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLC Introspection Module
|
|
3
|
+
* Reads TLC project state (ROADMAP.md, PROJECT.md, .tlc.json)
|
|
4
|
+
*/
|
|
5
|
+
import { promises as defaultFs } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse phases from ROADMAP.md content
|
|
10
|
+
* @param {string} content - ROADMAP.md content
|
|
11
|
+
* @returns {Array} Parsed phases
|
|
12
|
+
*/
|
|
13
|
+
export function parseRoadmap(content) {
|
|
14
|
+
const phases = [];
|
|
15
|
+
let currentMilestone = null;
|
|
16
|
+
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
// Check for milestone
|
|
21
|
+
const milestoneMatch = line.match(/^##\s+Milestone:\s*(.+)$/);
|
|
22
|
+
if (milestoneMatch) {
|
|
23
|
+
currentMilestone = milestoneMatch[1].trim();
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check for phase
|
|
28
|
+
const phaseMatch = line.match(/^###\s+Phase\s+(\d+):\s*(.+?)\s*\[(x|>|\s*)\]/);
|
|
29
|
+
if (phaseMatch) {
|
|
30
|
+
const number = parseInt(phaseMatch[1], 10);
|
|
31
|
+
const name = phaseMatch[2].trim();
|
|
32
|
+
const marker = phaseMatch[3];
|
|
33
|
+
|
|
34
|
+
let status;
|
|
35
|
+
if (marker === 'x') {
|
|
36
|
+
status = 'complete';
|
|
37
|
+
} else if (marker === '>') {
|
|
38
|
+
status = 'current';
|
|
39
|
+
} else {
|
|
40
|
+
status = 'pending';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
phases.push({
|
|
44
|
+
number,
|
|
45
|
+
name,
|
|
46
|
+
status,
|
|
47
|
+
milestone: currentMilestone
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return phases;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse PROJECT.md content
|
|
57
|
+
* @param {string} content - PROJECT.md content
|
|
58
|
+
* @returns {Object} Parsed project info
|
|
59
|
+
*/
|
|
60
|
+
export function parseProjectMd(content) {
|
|
61
|
+
if (!content || !content.trim()) {
|
|
62
|
+
return { name: 'Untitled', description: '' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lines = content.split('\n');
|
|
66
|
+
let name = 'Untitled';
|
|
67
|
+
let description = '';
|
|
68
|
+
|
|
69
|
+
// Extract name from first H1
|
|
70
|
+
const nameMatch = content.match(/^#\s+(.+)$/m);
|
|
71
|
+
if (nameMatch) {
|
|
72
|
+
name = nameMatch[1].trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Extract description (content between first H1 and next heading)
|
|
76
|
+
let foundName = false;
|
|
77
|
+
let descLines = [];
|
|
78
|
+
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (line.match(/^#\s+/)) {
|
|
81
|
+
if (foundName) break;
|
|
82
|
+
foundName = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (foundName && line.match(/^##/)) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (foundName && line.trim()) {
|
|
89
|
+
descLines.push(line);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
description = descLines.join('\n').trim();
|
|
94
|
+
|
|
95
|
+
return { name, description };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse .tlc.json config
|
|
100
|
+
* @param {string} content - JSON string
|
|
101
|
+
* @returns {Object} Parsed config
|
|
102
|
+
*/
|
|
103
|
+
export function parseTlcConfig(content) {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(content);
|
|
106
|
+
} catch {
|
|
107
|
+
return { project: 'unknown' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get complete project state
|
|
113
|
+
* @param {Object} options - Options with fs and basePath
|
|
114
|
+
* @returns {Promise<Object>} Project state
|
|
115
|
+
*/
|
|
116
|
+
export async function getProjectState(options = {}) {
|
|
117
|
+
const fs = options.fs || defaultFs;
|
|
118
|
+
const basePath = options.basePath || process.cwd();
|
|
119
|
+
|
|
120
|
+
let projectContent = '';
|
|
121
|
+
let roadmapContent = '';
|
|
122
|
+
let configContent = '';
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
projectContent = await fs.readFile(path.join(basePath, 'PROJECT.md'), 'utf-8');
|
|
126
|
+
} catch {
|
|
127
|
+
projectContent = '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
roadmapContent = await fs.readFile(path.join(basePath, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
132
|
+
} catch {
|
|
133
|
+
roadmapContent = '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
configContent = await fs.readFile(path.join(basePath, '.tlc.json'), 'utf-8');
|
|
138
|
+
} catch {
|
|
139
|
+
configContent = '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
project: parseProjectMd(projectContent),
|
|
144
|
+
phases: parseRoadmap(roadmapContent),
|
|
145
|
+
config: parseTlcConfig(configContent)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get current phase from phases array
|
|
151
|
+
* @param {Array} phases - Array of phases
|
|
152
|
+
* @returns {Object|null} Current phase
|
|
153
|
+
*/
|
|
154
|
+
export function getCurrentPhase(phases) {
|
|
155
|
+
// First look for explicitly current phase
|
|
156
|
+
const current = phases.find(p => p.status === 'current');
|
|
157
|
+
if (current) return current;
|
|
158
|
+
|
|
159
|
+
// Otherwise return first pending phase
|
|
160
|
+
const pending = phases.find(p => p.status === 'pending');
|
|
161
|
+
return pending || null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create TLC introspection instance
|
|
166
|
+
* @param {Object} options - Options
|
|
167
|
+
* @returns {Object} Introspection API
|
|
168
|
+
*/
|
|
169
|
+
export function createTlcIntrospection(options = {}) {
|
|
170
|
+
const fs = options.fs || defaultFs;
|
|
171
|
+
const basePath = options.basePath || process.cwd();
|
|
172
|
+
|
|
173
|
+
let cachedState = null;
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
async getState() {
|
|
177
|
+
if (!cachedState) {
|
|
178
|
+
cachedState = await getProjectState({ fs, basePath });
|
|
179
|
+
}
|
|
180
|
+
return cachedState;
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
async getPhases() {
|
|
184
|
+
const state = await this.getState();
|
|
185
|
+
return state.phases;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
async getCurrentPhase() {
|
|
189
|
+
const phases = await this.getPhases();
|
|
190
|
+
return getCurrentPhase(phases);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
invalidateCache() {
|
|
194
|
+
cachedState = null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|