plankit-cli 1.4.0 → 1.6.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/README.md +73 -0
- package/package.json +1 -1
- package/src/analyzer/dotnetScanner.js +111 -0
- package/src/analyzer/engine.js +290 -0
- package/src/analyzer/vueScanner.js +200 -0
- package/src/cli.js +116 -0
- package/src/dashboard/dashboard.js +272 -0
- package/src/server/clientHtml.js +674 -0
- package/src/server/server.js +155 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { URL } from 'node:url';
|
|
5
|
+
import { loadDashboardData } from '../dashboard/dashboard.js';
|
|
6
|
+
import { runAnalysis } from '../analyzer/engine.js';
|
|
7
|
+
import { getClientHtml } from './clientHtml.js';
|
|
8
|
+
import { safeJoin } from '../config.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Native, zero-dependency HTTP server for PlanKit Web Dashboard.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function createWebServer({ cwd, config, io }) {
|
|
15
|
+
const server = http.createServer(async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
18
|
+
const pathname = parsedUrl.pathname;
|
|
19
|
+
|
|
20
|
+
// CORS headers for local development
|
|
21
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
22
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
23
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
24
|
+
|
|
25
|
+
if (req.method === 'OPTIONS') {
|
|
26
|
+
res.writeHead(204);
|
|
27
|
+
res.end();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Root HTML page
|
|
32
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
33
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
34
|
+
res.end(getClientHtml());
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// API: Combined data (active features, archived count, and latest health scan)
|
|
39
|
+
if (pathname === '/api/data' && req.method === 'GET') {
|
|
40
|
+
const dashboardData = loadDashboardData(cwd, config);
|
|
41
|
+
|
|
42
|
+
let healthReport = null;
|
|
43
|
+
const indexPath = path.join(cwd, config.artifactsDir || 'artifacts', '.plankit-index.json');
|
|
44
|
+
if (fs.existsSync(indexPath)) {
|
|
45
|
+
try {
|
|
46
|
+
healthReport = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
47
|
+
} catch {
|
|
48
|
+
healthReport = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If no index exists yet, run a fast scan
|
|
53
|
+
if (!healthReport) {
|
|
54
|
+
healthReport = await runAnalysis(cwd, { artifactsDir: config.artifactsDir });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
58
|
+
res.end(JSON.stringify({
|
|
59
|
+
activeFeatures: dashboardData.activeFeatures,
|
|
60
|
+
archivedCount: dashboardData.archivedCount,
|
|
61
|
+
healthReport
|
|
62
|
+
}));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// API: Trigger live re-scan
|
|
67
|
+
if (pathname === '/api/scan' && req.method === 'POST') {
|
|
68
|
+
const report = await runAnalysis(cwd, { artifactsDir: config.artifactsDir });
|
|
69
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
70
|
+
res.end(JSON.stringify(report));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// API: View Phase Spec Markdown
|
|
75
|
+
if (pathname === '/api/spec' && req.method === 'GET') {
|
|
76
|
+
const feature = parsedUrl.searchParams.get('feature');
|
|
77
|
+
const phase = parsedUrl.searchParams.get('phase');
|
|
78
|
+
if (!feature || !phase) {
|
|
79
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
80
|
+
res.end(JSON.stringify({ error: 'Missing feature or phase parameter' }));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const phasesDir = safeJoin(cwd, config.artifactsDir || 'artifacts', 'current', feature, 'phases');
|
|
85
|
+
const prefix = `phase-${phase}-`;
|
|
86
|
+
const file = fs.existsSync(phasesDir)
|
|
87
|
+
? fs.readdirSync(phasesDir).find((n) => n.startsWith(prefix) && n.endsWith('.md'))
|
|
88
|
+
: null;
|
|
89
|
+
|
|
90
|
+
if (!file) {
|
|
91
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
92
|
+
res.end(JSON.stringify({ error: `Spec for Phase ${phase} not found` }));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const content = fs.readFileSync(path.join(phasesDir, file), 'utf8');
|
|
97
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
98
|
+
res.end(JSON.stringify({ file, content }));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// API: View Phase Output Report Markdown
|
|
103
|
+
if (pathname === '/api/output' && req.method === 'GET') {
|
|
104
|
+
const feature = parsedUrl.searchParams.get('feature');
|
|
105
|
+
const phase = parsedUrl.searchParams.get('phase');
|
|
106
|
+
if (!feature || !phase) {
|
|
107
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
108
|
+
res.end(JSON.stringify({ error: 'Missing feature or phase parameter' }));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const outputPath = safeJoin(cwd, config.artifactsDir || 'artifacts', 'current', feature, 'outputs', `phase-${phase}-output.md`);
|
|
113
|
+
if (!fs.existsSync(outputPath)) {
|
|
114
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
115
|
+
res.end(JSON.stringify({ error: `Output report for Phase ${phase} not found` }));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const content = fs.readFileSync(outputPath, 'utf8');
|
|
120
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
121
|
+
res.end(JSON.stringify({ file: path.basename(outputPath), content }));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 404
|
|
126
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
127
|
+
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
130
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return server;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function startServer({ cwd, config, port = 4200, io }) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const server = createWebServer({ cwd, config, io });
|
|
140
|
+
|
|
141
|
+
server.once('error', (err) => {
|
|
142
|
+
if (err.code === 'EADDRINUSE') {
|
|
143
|
+
reject(new Error(`Port ${port} is already in use. Try passing a different port with --port <number>.`));
|
|
144
|
+
} else {
|
|
145
|
+
reject(err);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
server.listen(port, () => {
|
|
150
|
+
const address = server.address();
|
|
151
|
+
const actualPort = typeof address === 'object' ? address.port : port;
|
|
152
|
+
resolve({ server, port: actualPort });
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|