@rune-kit/rune 2.4.0 → 2.7.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 +47 -17
- package/compiler/__tests__/executive-dashboards.test.js +285 -0
- package/compiler/__tests__/inject.test.js +128 -0
- package/compiler/__tests__/orchestrators.test.js +151 -0
- package/compiler/__tests__/org-templates.test.js +447 -0
- package/compiler/__tests__/pack-split.test.js +141 -1
- package/compiler/__tests__/parser.test.js +147 -1
- package/compiler/__tests__/scripts-bundling.test.js +10 -11
- package/compiler/__tests__/skill-index.test.js +218 -0
- package/compiler/__tests__/status.test.js +336 -0
- package/compiler/__tests__/templates.test.js +245 -0
- package/compiler/__tests__/visualizer.test.js +325 -0
- package/compiler/adapters/antigravity.js +18 -4
- package/compiler/bin/rune.js +90 -1
- package/compiler/doctor.js +283 -2
- package/compiler/emitter.js +490 -17
- package/compiler/parser.js +255 -4
- package/compiler/status.js +342 -0
- package/compiler/visualizer.js +622 -0
- package/hooks/hooks.json +12 -0
- package/hooks/intent-router/index.cjs +108 -0
- package/hooks/pre-tool-guard/index.cjs +177 -68
- package/package.json +63 -63
- package/skills/autopsy/SKILL.md +48 -1
- package/skills/brainstorm/SKILL.md +2 -0
- package/skills/completion-gate/SKILL.md +26 -1
- package/skills/context-engine/SKILL.md +93 -2
- package/skills/cook/SKILL.md +794 -648
- package/skills/debug/SKILL.md +409 -392
- package/skills/deploy/SKILL.md +2 -0
- package/skills/docs/SKILL.md +28 -3
- package/skills/fix/SKILL.md +284 -281
- package/skills/mcp-builder/SKILL.md +53 -1
- package/skills/onboard/SKILL.md +58 -1
- package/skills/perf/SKILL.md +34 -1
- package/skills/plan/SKILL.md +372 -342
- package/skills/preflight/SKILL.md +396 -360
- package/skills/retro/SKILL.md +95 -1
- package/skills/review/SKILL.md +535 -489
- package/skills/scope-guard/SKILL.md +1 -0
- package/skills/scout/SKILL.md +1 -0
- package/skills/sentinel/SKILL.md +353 -299
- package/skills/sentinel/references/policy-driven-constraints.md +424 -0
- package/skills/session-bridge/SKILL.md +58 -2
- package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
- package/skills/team/SKILL.md +16 -1
- package/skills/test/SKILL.md +587 -585
- package/skills/verification/SKILL.md +1 -0
- package/skills/watchdog/SKILL.md +2 -0
package/compiler/parser.js
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* Extracts frontmatter, cross-references, tool references, HARD-GATE blocks, and sections.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Metadata fields that should be parsed as comma-separated arrays
|
|
10
|
+
* e.g. emit: code.changed, tests.passed → ["code.changed", "tests.passed"]
|
|
11
|
+
*/
|
|
12
|
+
const COMMA_LIST_FIELDS = new Set(['emit', 'listen']);
|
|
13
|
+
|
|
8
14
|
const CROSS_REF_PATTERN = /`?rune:([a-z][\w-]*)`?/g;
|
|
9
15
|
const TOOL_REF_PATTERN = /`(Read|Write|Edit|Glob|Grep|Bash|TodoWrite|Skill|Agent)`/g;
|
|
10
16
|
const HARD_GATE_PATTERN = /<HARD-GATE>([\s\S]*?)<\/HARD-GATE>/g;
|
|
@@ -41,10 +47,29 @@ function parseFrontmatter(content) {
|
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
if (currentIndent === 'nested' && line.startsWith(' ')) {
|
|
44
|
-
|
|
50
|
+
// YAML list item (e.g. " - value")
|
|
51
|
+
const listMatch = trimmed.match(/^-\s+(.+)$/);
|
|
52
|
+
if (listMatch) {
|
|
53
|
+
// Convert nested block to array if not already
|
|
54
|
+
if (!Array.isArray(frontmatter[nestedKey])) {
|
|
55
|
+
frontmatter[nestedKey] = [];
|
|
56
|
+
}
|
|
57
|
+
frontmatter[nestedKey].push(listMatch[1].replace(/^["']|["']$/g, ''));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
45
62
|
if (kvMatch) {
|
|
46
|
-
const
|
|
47
|
-
|
|
63
|
+
const rawValue = kvMatch[2].replace(/^["']|["']$/g, '');
|
|
64
|
+
// Comma-separated list fields → parse as array
|
|
65
|
+
if (COMMA_LIST_FIELDS.has(kvMatch[1])) {
|
|
66
|
+
frontmatter[nestedKey][kvMatch[1]] = rawValue
|
|
67
|
+
.split(',')
|
|
68
|
+
.map((s) => s.trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
} else {
|
|
71
|
+
frontmatter[nestedKey][kvMatch[1]] = rawValue;
|
|
72
|
+
}
|
|
48
73
|
}
|
|
49
74
|
continue;
|
|
50
75
|
}
|
|
@@ -55,7 +80,15 @@ function parseFrontmatter(content) {
|
|
|
55
80
|
const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
56
81
|
if (kvMatch) {
|
|
57
82
|
const value = kvMatch[2].replace(/^["']|["']$/g, '');
|
|
58
|
-
|
|
83
|
+
// Comma-separated list fields at top level too (Pro/Business packs)
|
|
84
|
+
if (COMMA_LIST_FIELDS.has(kvMatch[1])) {
|
|
85
|
+
frontmatter[kvMatch[1]] = value
|
|
86
|
+
.split(',')
|
|
87
|
+
.map((s) => s.trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
} else {
|
|
90
|
+
frontmatter[kvMatch[1]] = value;
|
|
91
|
+
}
|
|
59
92
|
}
|
|
60
93
|
}
|
|
61
94
|
|
|
@@ -168,12 +201,22 @@ export function parseSkill(content, filePath = '') {
|
|
|
168
201
|
|
|
169
202
|
const metadata = frontmatter.metadata || {};
|
|
170
203
|
|
|
204
|
+
// Extract signals — emit/listen arrays from metadata or top-level (Pro/Business packs)
|
|
205
|
+
const emit = Array.isArray(metadata.emit) ? metadata.emit : Array.isArray(frontmatter.emit) ? frontmatter.emit : [];
|
|
206
|
+
const listen = Array.isArray(metadata.listen)
|
|
207
|
+
? metadata.listen
|
|
208
|
+
: Array.isArray(frontmatter.listen)
|
|
209
|
+
? frontmatter.listen
|
|
210
|
+
: [];
|
|
211
|
+
const signals = emit.length > 0 || listen.length > 0 ? { emit, listen } : null;
|
|
212
|
+
|
|
171
213
|
return {
|
|
172
214
|
name: frontmatter.name || '',
|
|
173
215
|
description: frontmatter.description || '',
|
|
174
216
|
layer: metadata.layer || 'L2',
|
|
175
217
|
model: metadata.model || 'sonnet',
|
|
176
218
|
group: metadata.group || 'general',
|
|
219
|
+
signals,
|
|
177
220
|
contextFork: frontmatter.context === 'fork',
|
|
178
221
|
agentType: frontmatter.agent || null,
|
|
179
222
|
body,
|
|
@@ -223,6 +266,214 @@ export function parsePack(content, filePath = '') {
|
|
|
223
266
|
* @param {Array} skills - array of skill entries from frontmatter metadata.skills
|
|
224
267
|
* @returns {Array<{name: string, file: string, model: string, description: string}>}
|
|
225
268
|
*/
|
|
269
|
+
/**
|
|
270
|
+
* Parse a workflow template file (templates/*.md)
|
|
271
|
+
*
|
|
272
|
+
* Templates have the same frontmatter structure as skills but with additional
|
|
273
|
+
* template-specific fields: domain, chain, connections[]
|
|
274
|
+
*
|
|
275
|
+
* @param {string} content - raw template file content
|
|
276
|
+
* @param {string} [filePath] - optional file path for error reporting
|
|
277
|
+
* @returns {ParsedTemplate}
|
|
278
|
+
*/
|
|
279
|
+
export function parseTemplate(content, filePath = '') {
|
|
280
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
281
|
+
|
|
282
|
+
// Parse signals — can be nested object { emit: "...", listen: "..." } or flat
|
|
283
|
+
const signalsRaw = frontmatter.signals || {};
|
|
284
|
+
const emit =
|
|
285
|
+
typeof signalsRaw.emit === 'string'
|
|
286
|
+
? signalsRaw.emit
|
|
287
|
+
.split(',')
|
|
288
|
+
.map((s) => s.trim())
|
|
289
|
+
.filter(Boolean)
|
|
290
|
+
: Array.isArray(signalsRaw.emit)
|
|
291
|
+
? signalsRaw.emit
|
|
292
|
+
: [];
|
|
293
|
+
const listen =
|
|
294
|
+
typeof signalsRaw.listen === 'string'
|
|
295
|
+
? signalsRaw.listen
|
|
296
|
+
.split(',')
|
|
297
|
+
.map((s) => s.trim())
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
: Array.isArray(signalsRaw.listen)
|
|
300
|
+
? signalsRaw.listen
|
|
301
|
+
: [];
|
|
302
|
+
|
|
303
|
+
// Parse connections array — supports both comma-separated string and YAML array
|
|
304
|
+
let connections = [];
|
|
305
|
+
if (frontmatter.connections) {
|
|
306
|
+
if (typeof frontmatter.connections === 'string') {
|
|
307
|
+
connections = frontmatter.connections
|
|
308
|
+
.split(',')
|
|
309
|
+
.map((s) => s.trim())
|
|
310
|
+
.filter(Boolean);
|
|
311
|
+
} else if (Array.isArray(frontmatter.connections)) {
|
|
312
|
+
connections = frontmatter.connections;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
name: frontmatter.name || '',
|
|
318
|
+
pack: frontmatter.pack || '',
|
|
319
|
+
version: frontmatter.version || '1.0.0',
|
|
320
|
+
description: frontmatter.description || '',
|
|
321
|
+
domain: frontmatter.domain || '',
|
|
322
|
+
chain: frontmatter.chain || 'standard',
|
|
323
|
+
signals: { emit, listen },
|
|
324
|
+
connections,
|
|
325
|
+
body,
|
|
326
|
+
crossRefs: extractCrossRefs(body),
|
|
327
|
+
sections: extractSections(body),
|
|
328
|
+
filePath,
|
|
329
|
+
frontmatter,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Parse organization config from .rune/org/org.md
|
|
335
|
+
* Extracts teams, roles, policies, approval flows, and governance level.
|
|
336
|
+
*/
|
|
337
|
+
export function parseOrgConfig(content, filePath = '') {
|
|
338
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
339
|
+
|
|
340
|
+
const teams = parseMarkdownTable(body, 'Teams');
|
|
341
|
+
const roles = parseMarkdownTable(body, 'Roles');
|
|
342
|
+
const policies = parseOrgPolicies(body);
|
|
343
|
+
const approvalFlows = parseApprovalFlows(body);
|
|
344
|
+
const governanceLevel = parseGovernanceLevel(body);
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
name: frontmatter.name || '',
|
|
348
|
+
description: frontmatter.description || '',
|
|
349
|
+
version: frontmatter.version || '1.0.0',
|
|
350
|
+
tier: frontmatter.tier || 'business',
|
|
351
|
+
teams,
|
|
352
|
+
roles,
|
|
353
|
+
policies,
|
|
354
|
+
approvalFlows,
|
|
355
|
+
governanceLevel,
|
|
356
|
+
body,
|
|
357
|
+
filePath,
|
|
358
|
+
frontmatter,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Parse a Markdown table under a ## heading into array of objects.
|
|
364
|
+
* Handles | col1 | col2 | format with header row + separator row + data rows.
|
|
365
|
+
*/
|
|
366
|
+
function parseMarkdownTable(body, sectionName) {
|
|
367
|
+
// Find the section
|
|
368
|
+
const sectionPattern = new RegExp(`## ${sectionName}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
369
|
+
const sectionMatch = body.match(sectionPattern);
|
|
370
|
+
if (!sectionMatch) return [];
|
|
371
|
+
|
|
372
|
+
const sectionContent = sectionMatch[1];
|
|
373
|
+
const lines = sectionContent.split('\n').filter((l) => l.trim().startsWith('|'));
|
|
374
|
+
if (lines.length < 3) return []; // need header + separator + at least 1 row
|
|
375
|
+
|
|
376
|
+
const parseRow = (line) =>
|
|
377
|
+
line
|
|
378
|
+
.split('|')
|
|
379
|
+
.slice(1, -1)
|
|
380
|
+
.map((cell) => cell.trim());
|
|
381
|
+
|
|
382
|
+
const headers = parseRow(lines[0]).map((h) => h.toLowerCase().replace(/\s+/g, '_'));
|
|
383
|
+
// Skip separator row (lines[1])
|
|
384
|
+
const rows = [];
|
|
385
|
+
for (let i = 2; i < lines.length; i++) {
|
|
386
|
+
const cells = parseRow(lines[i]);
|
|
387
|
+
if (cells.length === 0) continue;
|
|
388
|
+
const row = {};
|
|
389
|
+
headers.forEach((h, idx) => {
|
|
390
|
+
row[h] = cells[idx] || '';
|
|
391
|
+
});
|
|
392
|
+
rows.push(row);
|
|
393
|
+
}
|
|
394
|
+
return rows;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Parse ### subsections under ## Policies into a map of policy name → rules array.
|
|
399
|
+
*/
|
|
400
|
+
function parseOrgPolicies(body) {
|
|
401
|
+
const policiesPattern = /## Policies\s*\n([\s\S]*?)(?=\n## [^#]|$)/;
|
|
402
|
+
const policiesMatch = body.match(policiesPattern);
|
|
403
|
+
if (!policiesMatch) return {};
|
|
404
|
+
|
|
405
|
+
// Prepend \n so first ### subsection is also captured by split
|
|
406
|
+
const policiesContent = `\n${policiesMatch[1].trimStart()}`;
|
|
407
|
+
const policies = {};
|
|
408
|
+
const subSections = policiesContent.split(/\n### /);
|
|
409
|
+
|
|
410
|
+
for (let i = 1; i < subSections.length; i++) {
|
|
411
|
+
const lines = subSections[i].split('\n');
|
|
412
|
+
const name = lines[0].trim().toLowerCase().replace(/\s+/g, '_');
|
|
413
|
+
const rules = lines
|
|
414
|
+
.slice(1)
|
|
415
|
+
.filter((l) => l.trim().startsWith('- **'))
|
|
416
|
+
.map((l) => {
|
|
417
|
+
const match = l.match(/- \*\*(.+?)\*\*:\s*(.+)/);
|
|
418
|
+
if (!match) return null;
|
|
419
|
+
return {
|
|
420
|
+
key: match[1].trim().toLowerCase().replace(/\s+/g, '_'),
|
|
421
|
+
value: match[2].trim(),
|
|
422
|
+
};
|
|
423
|
+
})
|
|
424
|
+
.filter(Boolean);
|
|
425
|
+
policies[name] = rules;
|
|
426
|
+
}
|
|
427
|
+
return policies;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Parse ### subsections under ## Approval Flows into named flow strings.
|
|
432
|
+
*/
|
|
433
|
+
function parseApprovalFlows(body) {
|
|
434
|
+
const flowsPattern = /## Approval Flows\s*\n([\s\S]*?)(?=\n## [^#]|$)/;
|
|
435
|
+
const flowsMatch = body.match(flowsPattern);
|
|
436
|
+
if (!flowsMatch) return {};
|
|
437
|
+
|
|
438
|
+
// Prepend \n so first ### subsection is also captured by split
|
|
439
|
+
const flowsContent = `\n${flowsMatch[1].trimStart()}`;
|
|
440
|
+
const flows = {};
|
|
441
|
+
const subSections = flowsContent.split(/\n### /);
|
|
442
|
+
|
|
443
|
+
for (let i = 1; i < subSections.length; i++) {
|
|
444
|
+
const lines = subSections[i].split('\n');
|
|
445
|
+
const name = lines[0].trim().toLowerCase().replace(/\s+/g, '_');
|
|
446
|
+
// Extract content between ``` blocks
|
|
447
|
+
const codeMatch = subSections[i].match(/```\n([\s\S]*?)```/);
|
|
448
|
+
if (codeMatch) {
|
|
449
|
+
flows[name] = codeMatch[1].trim();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return flows;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Parse ## Governance Level section for governance mode and settings.
|
|
457
|
+
*/
|
|
458
|
+
function parseGovernanceLevel(body) {
|
|
459
|
+
const govPattern = /## Governance Level\s*\n([\s\S]*?)(?=\n## |$)/;
|
|
460
|
+
const govMatch = body.match(govPattern);
|
|
461
|
+
if (!govMatch) return { level: 'unknown', settings: [] };
|
|
462
|
+
|
|
463
|
+
const content = govMatch[1];
|
|
464
|
+
// Extract bold level: **Minimal**, **Moderate**, **Maximum**
|
|
465
|
+
const levelMatch = content.match(/\*\*(\w+)\*\*/);
|
|
466
|
+
const level = levelMatch ? levelMatch[1].toLowerCase() : 'unknown';
|
|
467
|
+
|
|
468
|
+
// Extract bullet settings
|
|
469
|
+
const settings = content
|
|
470
|
+
.split('\n')
|
|
471
|
+
.filter((l) => l.trim().startsWith('- '))
|
|
472
|
+
.map((l) => l.trim().replace(/^- /, ''));
|
|
473
|
+
|
|
474
|
+
return { level, settings };
|
|
475
|
+
}
|
|
476
|
+
|
|
226
477
|
function parseSkillManifest(skills) {
|
|
227
478
|
if (!Array.isArray(skills)) return [];
|
|
228
479
|
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status — Project Neofetch
|
|
3
|
+
*
|
|
4
|
+
* Shows a rich boxed dashboard of the current Rune project.
|
|
5
|
+
* Output varies by detected tier: Free, Pro, Business.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { parseSkill } from './parser.js';
|
|
12
|
+
|
|
13
|
+
// ─── Constants ───
|
|
14
|
+
|
|
15
|
+
// ─── Box Drawing ───
|
|
16
|
+
|
|
17
|
+
function box(lines, { title = '', width = 52 } = {}) {
|
|
18
|
+
const inner = width - 2;
|
|
19
|
+
const output = [];
|
|
20
|
+
|
|
21
|
+
const titleStr = title ? ` ${title} ` : '';
|
|
22
|
+
const topFill = inner - titleStr.length - 1;
|
|
23
|
+
output.push(`╭─${titleStr}${'─'.repeat(Math.max(topFill, 0))}╮`);
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const dw = displayWidth(line);
|
|
27
|
+
const pad = inner - dw;
|
|
28
|
+
output.push(`│ ${line}${' '.repeat(Math.max(pad, 0))}│`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
output.push(`╰${'─'.repeat(inner + 1)}╯`);
|
|
32
|
+
return output.join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function displayWidth(str) {
|
|
36
|
+
let w = 0;
|
|
37
|
+
for (const ch of str) {
|
|
38
|
+
const code = ch.codePointAt(0);
|
|
39
|
+
// Emoji and wide chars take 2 columns
|
|
40
|
+
if (
|
|
41
|
+
code > 0x1f600 ||
|
|
42
|
+
(code >= 0x2600 && code <= 0x27bf) ||
|
|
43
|
+
(code >= 0x2700 && code <= 0x27bf) ||
|
|
44
|
+
code === 0x2713 ||
|
|
45
|
+
code === 0x2717 ||
|
|
46
|
+
code === 0x2192 ||
|
|
47
|
+
code === 0x2593 ||
|
|
48
|
+
code === 0x2591
|
|
49
|
+
) {
|
|
50
|
+
w += 1; // These specific Unicode symbols are single-width in most terminals
|
|
51
|
+
} else if (code > 0xffff) {
|
|
52
|
+
w += 2; // Emoji (surrogate pairs) are double-width
|
|
53
|
+
} else {
|
|
54
|
+
w += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return w;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function progressBar(pct, width = 20) {
|
|
61
|
+
const filled = Math.round((pct / 100) * width);
|
|
62
|
+
const empty = width - filled;
|
|
63
|
+
return '▓'.repeat(filled) + '░'.repeat(empty);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Data Collection ───
|
|
67
|
+
|
|
68
|
+
export async function collectStats(runeRoot, tierSources = {}) {
|
|
69
|
+
const skillsDir = path.join(runeRoot, 'skills');
|
|
70
|
+
const extDir = path.join(runeRoot, 'extensions');
|
|
71
|
+
|
|
72
|
+
// Count core skills by layer
|
|
73
|
+
const layers = { L0: 0, L1: 0, L2: 0, L3: 0 };
|
|
74
|
+
const skillNames = [];
|
|
75
|
+
let signalCount = 0;
|
|
76
|
+
const signalMap = { emitters: {}, listeners: {} };
|
|
77
|
+
const parsedSkills = [];
|
|
78
|
+
|
|
79
|
+
if (existsSync(skillsDir)) {
|
|
80
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (!entry.isDirectory()) continue;
|
|
83
|
+
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
84
|
+
if (!existsSync(skillFile)) continue;
|
|
85
|
+
|
|
86
|
+
const content = await readFile(skillFile, 'utf-8');
|
|
87
|
+
const parsed = parseSkill(content, skillFile);
|
|
88
|
+
parsedSkills.push(parsed);
|
|
89
|
+
skillNames.push(parsed.name);
|
|
90
|
+
|
|
91
|
+
const layer = parsed.layer || 'L3';
|
|
92
|
+
if (layers[layer] !== undefined) layers[layer]++;
|
|
93
|
+
|
|
94
|
+
if (parsed.signals?.emit && parsed.signals?.listen) {
|
|
95
|
+
for (const sig of parsed.signals.emit) {
|
|
96
|
+
if (!signalMap.emitters[sig]) signalMap.emitters[sig] = [];
|
|
97
|
+
signalMap.emitters[sig].push(parsed.name);
|
|
98
|
+
}
|
|
99
|
+
for (const sig of parsed.signals.listen) {
|
|
100
|
+
if (!signalMap.listeners[sig]) signalMap.listeners[sig] = [];
|
|
101
|
+
signalMap.listeners[sig].push(parsed.name);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const allSignals = new Set([...Object.keys(signalMap.emitters), ...Object.keys(signalMap.listeners)]);
|
|
106
|
+
signalCount = allSignals.size;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Count connections
|
|
110
|
+
let totalConnections = 0;
|
|
111
|
+
for (const skill of parsedSkills) {
|
|
112
|
+
totalConnections += new Set((skill.crossRefs ?? []).map((r) => r.skillName)).size;
|
|
113
|
+
}
|
|
114
|
+
const avgConnections = parsedSkills.length > 0 ? (totalConnections / parsedSkills.length).toFixed(1) : '0';
|
|
115
|
+
|
|
116
|
+
// Count free packs
|
|
117
|
+
const freePacks = [];
|
|
118
|
+
if (existsSync(extDir)) {
|
|
119
|
+
const entries = await readdir(extDir, { withFileTypes: true });
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (!entry.isDirectory()) continue;
|
|
122
|
+
const packFile = path.join(extDir, entry.name, 'PACK.md');
|
|
123
|
+
if (!existsSync(packFile)) continue;
|
|
124
|
+
freePacks.push(entry.name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Count Pro packs
|
|
129
|
+
const proPacks = await scanTierPacks(tierSources.pro);
|
|
130
|
+
const bizPacks = await scanTierPacks(tierSources.business);
|
|
131
|
+
|
|
132
|
+
// Detect tier
|
|
133
|
+
const tier = bizPacks.length > 0 ? 'business' : proPacks.length > 0 ? 'pro' : 'free';
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
tier,
|
|
137
|
+
skillCount: parsedSkills.length,
|
|
138
|
+
layers,
|
|
139
|
+
signalCount,
|
|
140
|
+
signalMap,
|
|
141
|
+
totalConnections,
|
|
142
|
+
avgConnections,
|
|
143
|
+
freePacks,
|
|
144
|
+
proPacks,
|
|
145
|
+
bizPacks,
|
|
146
|
+
parsedSkills,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function scanTierPacks(tierDir) {
|
|
151
|
+
const packs = [];
|
|
152
|
+
if (!tierDir || !existsSync(tierDir)) return packs;
|
|
153
|
+
|
|
154
|
+
const entries = await readdir(tierDir, { withFileTypes: true });
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
if (!entry.isDirectory()) continue;
|
|
157
|
+
const packDir = path.join(tierDir, entry.name);
|
|
158
|
+
const packFile = path.join(packDir, 'PACK.md');
|
|
159
|
+
if (!existsSync(packFile)) continue;
|
|
160
|
+
|
|
161
|
+
// Count total lines in pack
|
|
162
|
+
let lines = 0;
|
|
163
|
+
const subEntries = await readdir(packDir, { withFileTypes: true });
|
|
164
|
+
for (const sub of subEntries) {
|
|
165
|
+
if (sub.isFile() && sub.name.endsWith('.md')) {
|
|
166
|
+
const content = await readFile(path.join(packDir, sub.name), 'utf-8');
|
|
167
|
+
lines += content.split('\n').length;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
packs.push({ name: entry.name, lines });
|
|
172
|
+
}
|
|
173
|
+
return packs;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Rendering ───
|
|
177
|
+
|
|
178
|
+
function tierIcon(tier) {
|
|
179
|
+
if (tier === 'business') return '🏢';
|
|
180
|
+
if (tier === 'pro') return '⚡';
|
|
181
|
+
return '🔮';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function tierLabel(tier) {
|
|
185
|
+
if (tier === 'business') return 'Rune Business';
|
|
186
|
+
if (tier === 'pro') return 'Rune Pro';
|
|
187
|
+
return 'Rune';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function fmtNum(n) {
|
|
191
|
+
return n.toLocaleString('en-US');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function renderStatus(stats, { version = '', platform = '', projectName = '' } = {}) {
|
|
195
|
+
const lines = [];
|
|
196
|
+
const icon = tierIcon(stats.tier);
|
|
197
|
+
const label = tierLabel(stats.tier);
|
|
198
|
+
|
|
199
|
+
// Header
|
|
200
|
+
lines.push('');
|
|
201
|
+
|
|
202
|
+
// Project info
|
|
203
|
+
if (projectName) lines.push(`Project ${projectName}`);
|
|
204
|
+
if (platform) lines.push(`Platform ${platform}`);
|
|
205
|
+
if (version) lines.push(`Version ${version}`);
|
|
206
|
+
if (projectName || platform || version) lines.push('');
|
|
207
|
+
|
|
208
|
+
// Core stats
|
|
209
|
+
const layerStr = Object.entries(stats.layers)
|
|
210
|
+
.filter(([, v]) => v > 0)
|
|
211
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
212
|
+
.join(' ');
|
|
213
|
+
lines.push(`Skills ${stats.skillCount} core (${layerStr})`);
|
|
214
|
+
|
|
215
|
+
const packParts = [`${stats.freePacks.length} free`];
|
|
216
|
+
if (stats.proPacks.length > 0) packParts.push(`${stats.proPacks.length} pro`);
|
|
217
|
+
if (stats.bizPacks.length > 0) packParts.push(`${stats.bizPacks.length} business`);
|
|
218
|
+
lines.push(`Packs ${packParts.join(' + ')}`);
|
|
219
|
+
|
|
220
|
+
lines.push(`Signals ${stats.signalCount} defined`);
|
|
221
|
+
lines.push(`Mesh ${stats.totalConnections}+ connections (${stats.avgConnections} avg/skill)`);
|
|
222
|
+
lines.push('');
|
|
223
|
+
|
|
224
|
+
// Health bar (based on signals + connections + pack count as simple heuristic)
|
|
225
|
+
const healthScore = computeHealth(stats);
|
|
226
|
+
lines.push(`${progressBar(healthScore)} ${healthScore}% mesh health`);
|
|
227
|
+
lines.push('');
|
|
228
|
+
|
|
229
|
+
// Pro section
|
|
230
|
+
if (stats.proPacks.length > 0) {
|
|
231
|
+
lines.push('Pro Packs');
|
|
232
|
+
for (const pack of stats.proPacks) {
|
|
233
|
+
lines.push(` ✓ ${formatPackName(pack.name, 'pro')} ${fmtNum(pack.lines)} lines`);
|
|
234
|
+
}
|
|
235
|
+
lines.push('');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Business section
|
|
239
|
+
if (stats.bizPacks.length > 0) {
|
|
240
|
+
lines.push('Business Packs');
|
|
241
|
+
for (const pack of stats.bizPacks) {
|
|
242
|
+
lines.push(` ✓ ${formatPackName(pack.name, 'business')} ${fmtNum(pack.lines)} lines`);
|
|
243
|
+
}
|
|
244
|
+
lines.push('');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Top signals (show signal flow)
|
|
248
|
+
const topSignals = getTopSignals(stats.signalMap, 3);
|
|
249
|
+
if (topSignals.length > 0) {
|
|
250
|
+
lines.push('Active Signals');
|
|
251
|
+
for (const sig of topSignals) {
|
|
252
|
+
const emitters = sig.emitters.slice(0, 2).join(', ');
|
|
253
|
+
const listeners = sig.listeners.slice(0, 3).join(', ');
|
|
254
|
+
let sigLine = ` → ${sig.name} (${emitters} → ${listeners})`;
|
|
255
|
+
if (sigLine.length > 64) sigLine = `${sigLine.slice(0, 61)}...`;
|
|
256
|
+
lines.push(sigLine);
|
|
257
|
+
}
|
|
258
|
+
lines.push('');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const title = `${icon} ${label}`;
|
|
262
|
+
const maxLineLen = lines.reduce((max, l) => Math.max(max, displayWidth(l)), 48);
|
|
263
|
+
const boxWidth = Math.min(Math.max(maxLineLen + 4, 52), 72);
|
|
264
|
+
|
|
265
|
+
return box(lines, { title, width: boxWidth });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function renderStatusJson(stats, { version = '', platform = '', projectName = '' } = {}) {
|
|
269
|
+
return JSON.stringify(
|
|
270
|
+
{
|
|
271
|
+
project: projectName || undefined,
|
|
272
|
+
platform: platform || undefined,
|
|
273
|
+
version: version || undefined,
|
|
274
|
+
tier: stats.tier,
|
|
275
|
+
skills: {
|
|
276
|
+
total: stats.skillCount,
|
|
277
|
+
layers: stats.layers,
|
|
278
|
+
},
|
|
279
|
+
packs: {
|
|
280
|
+
free: stats.freePacks.length,
|
|
281
|
+
pro: stats.proPacks.map((p) => ({ name: p.name, lines: p.lines })),
|
|
282
|
+
business: stats.bizPacks.map((p) => ({ name: p.name, lines: p.lines })),
|
|
283
|
+
},
|
|
284
|
+
signals: {
|
|
285
|
+
count: stats.signalCount,
|
|
286
|
+
top: getTopSignals(stats.signalMap, 5).map((s) => ({
|
|
287
|
+
name: s.name,
|
|
288
|
+
emitters: s.emitters,
|
|
289
|
+
listeners: s.listeners,
|
|
290
|
+
})),
|
|
291
|
+
},
|
|
292
|
+
mesh: {
|
|
293
|
+
connections: stats.totalConnections,
|
|
294
|
+
avgPerSkill: parseFloat(stats.avgConnections),
|
|
295
|
+
},
|
|
296
|
+
health: computeHealth(stats),
|
|
297
|
+
},
|
|
298
|
+
null,
|
|
299
|
+
2,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function formatPackName(dirName, tier = 'free') {
|
|
304
|
+
const baseName = dirName.replace(/^(pro|business)-/, '');
|
|
305
|
+
if (tier === 'business') return `@rune-biz/${baseName}`.padEnd(26);
|
|
306
|
+
if (tier === 'pro') return `@rune-pro/${baseName}`.padEnd(26);
|
|
307
|
+
return `@rune/${baseName}`.padEnd(26);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getTopSignals(signalMap, limit) {
|
|
311
|
+
const signals = new Set([...Object.keys(signalMap.emitters), ...Object.keys(signalMap.listeners)]);
|
|
312
|
+
|
|
313
|
+
const scored = [...signals].map((name) => ({
|
|
314
|
+
name,
|
|
315
|
+
emitters: signalMap.emitters[name] || [],
|
|
316
|
+
listeners: signalMap.listeners[name] || [],
|
|
317
|
+
score: (signalMap.emitters[name]?.length || 0) + (signalMap.listeners[name]?.length || 0),
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
scored.sort((a, b) => b.score - a.score);
|
|
321
|
+
return scored.slice(0, limit);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function computeHealth(stats) {
|
|
325
|
+
let score = 0;
|
|
326
|
+
|
|
327
|
+
// Skills (max 25)
|
|
328
|
+
score += Math.min(stats.skillCount / 60, 1) * 25;
|
|
329
|
+
|
|
330
|
+
// Signals (max 25)
|
|
331
|
+
score += Math.min(stats.signalCount / 15, 1) * 25;
|
|
332
|
+
|
|
333
|
+
// Connections density (max 25)
|
|
334
|
+
const avgConn = parseFloat(stats.avgConnections);
|
|
335
|
+
score += Math.min(avgConn / 3.5, 1) * 25;
|
|
336
|
+
|
|
337
|
+
// Pack coverage (max 25)
|
|
338
|
+
const totalPacks = stats.freePacks.length + stats.proPacks.length + stats.bizPacks.length;
|
|
339
|
+
score += Math.min(totalPacks / 14, 1) * 25;
|
|
340
|
+
|
|
341
|
+
return Math.round(score);
|
|
342
|
+
}
|