@tikoci/rosetta 0.6.7 → 0.6.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/README.md +2 -0
- package/package.json +1 -1
- package/src/browse.ts +211 -21
- package/src/db.ts +113 -0
- package/src/extract-all-versions.ts +83 -21
- package/src/extract-schema.ts +596 -0
- package/src/extract-skills.ts +390 -0
- package/src/mcp.ts +107 -8
- package/src/paths.ts +9 -2
- package/src/query.test.ts +128 -1
- package/src/query.ts +157 -9
- package/src/release.test.ts +29 -0
- package/src/schema-roundtrip.test.ts +373 -0
package/src/query.test.ts
CHANGED
|
@@ -34,6 +34,9 @@ const {
|
|
|
34
34
|
searchVideos,
|
|
35
35
|
searchDude,
|
|
36
36
|
getDudePage,
|
|
37
|
+
listSkills,
|
|
38
|
+
getSkill,
|
|
39
|
+
getSkillReference,
|
|
37
40
|
} = await import("./query.ts");
|
|
38
41
|
const { parseChangelog } = await import("./extract-changelogs.ts");
|
|
39
42
|
const { parseVtt, segmentTranscript } = await import("./extract-videos.ts");
|
|
@@ -92,6 +95,18 @@ beforeAll(() => {
|
|
|
92
95
|
(id, path, name, type, parent_path, page_id, description, ros_version)
|
|
93
96
|
VALUES (2, '/ip/dhcp-server', 'dhcp-server', 'dir', '/ip', 1, 'DHCP Server configuration', '7.22')`);
|
|
94
97
|
|
|
98
|
+
// schema_nodes for arch-filter tests: shared + x86-only child of /ip
|
|
99
|
+
db.run(`INSERT INTO schema_nodes (path, name, type, parent_path, dir_role, _arch)
|
|
100
|
+
VALUES ('/ip', 'ip', 'dir', NULL, 'namespace', NULL)`);
|
|
101
|
+
db.run(`INSERT INTO schema_nodes (path, name, type, parent_path, dir_role, _arch)
|
|
102
|
+
VALUES ('/ip/dhcp-server', 'dhcp-server', 'dir', '/ip', 'list', NULL)`);
|
|
103
|
+
db.run(`INSERT INTO schema_nodes (path, name, type, parent_path, dir_role, _arch)
|
|
104
|
+
VALUES ('/ip/x86-feature', 'x86-feature', 'cmd', '/ip', NULL, 'x86')`);
|
|
105
|
+
// Also insert a commands row for x86-feature so it participates in browseCommands
|
|
106
|
+
db.run(`INSERT INTO commands
|
|
107
|
+
(id, path, name, type, parent_path, page_id, description, ros_version)
|
|
108
|
+
VALUES (99, '/ip/x86-feature', 'x86-feature', 'cmd', '/ip', NULL, 'x86-only', '7.22')`);
|
|
109
|
+
|
|
95
110
|
db.run(`INSERT INTO command_versions (command_path, ros_version)
|
|
96
111
|
VALUES ('/ip/dhcp-server', '7.22')`);
|
|
97
112
|
db.run(`INSERT INTO command_versions (command_path, ros_version)
|
|
@@ -765,6 +780,28 @@ describe("browseCommands", () => {
|
|
|
765
780
|
const dhcp = children.find((c) => c.path === "/ip/dhcp-server");
|
|
766
781
|
expect(dhcp?.page_title).toBe("DHCP Server");
|
|
767
782
|
});
|
|
783
|
+
|
|
784
|
+
test("arch param binds correctly — x86-only node gets NULL enrichment when arch=arm64", () => {
|
|
785
|
+
// /ip/x86-feature has _arch='x86' in schema_nodes; browsing with arch='arm64'
|
|
786
|
+
// should still return the command (LEFT JOIN from commands), but schema enrichment
|
|
787
|
+
// (dir_role) should be NULL since the x86 schema_nodes row doesn't match arm64 filter.
|
|
788
|
+
const children = browseCommands("/ip", "arm64");
|
|
789
|
+
const x86Feature = children.find((c) => c.path === "/ip/x86-feature");
|
|
790
|
+
expect(x86Feature).toBeDefined(); // command still in results (from commands table)
|
|
791
|
+
expect(x86Feature?.dir_role).toBeNull(); // schema enrichment absent for other-arch node
|
|
792
|
+
|
|
793
|
+
// dhcp-server (_arch=NULL = shared) still gets enrichment
|
|
794
|
+
const dhcp = children.find((c) => c.path === "/ip/dhcp-server");
|
|
795
|
+
expect(dhcp?.dir_role).toBe("list"); // schema enrichment present for shared node
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test("arch param binds correctly — x86 filter enriches x86-only node", () => {
|
|
799
|
+
const children = browseCommands("/ip", "x86");
|
|
800
|
+
const x86Feature = children.find((c) => c.path === "/ip/x86-feature");
|
|
801
|
+
expect(x86Feature).toBeDefined();
|
|
802
|
+
// x86-feature has _arch='x86', matches the arch='x86' filter, so _arch is populated
|
|
803
|
+
expect(x86Feature?._arch).toBe("x86");
|
|
804
|
+
});
|
|
768
805
|
});
|
|
769
806
|
|
|
770
807
|
// ---------------------------------------------------------------------------
|
|
@@ -1494,6 +1531,7 @@ describe("schema", () => {
|
|
|
1494
1531
|
"commands", "command_versions", "ros_versions",
|
|
1495
1532
|
"devices", "device_test_results", "changelogs", "schema_migrations",
|
|
1496
1533
|
"videos", "video_segments",
|
|
1534
|
+
"skills", "skill_references",
|
|
1497
1535
|
];
|
|
1498
1536
|
for (const table of expected) {
|
|
1499
1537
|
expect(names).toContain(table);
|
|
@@ -1502,7 +1540,7 @@ describe("schema", () => {
|
|
|
1502
1540
|
|
|
1503
1541
|
test("all FTS5 virtual tables exist", () => {
|
|
1504
1542
|
const names = tableNames();
|
|
1505
|
-
const expected = ["pages_fts", "properties_fts", "callouts_fts", "devices_fts", "changelogs_fts", "videos_fts", "video_segments_fts"];
|
|
1543
|
+
const expected = ["pages_fts", "properties_fts", "callouts_fts", "devices_fts", "changelogs_fts", "videos_fts", "video_segments_fts", "skills_fts"];
|
|
1506
1544
|
for (const fts of expected) {
|
|
1507
1545
|
expect(names).toContain(fts);
|
|
1508
1546
|
}
|
|
@@ -1564,6 +1602,13 @@ describe("schema", () => {
|
|
|
1564
1602
|
expect(triggers).toContain("dude_pages_au");
|
|
1565
1603
|
});
|
|
1566
1604
|
|
|
1605
|
+
test("content-sync triggers exist for skills", () => {
|
|
1606
|
+
const triggers = triggerNames();
|
|
1607
|
+
expect(triggers).toContain("skills_ai");
|
|
1608
|
+
expect(triggers).toContain("skills_ad");
|
|
1609
|
+
expect(triggers).toContain("skills_au");
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1567
1612
|
test("PRAGMA user_version matches SCHEMA_VERSION", () => {
|
|
1568
1613
|
const result = checkSchemaVersion();
|
|
1569
1614
|
expect(result.ok).toBe(true);
|
|
@@ -1583,6 +1628,88 @@ describe("getDbStats", () => {
|
|
|
1583
1628
|
expect(stats.ros_version_min).toBe("7.9");
|
|
1584
1629
|
expect(stats.ros_version_max).toBe("7.22");
|
|
1585
1630
|
});
|
|
1631
|
+
|
|
1632
|
+
test("includes skills count", () => {
|
|
1633
|
+
const stats = getDbStats();
|
|
1634
|
+
expect(stats).toHaveProperty("skills");
|
|
1635
|
+
expect(typeof stats.skills).toBe("number");
|
|
1636
|
+
});
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
// ---------------------------------------------------------------------------
|
|
1640
|
+
// Skills: agent guides from tikoci/routeros-skills
|
|
1641
|
+
// ---------------------------------------------------------------------------
|
|
1642
|
+
|
|
1643
|
+
describe("skills", () => {
|
|
1644
|
+
// Insert fixture data for skills tests
|
|
1645
|
+
beforeAll(() => {
|
|
1646
|
+
db.run("DELETE FROM skill_references");
|
|
1647
|
+
db.run("DELETE FROM skills");
|
|
1648
|
+
|
|
1649
|
+
db.run(`INSERT INTO skills (name, description, content, source_repo, source_sha, source_url, word_count, extracted_at)
|
|
1650
|
+
VALUES ('routeros-test-skill', 'Test skill for unit tests', 'This is test content for the skill.', 'tikoci/routeros-skills', 'abc123', 'https://github.com/tikoci/routeros-skills/blob/abc123/routeros-test-skill/SKILL.md', 7, '2025-01-01T00:00:00Z')`);
|
|
1651
|
+
|
|
1652
|
+
const row = db.prepare("SELECT id FROM skills WHERE name = 'routeros-test-skill'").get() as { id: number };
|
|
1653
|
+
|
|
1654
|
+
db.run(`INSERT INTO skill_references (skill_id, path, filename, content, word_count)
|
|
1655
|
+
VALUES (${row.id}, 'references/test-ref.md', 'test-ref.md', 'Reference content here.', 3)`);
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
test("listSkills returns all skills", () => {
|
|
1659
|
+
const skills = listSkills();
|
|
1660
|
+
expect(skills.length).toBeGreaterThanOrEqual(1);
|
|
1661
|
+
const testSkill = skills.find(s => s.name === "routeros-test-skill");
|
|
1662
|
+
expect(testSkill).toBeDefined();
|
|
1663
|
+
if (!testSkill) throw new Error("Expected test skill to exist");
|
|
1664
|
+
expect(testSkill.description).toBe("Test skill for unit tests");
|
|
1665
|
+
expect(testSkill.word_count).toBe(7);
|
|
1666
|
+
expect(testSkill.ref_count).toBe(1);
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
test("getSkill returns full skill with provenance and references", () => {
|
|
1670
|
+
const skill = getSkill("routeros-test-skill");
|
|
1671
|
+
expect(skill).not.toBeNull();
|
|
1672
|
+
if (!skill) throw new Error("Expected skill details to exist");
|
|
1673
|
+
expect(skill.name).toBe("routeros-test-skill");
|
|
1674
|
+
expect(skill.content).toBe("This is test content for the skill.");
|
|
1675
|
+
expect(skill.provenance).toContain("PROVENANCE");
|
|
1676
|
+
expect(skill.provenance).toContain("tikoci/routeros-skills");
|
|
1677
|
+
expect(skill.source_sha).toBe("abc123");
|
|
1678
|
+
expect(skill.references).toHaveLength(1);
|
|
1679
|
+
expect(skill.references[0]?.filename).toBe("test-ref.md");
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
test("getSkill is case-insensitive", () => {
|
|
1683
|
+
const skill = getSkill("ROUTEROS-TEST-SKILL");
|
|
1684
|
+
expect(skill).not.toBeNull();
|
|
1685
|
+
if (!skill) throw new Error("Expected case-insensitive skill lookup to work");
|
|
1686
|
+
expect(skill.name).toBe("routeros-test-skill");
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
test("getSkill returns null for nonexistent skill", () => {
|
|
1690
|
+
const skill = getSkill("nonexistent-skill");
|
|
1691
|
+
expect(skill).toBeNull();
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
test("getSkillReference returns a specific reference file", () => {
|
|
1695
|
+
const ref = getSkillReference("routeros-test-skill", "test-ref.md");
|
|
1696
|
+
expect(ref).not.toBeNull();
|
|
1697
|
+
if (!ref) throw new Error("Expected skill reference to exist");
|
|
1698
|
+
expect(ref.content).toBe("Reference content here.");
|
|
1699
|
+
expect(ref.word_count).toBe(3);
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
test("getSkillReference returns null for nonexistent reference", () => {
|
|
1703
|
+
const ref = getSkillReference("routeros-test-skill", "nonexistent.md");
|
|
1704
|
+
expect(ref).toBeNull();
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
test("FTS5 indexes skill content via triggers", () => {
|
|
1708
|
+
const results = db.prepare(
|
|
1709
|
+
"SELECT rowid FROM skills_fts WHERE skills_fts MATCH 'test'"
|
|
1710
|
+
).all();
|
|
1711
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
1712
|
+
});
|
|
1586
1713
|
});
|
|
1587
1714
|
|
|
1588
1715
|
// ---------------------------------------------------------------------------
|
package/src/query.ts
CHANGED
|
@@ -475,9 +475,29 @@ export function lookupProperty(
|
|
|
475
475
|
.all(name) as typeof lookupProperty extends (...a: unknown[]) => infer R ? R : never;
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
+
/** Parse _attrs JSON and extract completion, stripping _attrs from output. */
|
|
479
|
+
function enrichWithCompletion(row: unknown): Record<string, unknown> {
|
|
480
|
+
const r = row as Record<string, unknown>;
|
|
481
|
+
const attrs = r._attrs as string | null;
|
|
482
|
+
const result = { ...r };
|
|
483
|
+
delete result._attrs;
|
|
484
|
+
if (attrs) {
|
|
485
|
+
try {
|
|
486
|
+
const parsed = JSON.parse(attrs);
|
|
487
|
+
result.completion = parsed.completion ?? null;
|
|
488
|
+
} catch {
|
|
489
|
+
result.completion = null;
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
result.completion = null;
|
|
493
|
+
}
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
|
|
478
497
|
/** Browse the command tree at a given path. */
|
|
479
498
|
export function browseCommands(
|
|
480
499
|
cmdPath: string,
|
|
500
|
+
arch?: string,
|
|
481
501
|
): Array<{
|
|
482
502
|
path: string;
|
|
483
503
|
name: string;
|
|
@@ -485,17 +505,29 @@ export function browseCommands(
|
|
|
485
505
|
description: string | null;
|
|
486
506
|
page_title: string | null;
|
|
487
507
|
page_url: string | null;
|
|
508
|
+
dir_role: string | null;
|
|
509
|
+
data_type: string | null;
|
|
510
|
+
enum_values: string | null;
|
|
511
|
+
_arch: string | null;
|
|
512
|
+
completion: Record<string, { style: string; preference: number; desc?: string }> | null;
|
|
488
513
|
}> {
|
|
514
|
+
const archFilter = arch ? " AND (sn._arch IS NULL OR sn._arch = ?)" : "";
|
|
515
|
+
// arch placeholder is in the JOIN clause (before WHERE), so it must come first
|
|
516
|
+
const params: string[] = [...(arch ? [arch] : []), cmdPath];
|
|
517
|
+
|
|
489
518
|
return db
|
|
490
519
|
.prepare(
|
|
491
520
|
`SELECT c.path, c.name, c.type, c.description,
|
|
492
|
-
p.title as page_title, p.url as page_url
|
|
521
|
+
p.title as page_title, p.url as page_url,
|
|
522
|
+
sn.dir_role, sn.data_type, sn.enum_values, sn._arch, sn._attrs
|
|
493
523
|
FROM commands c
|
|
494
524
|
LEFT JOIN pages p ON c.page_id = p.id
|
|
525
|
+
LEFT JOIN schema_nodes sn ON sn.path = c.path AND sn.type = c.type${archFilter}
|
|
495
526
|
WHERE c.parent_path = ?
|
|
496
527
|
ORDER BY c.type DESC, c.name`,
|
|
497
528
|
)
|
|
498
|
-
.all(
|
|
529
|
+
.all(...params)
|
|
530
|
+
.map(enrichWithCompletion) as ReturnType<typeof browseCommands>;
|
|
499
531
|
}
|
|
500
532
|
|
|
501
533
|
/** Search properties by FTS query. */
|
|
@@ -663,6 +695,7 @@ export function diffCommandVersions(
|
|
|
663
695
|
fromVersion: string,
|
|
664
696
|
toVersion: string,
|
|
665
697
|
pathPrefix?: string,
|
|
698
|
+
arch?: string,
|
|
666
699
|
): CommandDiffResult {
|
|
667
700
|
const allVersionRows = db
|
|
668
701
|
.prepare("SELECT DISTINCT version FROM ros_versions")
|
|
@@ -682,31 +715,46 @@ export function diffCommandVersions(
|
|
|
682
715
|
const prefixFilter = prefix ? " AND (command_path = ? OR command_path LIKE ? || '/%')" : "";
|
|
683
716
|
const prefixParams = (v: string) => prefix ? [v, prefix, prefix] : [v];
|
|
684
717
|
|
|
718
|
+
// When arch is specified, exclude paths belonging to the other arch
|
|
719
|
+
const otherArch = arch === "x86" ? "arm64" : arch === "arm64" ? "x86" : null;
|
|
720
|
+
const archExclude = otherArch
|
|
721
|
+
? " AND cv_to.command_path NOT IN (SELECT path FROM schema_nodes WHERE _arch = ?)"
|
|
722
|
+
: "";
|
|
723
|
+
const archExcludeFrom = otherArch
|
|
724
|
+
? " AND cv_from.command_path NOT IN (SELECT path FROM schema_nodes WHERE _arch = ?)"
|
|
725
|
+
: "";
|
|
726
|
+
|
|
685
727
|
type Row = { command_path: string };
|
|
686
728
|
|
|
729
|
+
const addedParams = [...prefixParams(toVersion), ...(otherArch ? [otherArch] : []), ...prefixParams(fromVersion)];
|
|
687
730
|
const addedRows = db
|
|
688
731
|
.prepare(
|
|
689
732
|
`SELECT DISTINCT cv_to.command_path
|
|
690
733
|
FROM command_versions cv_to
|
|
691
|
-
WHERE cv_to.ros_version = ?${prefixFilter}
|
|
734
|
+
WHERE cv_to.ros_version = ?${prefixFilter}${archExclude}
|
|
692
735
|
AND cv_to.command_path NOT IN (
|
|
693
736
|
SELECT command_path FROM command_versions WHERE ros_version = ?${prefixFilter}
|
|
694
737
|
)
|
|
695
738
|
ORDER BY cv_to.command_path`,
|
|
696
739
|
)
|
|
697
|
-
.all(...
|
|
740
|
+
.all(...addedParams) as Row[];
|
|
698
741
|
|
|
742
|
+
const removedParams = [...prefixParams(fromVersion), ...(otherArch ? [otherArch] : []), ...prefixParams(toVersion)];
|
|
699
743
|
const removedRows = db
|
|
700
744
|
.prepare(
|
|
701
745
|
`SELECT DISTINCT cv_from.command_path
|
|
702
746
|
FROM command_versions cv_from
|
|
703
|
-
WHERE cv_from.ros_version = ?${prefixFilter}
|
|
747
|
+
WHERE cv_from.ros_version = ?${prefixFilter}${archExcludeFrom}
|
|
704
748
|
AND cv_from.command_path NOT IN (
|
|
705
749
|
SELECT command_path FROM command_versions WHERE ros_version = ?${prefixFilter}
|
|
706
750
|
)
|
|
707
751
|
ORDER BY cv_from.command_path`,
|
|
708
752
|
)
|
|
709
|
-
.all(...
|
|
753
|
+
.all(...removedParams) as Row[];
|
|
754
|
+
|
|
755
|
+
if (arch) {
|
|
756
|
+
notes.push(`Filtered to ${arch} architecture (excluding ${otherArch}-only paths).`);
|
|
757
|
+
}
|
|
710
758
|
|
|
711
759
|
return {
|
|
712
760
|
from_version: fromVersion,
|
|
@@ -789,6 +837,7 @@ export function compareVersions(a: string, b: string): number {
|
|
|
789
837
|
export function browseCommandsAtVersion(
|
|
790
838
|
cmdPath: string,
|
|
791
839
|
version: string,
|
|
840
|
+
arch?: string,
|
|
792
841
|
): Array<{
|
|
793
842
|
path: string;
|
|
794
843
|
name: string;
|
|
@@ -796,18 +845,30 @@ export function browseCommandsAtVersion(
|
|
|
796
845
|
description: string | null;
|
|
797
846
|
page_title: string | null;
|
|
798
847
|
page_url: string | null;
|
|
848
|
+
dir_role: string | null;
|
|
849
|
+
data_type: string | null;
|
|
850
|
+
enum_values: string | null;
|
|
851
|
+
_arch: string | null;
|
|
852
|
+
completion: Record<string, { style: string; preference: number; desc?: string }> | null;
|
|
799
853
|
}> {
|
|
854
|
+
const archFilter = arch ? " AND (sn._arch IS NULL OR sn._arch = ?)" : "";
|
|
855
|
+
// arch placeholder is in the JOIN clause (before WHERE), so it must come first
|
|
856
|
+
const params: string[] = [...(arch ? [arch] : []), cmdPath, version];
|
|
857
|
+
|
|
800
858
|
return db
|
|
801
859
|
.prepare(
|
|
802
860
|
`SELECT c.path, c.name, c.type, c.description,
|
|
803
|
-
p.title as page_title, p.url as page_url
|
|
861
|
+
p.title as page_title, p.url as page_url,
|
|
862
|
+
sn.dir_role, sn.data_type, sn.enum_values, sn._arch, sn._attrs
|
|
804
863
|
FROM commands c
|
|
805
864
|
LEFT JOIN pages p ON c.page_id = p.id
|
|
806
865
|
JOIN command_versions cv ON cv.command_path = c.path
|
|
866
|
+
LEFT JOIN schema_nodes sn ON sn.path = c.path AND sn.type = c.type${archFilter}
|
|
807
867
|
WHERE c.parent_path = ? AND cv.ros_version = ?
|
|
808
868
|
ORDER BY c.type DESC, c.name`,
|
|
809
869
|
)
|
|
810
|
-
.all(
|
|
870
|
+
.all(...params)
|
|
871
|
+
.map(enrichWithCompletion) as ReturnType<typeof browseCommandsAtVersion>;
|
|
811
872
|
}
|
|
812
873
|
|
|
813
874
|
// ── Device lookup and search ──
|
|
@@ -1648,7 +1709,94 @@ export function getDudePage(idOrTitle: number | string, maxLength?: number): Dud
|
|
|
1648
1709
|
return page;
|
|
1649
1710
|
}
|
|
1650
1711
|
|
|
1651
|
-
// ──
|
|
1712
|
+
// ── Skills (agent guides from tikoci/routeros-skills) ──
|
|
1713
|
+
|
|
1714
|
+
const SKILLS_PROVENANCE = `⚠️ PROVENANCE: Community-created content from tikoci/routeros-skills —
|
|
1715
|
+
NOT official MikroTik documentation. AI-generated, human-reviewed. May contain errors.
|
|
1716
|
+
Verify claims using routeros_search and routeros_get_page for official documentation.`;
|
|
1717
|
+
|
|
1718
|
+
export type SkillSummary = {
|
|
1719
|
+
name: string;
|
|
1720
|
+
description: string;
|
|
1721
|
+
word_count: number;
|
|
1722
|
+
ref_count: number;
|
|
1723
|
+
source_url: string;
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
export type SkillReference = {
|
|
1727
|
+
path: string;
|
|
1728
|
+
filename: string;
|
|
1729
|
+
content: string;
|
|
1730
|
+
word_count: number;
|
|
1731
|
+
};
|
|
1732
|
+
|
|
1733
|
+
export type SkillDetail = {
|
|
1734
|
+
name: string;
|
|
1735
|
+
description: string;
|
|
1736
|
+
content: string;
|
|
1737
|
+
provenance: string;
|
|
1738
|
+
source_repo: string;
|
|
1739
|
+
source_sha: string | null;
|
|
1740
|
+
source_url: string;
|
|
1741
|
+
word_count: number;
|
|
1742
|
+
extracted_at: string | null;
|
|
1743
|
+
references: SkillReference[];
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
/** List all available skills with reference counts. */
|
|
1747
|
+
export function listSkills(): SkillSummary[] {
|
|
1748
|
+
return db
|
|
1749
|
+
.prepare(
|
|
1750
|
+
`SELECT s.name, s.description, s.word_count, s.source_url,
|
|
1751
|
+
(SELECT COUNT(*) FROM skill_references sr WHERE sr.skill_id = s.id) as ref_count
|
|
1752
|
+
FROM skills s
|
|
1753
|
+
ORDER BY s.name`,
|
|
1754
|
+
)
|
|
1755
|
+
.all() as SkillSummary[];
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/** Get a single skill by name, including its references and provenance header. */
|
|
1759
|
+
export function getSkill(name: string): SkillDetail | null {
|
|
1760
|
+
const row = db
|
|
1761
|
+
.prepare("SELECT * FROM skills WHERE name = ? COLLATE NOCASE")
|
|
1762
|
+
.get(name) as Record<string, unknown> | null;
|
|
1763
|
+
if (!row) return null;
|
|
1764
|
+
|
|
1765
|
+
const refs = db
|
|
1766
|
+
.prepare(
|
|
1767
|
+
`SELECT path, filename, content, word_count
|
|
1768
|
+
FROM skill_references
|
|
1769
|
+
WHERE skill_id = ?
|
|
1770
|
+
ORDER BY path`,
|
|
1771
|
+
)
|
|
1772
|
+
.all(row.id as number) as SkillReference[];
|
|
1773
|
+
|
|
1774
|
+
return {
|
|
1775
|
+
name: row.name as string,
|
|
1776
|
+
description: row.description as string,
|
|
1777
|
+
content: row.content as string,
|
|
1778
|
+
provenance: `${SKILLS_PROVENANCE}\nSource: ${row.source_url as string}`,
|
|
1779
|
+
source_repo: row.source_repo as string,
|
|
1780
|
+
source_sha: row.source_sha as string | null,
|
|
1781
|
+
source_url: row.source_url as string,
|
|
1782
|
+
word_count: row.word_count as number,
|
|
1783
|
+
extracted_at: row.extracted_at as string | null,
|
|
1784
|
+
references: refs,
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/** Get a single reference file from a skill. */
|
|
1789
|
+
export function getSkillReference(skillName: string, refFilename: string): SkillReference | null {
|
|
1790
|
+
const row = db
|
|
1791
|
+
.prepare(
|
|
1792
|
+
`SELECT sr.path, sr.filename, sr.content, sr.word_count
|
|
1793
|
+
FROM skill_references sr
|
|
1794
|
+
JOIN skills s ON s.id = sr.skill_id
|
|
1795
|
+
WHERE s.name = ? COLLATE NOCASE AND sr.filename = ? COLLATE NOCASE`,
|
|
1796
|
+
)
|
|
1797
|
+
.get(skillName, refFilename) as SkillReference | null;
|
|
1798
|
+
return row;
|
|
1799
|
+
}
|
|
1652
1800
|
|
|
1653
1801
|
const VERSION_BASE_URL = "https://upgrade.mikrotik.com/routeros/NEWESTa7";
|
|
1654
1802
|
|
package/src/release.test.ts
CHANGED
|
@@ -210,6 +210,21 @@ describe("Makefile", () => {
|
|
|
210
210
|
expect(makefile).toContain("extract-dude-from-cache:");
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
test("has extract-skills target", () => {
|
|
214
|
+
expect(makefile).toContain("extract-skills:");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("has extract-skills-from-cache target", () => {
|
|
218
|
+
expect(makefile).toContain("extract-skills-from-cache:");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("extract-skills is in PHONY", () => {
|
|
222
|
+
const phonyStart = makefile.indexOf(".PHONY:");
|
|
223
|
+
const phonyEnd = makefile.indexOf("\n\n", phonyStart);
|
|
224
|
+
const phonyBlock = makefile.slice(phonyStart, phonyEnd);
|
|
225
|
+
expect(phonyBlock).toContain("extract-skills");
|
|
226
|
+
});
|
|
227
|
+
|
|
213
228
|
test("extract-dude is in PHONY", () => {
|
|
214
229
|
const phonyStart = makefile.indexOf(".PHONY:");
|
|
215
230
|
const phonyEnd = makefile.indexOf("\n\n", phonyStart);
|
|
@@ -225,6 +240,14 @@ describe("Makefile", () => {
|
|
|
225
240
|
expect(makefile).toMatch(/^release:.*build-release/m);
|
|
226
241
|
});
|
|
227
242
|
|
|
243
|
+
test("extract target includes skills", () => {
|
|
244
|
+
expect(makefile).toMatch(/^extract:.*extract-skills/m);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("extract-full target includes skills", () => {
|
|
248
|
+
expect(makefile).toMatch(/^extract-full:.*extract-skills/m);
|
|
249
|
+
});
|
|
250
|
+
|
|
228
251
|
test("extract target includes Dude cache import", () => {
|
|
229
252
|
expect(makefile).toMatch(/^extract:.*extract-dude-from-cache/m);
|
|
230
253
|
});
|
|
@@ -275,6 +298,7 @@ describe("release.yml", () => {
|
|
|
275
298
|
expect(src).toContain("extract-devices.ts");
|
|
276
299
|
expect(src).toContain("extract-test-results.ts");
|
|
277
300
|
expect(src).toContain("extract-changelogs.ts");
|
|
301
|
+
expect(src).toContain("extract-skills.ts");
|
|
278
302
|
expect(src).toContain("link-commands.ts");
|
|
279
303
|
});
|
|
280
304
|
|
|
@@ -283,6 +307,11 @@ describe("release.yml", () => {
|
|
|
283
307
|
expect(src).toContain("extract-dude-from-cache");
|
|
284
308
|
});
|
|
285
309
|
|
|
310
|
+
test("extracts agent skills in CI", () => {
|
|
311
|
+
const src = readText(".github/workflows/release.yml");
|
|
312
|
+
expect(src).toContain("extract-skills.ts");
|
|
313
|
+
});
|
|
314
|
+
|
|
286
315
|
test("runs quality gate before release", () => {
|
|
287
316
|
const src = readText(".github/workflows/release.yml");
|
|
288
317
|
expect(src).toContain("bun run typecheck");
|