thachvd-kit 1.0.37 → 1.0.39
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/LICENSE +21 -21
- package/README.md +303 -11
- package/THIRD_PARTY_NOTICES.md +49 -49
- package/bin/cli.js +1791 -1785
- package/bin/config.js +164 -0
- package/bin/entry.js +11 -1
- package/bin/native-skills.js +183 -149
- package/bin/spec-doctor.js +252 -0
- package/bin/spec-link.js +97 -0
- package/bin/spec-recipe.js +74 -0
- package/bin/spec-site.js +639 -0
- package/bin/spec-state.js +415 -0
- package/bin/spec.js +901 -0
- package/bin/upgrade.js +303 -303
- package/package.json +3 -3
- package/skills/finishing-a-development-branch/SKILL.md +240 -240
- package/skills/requesting-code-review/code-reviewer.md +198 -198
- package/skills/subagent-driven-development/SKILL.md +574 -574
- package/skills/subagent-driven-development/implementer-prompt.md +154 -154
- package/skills/subagent-driven-development/re-review-prompt.md +115 -115
- package/skills/subagent-driven-development/scripts/review-package +53 -53
- package/skills/subagent-driven-development/scripts/review-package.js +52 -52
- package/skills/subagent-driven-development/scripts/sdd-workspace +82 -82
- package/skills/subagent-driven-development/scripts/sdd-workspace-lib.js +62 -62
- package/skills/subagent-driven-development/scripts/sdd-workspace.js +15 -15
- package/skills/subagent-driven-development/scripts/task-brief +43 -43
- package/skills/subagent-driven-development/scripts/task-brief.js +46 -46
- package/skills/subagent-driven-development/task-reviewer-prompt.md +207 -207
- package/skills/system-discovery/SKILL.md +140 -0
- package/skills/system-reverse-engineer/SKILL.md +208 -0
- package/skills/system-spec-review/SKILL.md +177 -0
- package/skills/upstream.json +30 -30
- package/skills/using-git-worktrees/SKILL.md +175 -175
package/bin/spec-site.js
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const specState = require('./spec-state');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SITE_DIR = '_site';
|
|
7
|
+
const DEFAULT_PORT = 4173;
|
|
8
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
9
|
+
const TICK = String.fromCharCode(96);
|
|
10
|
+
const FENCE = TICK + TICK + TICK;
|
|
11
|
+
|
|
12
|
+
function portable(value) {
|
|
13
|
+
return value.split(path.sep).join('/');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function escapeHtml(value) {
|
|
17
|
+
return String(value == null ? '' : value)
|
|
18
|
+
.replace(/&/g, '&')
|
|
19
|
+
.replace(/</g, '<')
|
|
20
|
+
.replace(/>/g, '>')
|
|
21
|
+
.replace(/"/g, '"')
|
|
22
|
+
.replace(/'/g, ''');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function inlineMarkdown(value) {
|
|
26
|
+
let text = escapeHtml(value);
|
|
27
|
+
const parts = text.split(TICK);
|
|
28
|
+
if (parts.length > 1) {
|
|
29
|
+
text = parts.map(function(part, index) {
|
|
30
|
+
return index % 2 ? '<code>' + part + '</code>' : part;
|
|
31
|
+
}).join('');
|
|
32
|
+
}
|
|
33
|
+
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
34
|
+
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
35
|
+
return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(_m, label, href) {
|
|
36
|
+
const raw = String(href || '').trim();
|
|
37
|
+
const safe = /^(https?:|mailto:|#|\.{0,2}\/)/i.test(raw) ? escapeHtml(raw) : '#';
|
|
38
|
+
return '<a href="' + safe + '">' + label + '</a>';
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isTableSeparator(line) {
|
|
43
|
+
return /^\s*\|?[\s:|-]+\|[\s:|-|]*\|?\s*$/.test(line || '') && String(line || '').includes('-');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function splitTableRow(line) {
|
|
47
|
+
return String(line || '').trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(function(cell) {
|
|
48
|
+
return cell.trim();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function markdownToHtml(markdown) {
|
|
53
|
+
const lines = String(markdown || '').replace(/\r\n/g, '\n').split('\n');
|
|
54
|
+
const out = [];
|
|
55
|
+
let paragraph = [];
|
|
56
|
+
let list = null;
|
|
57
|
+
let items = [];
|
|
58
|
+
let code = null;
|
|
59
|
+
let language = '';
|
|
60
|
+
|
|
61
|
+
function flushParagraph() {
|
|
62
|
+
if (!paragraph.length) return;
|
|
63
|
+
out.push('<p>' + inlineMarkdown(paragraph.join(' ')) + '</p>');
|
|
64
|
+
paragraph = [];
|
|
65
|
+
}
|
|
66
|
+
function flushList() {
|
|
67
|
+
if (!list) return;
|
|
68
|
+
out.push('<' + list + '>' + items.map(function(item) {
|
|
69
|
+
return '<li>' + inlineMarkdown(item) + '</li>';
|
|
70
|
+
}).join('') + '</' + list + '>');
|
|
71
|
+
list = null;
|
|
72
|
+
items = [];
|
|
73
|
+
}
|
|
74
|
+
function flush() {
|
|
75
|
+
flushParagraph();
|
|
76
|
+
flushList();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
const line = lines[i];
|
|
81
|
+
if (code !== null) {
|
|
82
|
+
if (line.indexOf(FENCE) === 0) {
|
|
83
|
+
const body = escapeHtml(code.join('\n'));
|
|
84
|
+
out.push(language.toLowerCase() === 'mermaid'
|
|
85
|
+
? '<pre class="mermaid">' + body + '</pre>'
|
|
86
|
+
: '<pre><code' + (language ? ' class="language-' + escapeHtml(language) + '"' : '') + '>' + body + '</code></pre>');
|
|
87
|
+
code = null;
|
|
88
|
+
language = '';
|
|
89
|
+
} else {
|
|
90
|
+
code.push(line);
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (line.indexOf(FENCE) === 0) {
|
|
96
|
+
flush();
|
|
97
|
+
code = [];
|
|
98
|
+
language = line.slice(FENCE.length).trim();
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
103
|
+
if (heading) {
|
|
104
|
+
flush();
|
|
105
|
+
const level = heading[1].length;
|
|
106
|
+
const title = heading[2].trim();
|
|
107
|
+
out.push('<h' + level + '>' + inlineMarkdown(title) + '</h' + level + '>');
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (line.trim() === '---') {
|
|
111
|
+
flush();
|
|
112
|
+
out.push('<hr>');
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (line.indexOf('> ') === 0) {
|
|
116
|
+
flush();
|
|
117
|
+
out.push('<blockquote>' + inlineMarkdown(line.slice(2)) + '</blockquote>');
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (line.includes('|') && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
|
|
122
|
+
flush();
|
|
123
|
+
const headers = splitTableRow(line);
|
|
124
|
+
i += 1;
|
|
125
|
+
const rows = [];
|
|
126
|
+
while (i + 1 < lines.length && lines[i + 1].includes('|') && lines[i + 1].trim()) {
|
|
127
|
+
rows.push(splitTableRow(lines[++i]));
|
|
128
|
+
}
|
|
129
|
+
out.push('<div class="table-wrap"><table><thead><tr>' +
|
|
130
|
+
headers.map(function(cell) { return '<th>' + inlineMarkdown(cell) + '</th>'; }).join('') +
|
|
131
|
+
'</tr></thead><tbody>' +
|
|
132
|
+
rows.map(function(row) {
|
|
133
|
+
return '<tr>' + headers.map(function(_header, index) {
|
|
134
|
+
return '<td>' + inlineMarkdown(row[index] || '') + '</td>';
|
|
135
|
+
}).join('') + '</tr>';
|
|
136
|
+
}).join('') +
|
|
137
|
+
'</tbody></table></div>');
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const unordered = line.match(/^\s*[-*+]\s+(.+)$/);
|
|
142
|
+
const ordered = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
143
|
+
if (unordered || ordered) {
|
|
144
|
+
flushParagraph();
|
|
145
|
+
const type = ordered ? 'ol' : 'ul';
|
|
146
|
+
if (list && list !== type) flushList();
|
|
147
|
+
list = type;
|
|
148
|
+
items.push((ordered || unordered)[1]);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!line.trim()) {
|
|
153
|
+
flush();
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
paragraph.push(line.trim());
|
|
157
|
+
}
|
|
158
|
+
if (code !== null) out.push('<pre><code>' + escapeHtml(code.join('\n')) + '</code></pre>');
|
|
159
|
+
flush();
|
|
160
|
+
return out.join('\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readText(filePath) {
|
|
164
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readJson(filePath) {
|
|
168
|
+
if (!fs.existsSync(filePath)) return null;
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return { __error: error.message };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function firstHeading(markdown, fallback) {
|
|
177
|
+
const match = String(markdown || '').match(/^#\s+(.+)$/m);
|
|
178
|
+
return match ? match[1].trim() : fallback;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function listMarkdownFiles(directory) {
|
|
182
|
+
if (!fs.existsSync(directory)) return [];
|
|
183
|
+
return fs.readdirSync(directory, { withFileTypes: true })
|
|
184
|
+
.filter(function(entry) { return entry.isFile() && entry.name.toLowerCase().endsWith('.md'); })
|
|
185
|
+
.map(function(entry) { return path.join(directory, entry.name); })
|
|
186
|
+
.sort(function(a, b) { return path.basename(a).localeCompare(path.basename(b)); });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function safeCapabilitySlug(value) {
|
|
190
|
+
const slug = String(value || '');
|
|
191
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(slug)) {
|
|
192
|
+
throw new Error('unsafe capability slug for HTML output: ' + value);
|
|
193
|
+
}
|
|
194
|
+
return slug;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resolveOutputRoot(workspaceRoot, manifest, outputOption) {
|
|
198
|
+
const workspace = path.resolve(workspaceRoot);
|
|
199
|
+
const specRoot = path.resolve(workspace, manifest.spec_root);
|
|
200
|
+
const requested = outputOption ? path.resolve(workspace, outputOption) : path.join(specRoot, DEFAULT_SITE_DIR);
|
|
201
|
+
const relative = path.relative(workspace, requested);
|
|
202
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
203
|
+
throw new Error('site output must stay inside the workspace root');
|
|
204
|
+
}
|
|
205
|
+
if (requested === workspace || requested === specRoot) {
|
|
206
|
+
throw new Error('site output cannot replace the workspace root or spec root');
|
|
207
|
+
}
|
|
208
|
+
return requested;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildSystemModel(workspaceRoot, manifest, options) {
|
|
212
|
+
options = options || {};
|
|
213
|
+
const specRoot = path.resolve(workspaceRoot, manifest.spec_root);
|
|
214
|
+
const rawVerification = readJson(path.join(specRoot, '_meta', 'verification.json'));
|
|
215
|
+
const progress = readJson(path.join(specRoot, '_meta', 'reverse-progress.json'));
|
|
216
|
+
let verification = rawVerification;
|
|
217
|
+
let verificationError = rawVerification && rawVerification.__error ? rawVerification.__error : null;
|
|
218
|
+
if (verification && !verificationError) {
|
|
219
|
+
try {
|
|
220
|
+
specState.validateVerification(manifest, verification);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
verificationError = error.message;
|
|
223
|
+
verification = null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const review = readText(path.join(specRoot, '_meta', 'review.md'));
|
|
227
|
+
let drift = Object.prototype.hasOwnProperty.call(options, 'driftReport') ? options.driftReport : null;
|
|
228
|
+
let driftError = null;
|
|
229
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'driftReport') && verification && !verification.__error) {
|
|
230
|
+
try {
|
|
231
|
+
drift = specState.checkSpecDrift(workspaceRoot, manifest, verification);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
driftError = error.message;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const verificationCaps = verification && !verification.__error ? verification.capabilities || {} : {};
|
|
238
|
+
const progressCaps = progress && !progress.__error ? progress.capabilities || {} : {};
|
|
239
|
+
const capabilityRoot = path.join(specRoot, 'capabilities');
|
|
240
|
+
const slugs = new Set(Object.keys(verificationCaps).concat(Object.keys(progressCaps)));
|
|
241
|
+
if (fs.existsSync(capabilityRoot)) {
|
|
242
|
+
fs.readdirSync(capabilityRoot, { withFileTypes: true }).forEach(function(entry) {
|
|
243
|
+
if (entry.isDirectory()) slugs.add(entry.name);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const capabilities = Array.from(slugs).sort().map(function(rawSlug) {
|
|
248
|
+
const slug = safeCapabilitySlug(rawSlug);
|
|
249
|
+
const root = path.join(capabilityRoot, slug);
|
|
250
|
+
const prd = readText(path.join(root, 'prd.md'));
|
|
251
|
+
const design = readText(path.join(root, 'design.md'));
|
|
252
|
+
const verified = verificationCaps[slug] || null;
|
|
253
|
+
const progressed = progressCaps[slug] || null;
|
|
254
|
+
const driftState = drift && drift.capabilities ? drift.capabilities[slug] || null : null;
|
|
255
|
+
const sourcePaths = (verified && verified.source_paths) || (progressed && progressed.source_paths) || {};
|
|
256
|
+
let status = verified ? verified.status : 'unverified';
|
|
257
|
+
if (driftState && driftState.status === 'stale') status = 'stale';
|
|
258
|
+
else if (driftState && driftState.status === 'unknown') status = 'unknown';
|
|
259
|
+
return {
|
|
260
|
+
slug: slug,
|
|
261
|
+
title: (verified && verified.title) || (progressed && progressed.title) || firstHeading(prd, firstHeading(design, slug)),
|
|
262
|
+
verification_status: verified ? verified.status : 'unverified',
|
|
263
|
+
drift_status: driftState ? driftState.status : 'unknown',
|
|
264
|
+
status: status,
|
|
265
|
+
repos: Object.keys(sourcePaths).sort(),
|
|
266
|
+
source_paths: sourcePaths,
|
|
267
|
+
evidence: verified ? verified.evidence || {} : {},
|
|
268
|
+
prd_html: markdownToHtml(prd || ''),
|
|
269
|
+
design_html: markdownToHtml(design || '')
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
function docs(name) {
|
|
274
|
+
return listMarkdownFiles(path.join(specRoot, name)).map(function(filePath) {
|
|
275
|
+
const markdown = readText(filePath) || '';
|
|
276
|
+
return {
|
|
277
|
+
name: path.basename(filePath, '.md'),
|
|
278
|
+
title: firstHeading(markdown, path.basename(filePath, '.md')),
|
|
279
|
+
html: markdownToHtml(markdown)
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
generated_at: new Date().toISOString(),
|
|
286
|
+
system: { name: manifest.name, language: manifest.language, profile: manifest.profile, spec_root: manifest.spec_root },
|
|
287
|
+
repositories: manifest.repos.map(function(repo) { return { name: repo.name, path: repo.path }; }),
|
|
288
|
+
capabilities: capabilities,
|
|
289
|
+
architecture: docs('architecture'),
|
|
290
|
+
flows: docs('flows'),
|
|
291
|
+
contracts: docs('contracts'),
|
|
292
|
+
review_html: markdownToHtml(review || ''),
|
|
293
|
+
verification: verification && !verification.__error ? {
|
|
294
|
+
verified_at: verification.verified_at || null,
|
|
295
|
+
repositories: verification.repositories || {}
|
|
296
|
+
} : null,
|
|
297
|
+
verification_error: verificationError,
|
|
298
|
+
progress_error: progress && progress.__error ? progress.__error : null,
|
|
299
|
+
drift_error: driftError,
|
|
300
|
+
counts: {
|
|
301
|
+
repos: manifest.repos.length,
|
|
302
|
+
capabilities: capabilities.length,
|
|
303
|
+
verified: capabilities.filter(function(item) { return item.verification_status === 'verified'; }).length,
|
|
304
|
+
stale: capabilities.filter(function(item) { return item.drift_status === 'stale'; }).length,
|
|
305
|
+
needs_review: capabilities.filter(function(item) { return item.verification_status === 'needs_review'; }).length,
|
|
306
|
+
blocked: capabilities.filter(function(item) { return item.verification_status === 'blocked'; }).length,
|
|
307
|
+
unverified: capabilities.filter(function(item) { return item.verification_status === 'unverified'; }).length
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function copy(model) {
|
|
313
|
+
if (model.system.language === 'vi') {
|
|
314
|
+
return {
|
|
315
|
+
overview: 'Tổng quan',
|
|
316
|
+
architecture: 'Kiến trúc',
|
|
317
|
+
capabilities: 'Capability',
|
|
318
|
+
flows: 'Luồng',
|
|
319
|
+
contracts: 'Contract',
|
|
320
|
+
health: 'Sức khỏe spec',
|
|
321
|
+
repositories: 'Repository',
|
|
322
|
+
verified: 'Đã xác minh',
|
|
323
|
+
stale: 'Đã lệch',
|
|
324
|
+
needsReview: 'Cần review',
|
|
325
|
+
blocked: 'Bị chặn',
|
|
326
|
+
unverified: 'Chưa xác minh',
|
|
327
|
+
capabilityHealth: 'Tình trạng capability',
|
|
328
|
+
viewAll: 'Xem tất cả',
|
|
329
|
+
latestReview: 'Review gần nhất',
|
|
330
|
+
noMapping: 'Chưa có mapping repository',
|
|
331
|
+
details: 'Chi tiết capability',
|
|
332
|
+
sourceMap: 'Source map',
|
|
333
|
+
evidence: 'Evidence',
|
|
334
|
+
drift: 'Drift',
|
|
335
|
+
filter: 'Lọc capability hoặc repository…',
|
|
336
|
+
systemExplorer: 'BẢN ĐỒ HỆ THỐNG AS-IS',
|
|
337
|
+
explorerDescription: 'Được tạo từ Markdown spec và metadata xác minh. HTML chỉ là lớp hiển thị, không phải source of truth.',
|
|
338
|
+
verification: 'Xác minh',
|
|
339
|
+
capabilityStatus: 'Trạng thái capability',
|
|
340
|
+
noCapabilities: 'Chưa có capability spec.'
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
overview: 'Overview',
|
|
345
|
+
architecture: 'Architecture',
|
|
346
|
+
capabilities: 'Capabilities',
|
|
347
|
+
flows: 'Flows',
|
|
348
|
+
contracts: 'Contracts',
|
|
349
|
+
health: 'Spec health',
|
|
350
|
+
repositories: 'Repositories',
|
|
351
|
+
verified: 'Verified',
|
|
352
|
+
stale: 'Stale',
|
|
353
|
+
needsReview: 'Needs review',
|
|
354
|
+
blocked: 'Blocked',
|
|
355
|
+
unverified: 'Unverified',
|
|
356
|
+
capabilityHealth: 'Capability health',
|
|
357
|
+
viewAll: 'View all',
|
|
358
|
+
latestReview: 'Latest review',
|
|
359
|
+
noMapping: 'No repository mapping',
|
|
360
|
+
details: 'Capability details',
|
|
361
|
+
sourceMap: 'Source map',
|
|
362
|
+
evidence: 'Evidence',
|
|
363
|
+
drift: 'Drift',
|
|
364
|
+
filter: 'Filter capabilities or repositories…',
|
|
365
|
+
systemExplorer: 'AS-IS SYSTEM EXPLORER',
|
|
366
|
+
explorerDescription: 'Generated from Markdown specs and verification metadata. HTML is a read-only presentation layer.',
|
|
367
|
+
verification: 'Verification',
|
|
368
|
+
capabilityStatus: 'Capability status',
|
|
369
|
+
noCapabilities: 'No capability specs yet.'
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function badge(status) {
|
|
374
|
+
const tone = ['verified', 'current'].includes(status) ? 'ok' :
|
|
375
|
+
['stale', 'needs_review', 'unknown'].includes(status) ? 'warn' :
|
|
376
|
+
status === 'blocked' ? 'bad' : 'neutral';
|
|
377
|
+
return '<span class="badge ' + tone + '">' + escapeHtml(String(status || 'unknown').replace(/_/g, ' ')) + '</span>';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function shell(title, model, active, body, depth) {
|
|
381
|
+
depth = depth || 0;
|
|
382
|
+
const prefix = depth ? '../'.repeat(depth) : '';
|
|
383
|
+
const text = copy(model);
|
|
384
|
+
const links = [
|
|
385
|
+
['index.html', text.overview, 'overview'],
|
|
386
|
+
['architecture.html', text.architecture, 'architecture'],
|
|
387
|
+
['capabilities.html', text.capabilities, 'capabilities'],
|
|
388
|
+
['flows.html', text.flows, 'flows'],
|
|
389
|
+
['contracts.html', text.contracts, 'contracts'],
|
|
390
|
+
['health.html', text.health, 'health']
|
|
391
|
+
];
|
|
392
|
+
const nav = links.map(function(item) {
|
|
393
|
+
return '<a class="' + (active === item[2] ? 'active' : '') + '" href="' + prefix + item[0] + '">' + item[1] + '</a>';
|
|
394
|
+
}).join('');
|
|
395
|
+
return '<!doctype html><html lang="' + escapeHtml(model.system.language || 'en') + '"><head>' +
|
|
396
|
+
'<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">' +
|
|
397
|
+
'<title>' + escapeHtml(title) + ' · ' + escapeHtml(model.system.name) + '</title>' +
|
|
398
|
+
'<link rel="stylesheet" href="' + prefix + 'assets/site.css"><script defer src="' + prefix + 'assets/app.js"></script></head><body>' +
|
|
399
|
+
'<header class="topbar"><a class="brand" href="' + prefix + 'index.html">' + escapeHtml(model.system.name) + '</a>' +
|
|
400
|
+
'<span class="top-meta">' + escapeHtml(model.system.profile) + ' · ' + escapeHtml(model.generated_at.slice(0, 19).replace('T', ' ')) + '</span></header>' +
|
|
401
|
+
'<div class="layout"><nav class="sidebar">' + nav + '</nav><main class="content">' + body + '</main></div></body></html>';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function metric(label, value, tone) {
|
|
405
|
+
return '<div class="metric ' + (tone || '') + '"><div class="metric-value">' + escapeHtml(value) + '</div><div class="metric-label">' + escapeHtml(label) + '</div></div>';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function renderOverview(model) {
|
|
409
|
+
const text = copy(model);
|
|
410
|
+
const repos = model.repositories.map(function(repo) {
|
|
411
|
+
return '<div class="repo-card"><strong>' + escapeHtml(repo.name) + '</strong><code>' + escapeHtml(repo.path) + '</code></div>';
|
|
412
|
+
}).join('');
|
|
413
|
+
const caps = model.capabilities.slice(0, 12).map(function(cap) {
|
|
414
|
+
return '<a class="cap-row" href="capability/' + encodeURIComponent(cap.slug) + '.html"><span><strong>' +
|
|
415
|
+
escapeHtml(cap.title) + '</strong><small>' + escapeHtml(cap.repos.join(', ') || text.noMapping) +
|
|
416
|
+
'</small></span>' + badge(cap.status) + '</a>';
|
|
417
|
+
}).join('') || '<p class="muted">' + text.noCapabilities + '</p>';
|
|
418
|
+
return shell('Overview', model, 'overview',
|
|
419
|
+
'<section class="hero"><p class="eyebrow">' + text.systemExplorer + '</p><h1>' + escapeHtml(model.system.name) +
|
|
420
|
+
'</h1><p>' + text.explorerDescription + '</p></section>' +
|
|
421
|
+
'<section class="metrics">' + metric(text.repositories, model.counts.repos) + metric(text.capabilities, model.counts.capabilities) +
|
|
422
|
+
metric(text.verified, model.counts.verified, 'good') + metric(text.stale, model.counts.stale, model.counts.stale ? 'warning' : '') +
|
|
423
|
+
metric(text.needsReview, model.counts.needs_review) + metric(text.blocked, model.counts.blocked) + '</section>' +
|
|
424
|
+
'<section><div class="section-head"><h2>' + text.repositories + '</h2><a href="architecture.html">' + text.architecture + ' →</a></div><div class="repo-grid">' + repos + '</div></section>' +
|
|
425
|
+
'<section><div class="section-head"><h2>' + text.capabilityHealth + '</h2><a href="capabilities.html">' + text.viewAll + ' →</a></div><div class="cap-list">' + caps + '</div></section>' +
|
|
426
|
+
(model.review_html ? '<section><h2>' + text.latestReview + '</h2><div class="doc">' + model.review_html + '</div></section>' : ''));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function renderDocs(title, documents, model, active) {
|
|
430
|
+
const content = documents.map(function(doc) {
|
|
431
|
+
return '<article class="doc-card"><div class="doc-title"><h2>' + escapeHtml(doc.title) + '</h2><span>' +
|
|
432
|
+
escapeHtml(doc.name) + '.md</span></div><div class="doc">' + doc.html + '</div></article>';
|
|
433
|
+
}).join('') || '<p class="muted">No ' + escapeHtml(title.toLowerCase()) + ' documents yet.</p>';
|
|
434
|
+
return shell(title, model, active, '<h1>' + escapeHtml(title) + '</h1>' + content);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function renderMatrix(model) {
|
|
438
|
+
if (!model.capabilities.length || !model.repositories.length) return '<p class="muted">No capability mapping available.</p>';
|
|
439
|
+
const header = model.repositories.map(function(repo) { return '<th>' + escapeHtml(repo.name) + '</th>'; }).join('');
|
|
440
|
+
const rows = model.capabilities.map(function(cap) {
|
|
441
|
+
const cells = model.repositories.map(function(repo) {
|
|
442
|
+
return '<td class="matrix-cell">' + (cap.repos.includes(repo.name) ? '<span class="dot">●</span>' : '') + '</td>';
|
|
443
|
+
}).join('');
|
|
444
|
+
return '<tr><th><a href="capability/' + encodeURIComponent(cap.slug) + '.html">' + escapeHtml(cap.title) +
|
|
445
|
+
'</a><br>' + badge(cap.status) + '</th>' + cells + '</tr>';
|
|
446
|
+
}).join('');
|
|
447
|
+
return '<div class="table-wrap"><table class="matrix"><thead><tr><th>Capability</th>' + header + '</tr></thead><tbody>' + rows + '</tbody></table></div>';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function renderCapabilities(model) {
|
|
451
|
+
const text = copy(model);
|
|
452
|
+
const cards = model.capabilities.map(function(cap) {
|
|
453
|
+
const haystack = (cap.title + ' ' + cap.slug + ' ' + cap.repos.join(' ')).toLowerCase();
|
|
454
|
+
return '<a class="cap-card" data-capability="' + escapeHtml(haystack) + '" href="capability/' + encodeURIComponent(cap.slug) +
|
|
455
|
+
'.html"><div><h3>' + escapeHtml(cap.title) + '</h3><p>' + escapeHtml(cap.repos.join(' · ') || text.noMapping) +
|
|
456
|
+
'</p></div><div>' + badge(cap.status) + '</div></a>';
|
|
457
|
+
}).join('') || '<p class="muted">No capability specs yet.</p>';
|
|
458
|
+
return shell(text.capabilities, model, 'capabilities',
|
|
459
|
+
'<h1>' + text.capabilities + '</h1><p class="muted">Business capabilities mapped back to repositories and evidence.</p>' +
|
|
460
|
+
'<input id="capability-search" class="search" type="search" placeholder="' + escapeHtml(text.filter) + '">' +
|
|
461
|
+
renderMatrix(model) + '<h2>' + text.details + '</h2><div id="capability-list" class="cap-grid">' + cards + '</div>');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function renderCapability(model, cap) {
|
|
465
|
+
const text = copy(model);
|
|
466
|
+
const sources = Object.entries(cap.source_paths || {}).map(function(entry) {
|
|
467
|
+
return '<div class="source-group"><h3>' + escapeHtml(entry[0]) + '</h3><ul>' +
|
|
468
|
+
entry[1].map(function(p) { return '<li><code>' + escapeHtml(p) + '</code></li>'; }).join('') + '</ul></div>';
|
|
469
|
+
}).join('') || '<p class="muted">No source map recorded.</p>';
|
|
470
|
+
const evidence = Object.entries(cap.evidence || {}).map(function(entry) {
|
|
471
|
+
return '<tr><th>' + escapeHtml(entry[0].replace(/_/g, ' ')) + '</th><td>' + escapeHtml(entry[1]) + '</td></tr>';
|
|
472
|
+
}).join('');
|
|
473
|
+
return shell(cap.title, model, 'capabilities',
|
|
474
|
+
'<div class="breadcrumbs"><a href="../capabilities.html">Capabilities</a> / ' + escapeHtml(cap.title) + '</div>' +
|
|
475
|
+
'<div class="detail-head"><div><h1>' + escapeHtml(cap.title) + '</h1><p class="muted">' + escapeHtml(cap.slug) +
|
|
476
|
+
'</p></div><div>' + badge(cap.status) + ' ' + badge(cap.verification_status) + '</div></div>' +
|
|
477
|
+
'<section class="two-col"><div><h2>' + text.repositories + '</h2><p>' + escapeHtml(cap.repos.join(' · ') || text.noMapping) +
|
|
478
|
+
'</p></div><div><h2>' + text.drift + '</h2><p>' + badge(cap.drift_status) + '</p></div></section>' +
|
|
479
|
+
'<section><h2>' + text.sourceMap + '</h2><div class="source-grid">' + sources + '</div></section>' +
|
|
480
|
+
(evidence ? '<section><h2>' + text.evidence + '</h2><div class="table-wrap"><table><tbody>' + evidence + '</tbody></table></div></section>' : '') +
|
|
481
|
+
'<section><h2>PRD</h2><div class="doc">' + (cap.prd_html || '<p class="muted">prd.md not found.</p>') + '</div></section>' +
|
|
482
|
+
'<section><h2>Design</h2><div class="doc">' + (cap.design_html || '<p class="muted">design.md not found.</p>') + '</div></section>', 1);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function renderHealth(model) {
|
|
486
|
+
const text = copy(model);
|
|
487
|
+
const caps = model.capabilities.map(function(cap) {
|
|
488
|
+
return '<tr><th><a href="capability/' + encodeURIComponent(cap.slug) + '.html">' + escapeHtml(cap.title) +
|
|
489
|
+
'</a></th><td>' + badge(cap.verification_status) + '</td><td>' + badge(cap.drift_status) + '</td></tr>';
|
|
490
|
+
}).join('');
|
|
491
|
+
const warnings = [model.verification_error, model.progress_error, model.drift_error].filter(Boolean);
|
|
492
|
+
return shell(text.health, model, 'health',
|
|
493
|
+
'<h1>' + text.health + '</h1>' + (warnings.length ? '<div class="alert">' + warnings.map(escapeHtml).join('<br>') + '</div>' : '') +
|
|
494
|
+
'<section class="metrics">' + metric(text.verified, model.counts.verified, 'good') +
|
|
495
|
+
metric(text.stale, model.counts.stale, model.counts.stale ? 'warning' : '') + metric(text.needsReview, model.counts.needs_review) +
|
|
496
|
+
metric(text.blocked, model.counts.blocked) + metric(text.unverified, model.counts.unverified) + '</section>' +
|
|
497
|
+
'<section><h2>' + text.capabilityStatus + '</h2><div class="table-wrap"><table><thead><tr><th>Capability</th><th>' + text.verification + '</th><th>' + text.drift + '</th></tr></thead><tbody>' +
|
|
498
|
+
(caps || '<tr><td colspan="3">No capabilities.</td></tr>') + '</tbody></table></div></section>' +
|
|
499
|
+
(model.review_html ? '<section><h2>Independent review</h2><div class="doc">' + model.review_html + '</div></section>' : ''));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const SITE_CSS = [
|
|
503
|
+
':root{--bg:#f5f5f2;--panel:#fff;--ink:#171714;--muted:#6f7069;--line:#deded8;--accent:#2c5d4c;--ok:#2f6b4f;--warn:#9a6500;--bad:#9f3737;--soft:#ecece7}',
|
|
504
|
+
'*{box-sizing:border-box}body{margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:var(--bg);color:var(--ink);line-height:1.6}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}',
|
|
505
|
+
'.topbar{height:58px;display:flex;align-items:center;justify-content:space-between;padding:0 24px;border-bottom:1px solid var(--line);background:#fff;position:sticky;top:0;z-index:10}.brand{font-weight:800;color:var(--ink)}.top-meta{font-size:12px;color:var(--muted)}',
|
|
506
|
+
'.layout{display:grid;grid-template-columns:210px minmax(0,1fr);min-height:calc(100vh - 58px)}.sidebar{padding:24px 14px;border-right:1px solid var(--line);background:#fafaf7;position:sticky;top:58px;height:calc(100vh - 58px)}.sidebar a{display:block;padding:9px 12px;border-radius:8px;color:#454641;margin:2px 0}.sidebar a.active{background:#e7eee9;color:#194b3a;font-weight:700}.content{width:min(1180px,100%);padding:42px 48px 80px}',
|
|
507
|
+
'.hero{padding:36px;border:1px solid var(--line);border-radius:18px;background:linear-gradient(135deg,#fff,#edf2ee);margin-bottom:24px}.hero h1{font-size:42px;margin:6px 0}.hero p:last-child{max-width:720px;color:var(--muted)}.eyebrow{font-size:12px;letter-spacing:.16em;color:var(--accent);font-weight:800}',
|
|
508
|
+
'.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin:24px 0}.metric{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px}.metric-value{font-size:28px;font-weight:800}.metric-label{font-size:12px;color:var(--muted);text-transform:uppercase}.metric.good{border-color:#bdd4c8}.metric.warning{border-color:#dec690}',
|
|
509
|
+
'section{margin:34px 0}.section-head,.detail-head,.doc-title{display:flex;justify-content:space-between;gap:18px}.repo-grid,.cap-grid,.source-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}.repo-card,.cap-card,.source-group{background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px}.repo-card{display:flex;flex-direction:column}.cap-card{display:flex;justify-content:space-between;color:var(--ink)}',
|
|
510
|
+
'.cap-list{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:#fff}.cap-row{display:flex;justify-content:space-between;align-items:center;padding:14px 16px;border-bottom:1px solid var(--line);color:var(--ink)}.cap-row small{display:block;color:var(--muted)}',
|
|
511
|
+
'.badge{display:inline-block;font-size:11px;font-weight:800;text-transform:uppercase;border-radius:999px;padding:4px 8px;background:var(--soft)}.badge.ok{background:#dcece3;color:var(--ok)}.badge.warn{background:#f2e7c9;color:var(--warn)}.badge.bad{background:#f2dada;color:var(--bad)}',
|
|
512
|
+
'.table-wrap{overflow:auto;border:1px solid var(--line);border-radius:12px;background:#fff}table{border-collapse:collapse;width:100%;font-size:14px}th,td{text-align:left;padding:11px 13px;border-bottom:1px solid var(--line)}thead th{background:#f4f4ef}.matrix-cell{text-align:center}.dot{color:var(--accent)}',
|
|
513
|
+
'.search{width:100%;padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#fff;font:inherit;margin-bottom:18px}.muted{color:var(--muted)}.breadcrumbs{font-size:13px;color:var(--muted)}.two-col{display:grid;grid-template-columns:1fr 1fr;gap:18px}',
|
|
514
|
+
'.doc-card,.doc{background:#fff;border:1px solid var(--line);border-radius:14px;padding:22px;margin:16px 0}.doc-card>.doc{border:0;padding:0}.doc pre{background:#181a18;color:#f2f2ed;padding:16px;border-radius:10px;overflow:auto}.doc :not(pre)>code{background:#efefe9;padding:2px 5px;border-radius:5px}.doc blockquote{padding:8px 16px;border-left:3px solid var(--accent);background:#f4f7f4}.doc .mermaid{background:#fafaf7;color:var(--ink);border:1px dashed var(--line)}.alert{padding:14px;border:1px solid #dfc38e;background:#fff5dc;border-radius:10px}',
|
|
515
|
+
'@media(max-width:760px){.layout{display:block}.sidebar{position:static;height:auto;display:flex;overflow:auto;padding:8px}.sidebar a{white-space:nowrap}.content{padding:28px 18px}.top-meta{display:none}.two-col{grid-template-columns:1fr}}'
|
|
516
|
+
].join('');
|
|
517
|
+
|
|
518
|
+
const SITE_JS = [
|
|
519
|
+
"(function(){",
|
|
520
|
+
"var search=document.getElementById('capability-search');",
|
|
521
|
+
"if(search){search.addEventListener('input',function(){var q=search.value.trim().toLowerCase();document.querySelectorAll('[data-capability]').forEach(function(el){el.style.display=!q||el.dataset.capability.includes(q)?'flex':'none';});});}",
|
|
522
|
+
"})();"
|
|
523
|
+
].join('');
|
|
524
|
+
|
|
525
|
+
const REMOTE_MERMAID_JS = [
|
|
526
|
+
"(function(){",
|
|
527
|
+
"var diagrams=[].slice.call(document.querySelectorAll('pre.mermaid'));",
|
|
528
|
+
"if(diagrams.length&&navigator.onLine){var script=document.createElement('script');script.src='https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';script.onload=function(){if(window.mermaid){window.mermaid.initialize({startOnLoad:true,securityLevel:'strict'});}};document.head.appendChild(script);}",
|
|
529
|
+
"})();"
|
|
530
|
+
].join('');
|
|
531
|
+
|
|
532
|
+
function writeFile(filePath, content) {
|
|
533
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
534
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function buildSite(workspaceRoot, manifest, options) {
|
|
538
|
+
options = options || {};
|
|
539
|
+
const outputRoot = resolveOutputRoot(workspaceRoot, manifest, options.output);
|
|
540
|
+
if (options.clean !== false) fs.rmSync(outputRoot, { recursive: true, force: true });
|
|
541
|
+
fs.mkdirSync(outputRoot, { recursive: true });
|
|
542
|
+
const model = buildSystemModel(workspaceRoot, manifest, options);
|
|
543
|
+
writeFile(path.join(outputRoot, 'assets', 'site.css'), SITE_CSS + '\n');
|
|
544
|
+
writeFile(path.join(outputRoot, 'assets', 'app.js'), SITE_JS + (options.remoteMermaid ? REMOTE_MERMAID_JS : '') + '\n');
|
|
545
|
+
writeFile(path.join(outputRoot, 'index.html'), renderOverview(model));
|
|
546
|
+
const text = copy(model);
|
|
547
|
+
writeFile(path.join(outputRoot, 'architecture.html'), renderDocs(text.architecture, model.architecture, model, 'architecture'));
|
|
548
|
+
writeFile(path.join(outputRoot, 'capabilities.html'), renderCapabilities(model));
|
|
549
|
+
writeFile(path.join(outputRoot, 'flows.html'), renderDocs(text.flows, model.flows, model, 'flows'));
|
|
550
|
+
writeFile(path.join(outputRoot, 'contracts.html'), renderDocs(text.contracts, model.contracts, model, 'contracts'));
|
|
551
|
+
writeFile(path.join(outputRoot, 'health.html'), renderHealth(model));
|
|
552
|
+
model.capabilities.forEach(function(cap) {
|
|
553
|
+
writeFile(path.join(outputRoot, 'capability', cap.slug + '.html'), renderCapability(model, cap));
|
|
554
|
+
});
|
|
555
|
+
const publicModel = JSON.parse(JSON.stringify(model));
|
|
556
|
+
publicModel.capabilities.forEach(function(cap) {
|
|
557
|
+
delete cap.prd_html;
|
|
558
|
+
delete cap.design_html;
|
|
559
|
+
});
|
|
560
|
+
['architecture', 'flows', 'contracts'].forEach(function(group) {
|
|
561
|
+
publicModel[group].forEach(function(doc) { delete doc.html; });
|
|
562
|
+
});
|
|
563
|
+
delete publicModel.review_html;
|
|
564
|
+
writeFile(path.join(outputRoot, 'data', 'system.json'), JSON.stringify(publicModel, null, 2) + '\n');
|
|
565
|
+
return { outputRoot: outputRoot, model: model, pages: 6 + model.capabilities.length };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function createStaticServer(outputRoot) {
|
|
569
|
+
const root = path.resolve(outputRoot);
|
|
570
|
+
return http.createServer(function(req, res) {
|
|
571
|
+
let pathname;
|
|
572
|
+
try {
|
|
573
|
+
pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
|
|
574
|
+
} catch (_error) {
|
|
575
|
+
res.writeHead(400, { 'content-type': 'text/plain; charset=utf-8' });
|
|
576
|
+
res.end('Bad request');
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (pathname === '/') pathname = '/index.html';
|
|
580
|
+
const requested = path.resolve(root, '.' + pathname);
|
|
581
|
+
const relative = path.relative(root, requested);
|
|
582
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
583
|
+
res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
|
|
584
|
+
res.end('Forbidden');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (!fs.existsSync(requested) || !fs.statSync(requested).isFile()) {
|
|
588
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
589
|
+
res.end('Not found');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const types = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8' };
|
|
593
|
+
res.writeHead(200, { 'content-type': types[path.extname(requested)] || 'application/octet-stream', 'cache-control': 'no-cache' });
|
|
594
|
+
fs.createReadStream(requested).pipe(res);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function normalizePort(value) {
|
|
599
|
+
const port = value == null ? DEFAULT_PORT : Number(value);
|
|
600
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Invalid port: ' + value);
|
|
601
|
+
return port;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function serveSite(outputRoot, options) {
|
|
605
|
+
options = options || {};
|
|
606
|
+
const host = options.host || DEFAULT_HOST;
|
|
607
|
+
const port = normalizePort(options.port);
|
|
608
|
+
const server = createStaticServer(outputRoot);
|
|
609
|
+
if (!['127.0.0.1', 'localhost', '::1'].includes(host)) {
|
|
610
|
+
console.warn('WARNING: spec serve is bound to ' + host + '; system documentation may be reachable from other machines.');
|
|
611
|
+
}
|
|
612
|
+
server.on('error', function(error) {
|
|
613
|
+
console.error('spec serve failed: ' + error.message);
|
|
614
|
+
process.exitCode = 1;
|
|
615
|
+
});
|
|
616
|
+
server.listen(port, host, function() {
|
|
617
|
+
const address = server.address();
|
|
618
|
+
const actualPort = address && typeof address === 'object' ? address.port : port;
|
|
619
|
+
console.log('System spec site: http://' + host + ':' + actualPort);
|
|
620
|
+
console.log('Serving ' + portable(path.resolve(outputRoot)));
|
|
621
|
+
console.log('Press Ctrl+C to stop.');
|
|
622
|
+
});
|
|
623
|
+
return server;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
module.exports = {
|
|
627
|
+
DEFAULT_SITE_DIR: DEFAULT_SITE_DIR,
|
|
628
|
+
DEFAULT_PORT: DEFAULT_PORT,
|
|
629
|
+
DEFAULT_HOST: DEFAULT_HOST,
|
|
630
|
+
escapeHtml: escapeHtml,
|
|
631
|
+
markdownToHtml: markdownToHtml,
|
|
632
|
+
safeCapabilitySlug: safeCapabilitySlug,
|
|
633
|
+
resolveOutputRoot: resolveOutputRoot,
|
|
634
|
+
buildSystemModel: buildSystemModel,
|
|
635
|
+
buildSite: buildSite,
|
|
636
|
+
createStaticServer: createStaticServer,
|
|
637
|
+
normalizePort: normalizePort,
|
|
638
|
+
serveSite: serveSite
|
|
639
|
+
};
|