convoke-agents 3.1.0 → 3.2.1
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/CHANGELOG.md +31 -0
- package/README.md +37 -10
- package/_bmad/bme/_artifacts/config.yaml +15 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/SKILL.md +6 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-01-scope.md +138 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-02-dryrun.md +199 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-03-resolve.md +174 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/steps/step-04-execute.md +213 -0
- package/_bmad/bme/_artifacts/workflows/bmad-migrate-artifacts/workflow.md +85 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/SKILL.md +6 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-01-scan.md +131 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-02-explore.md +131 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/steps/step-03-recommend.md +149 -0
- package/_bmad/bme/_artifacts/workflows/bmad-portfolio-status/workflow.md +78 -0
- package/_bmad/bme/_portability/skills/bmad-export-skill/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-export-skill/workflow.md +74 -0
- package/_bmad/bme/_portability/skills/bmad-generate-catalog/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-generate-catalog/workflow.md +42 -0
- package/_bmad/bme/_portability/skills/bmad-seed-catalog/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-seed-catalog/workflow.md +61 -0
- package/_bmad/bme/_portability/skills/bmad-validate-exports/SKILL.md +6 -0
- package/_bmad/bme/_portability/skills/bmad-validate-exports/workflow.md +43 -0
- package/_bmad/bme/_team-factory/agents/team-factory.md +128 -0
- package/_bmad/bme/_team-factory/config.yaml +13 -0
- package/_bmad/bme/_team-factory/lib/cascade-logic.js +184 -0
- package/_bmad/bme/_team-factory/lib/collision-detector.js +228 -0
- package/_bmad/bme/_team-factory/lib/manifest-tracker.js +214 -0
- package/_bmad/bme/_team-factory/lib/spec-differ.js +176 -0
- package/_bmad/bme/_team-factory/lib/spec-parser.js +201 -0
- package/_bmad/bme/_team-factory/lib/spec-writer.js +128 -0
- package/_bmad/bme/_team-factory/lib/types/factory-types.js +193 -0
- package/_bmad/bme/_team-factory/lib/utils/csv-utils.js +62 -0
- package/_bmad/bme/_team-factory/lib/utils/naming-utils.js +45 -0
- package/_bmad/bme/_team-factory/lib/validators/end-to-end-validator.js +898 -0
- package/_bmad/bme/_team-factory/lib/writers/activation-validator.js +175 -0
- package/_bmad/bme/_team-factory/lib/writers/config-appender.js +192 -0
- package/_bmad/bme/_team-factory/lib/writers/config-creator.js +215 -0
- package/_bmad/bme/_team-factory/lib/writers/csv-appender.js +118 -0
- package/_bmad/bme/_team-factory/lib/writers/csv-creator.js +190 -0
- package/_bmad/bme/_team-factory/lib/writers/registry-appender.js +372 -0
- package/_bmad/bme/_team-factory/lib/writers/registry-writer.js +409 -0
- package/_bmad/bme/_team-factory/module-help.csv +3 -0
- package/_bmad/bme/_team-factory/schemas/schema-independent.json +147 -0
- package/_bmad/bme/_team-factory/schemas/schema-sequential.json +242 -0
- package/_bmad/bme/_team-factory/templates/team-spec-template.yaml +86 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-01-scope.md +105 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-02-connect.md +110 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-03-review.md +116 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-04-generate.md +160 -0
- package/_bmad/bme/_team-factory/workflows/add-team/step-05-validate.md +146 -0
- package/_bmad/bme/_team-factory/workflows/step-00-route.md +76 -0
- package/_bmad/bme/_vortex/config.yaml +4 -4
- package/package.json +13 -7
- package/scripts/convoke-doctor.js +172 -1
- package/scripts/install-gyre-agents.js +0 -0
- package/scripts/lib/artifact-utils.js +521 -13
- package/scripts/lib/portfolio/portfolio-engine.js +301 -34
- package/scripts/lib/portfolio/rules/artifact-chain-rule.js +33 -3
- package/scripts/lib/portfolio/rules/conflict-resolver.js +22 -0
- package/scripts/migrate-artifacts.js +69 -10
- package/scripts/portability/catalog-generator.js +353 -0
- package/scripts/portability/classify-skills.js +646 -0
- package/scripts/portability/convoke-export.js +522 -0
- package/scripts/portability/export-engine.js +1156 -0
- package/scripts/portability/generate-adapters.js +79 -0
- package/scripts/portability/manifest-csv.js +147 -0
- package/scripts/portability/seed-catalog-repo.js +427 -0
- package/scripts/portability/templates/canonical-example.md +102 -0
- package/scripts/portability/templates/canonical-format.md +218 -0
- package/scripts/portability/templates/readme-template.md +72 -0
- package/scripts/portability/test-constants.js +42 -0
- package/scripts/portability/validate-classification.js +529 -0
- package/scripts/portability/validate-exports.js +348 -0
- package/scripts/update/lib/agent-registry.js +35 -0
- package/scripts/update/lib/config-merger.js +140 -10
- package/scripts/update/lib/refresh-installation.js +293 -8
- package/scripts/update/lib/utils.js +27 -1
- package/scripts/update/lib/validator.js +114 -4
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generate-adapters.js — Story sp-5-2
|
|
3
|
+
*
|
|
4
|
+
* Generates per-platform adapter files for exported skills.
|
|
5
|
+
* Each adapter wraps the canonical instructions.md with platform-specific
|
|
6
|
+
* metadata/formatting. Thin adapter principle: adapters add packaging,
|
|
7
|
+
* NOT content modifications.
|
|
8
|
+
*
|
|
9
|
+
* This is a module (no CLI entry point). Called from convoke-export.js's
|
|
10
|
+
* runSingle() after writing instructions.md and README.md.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Truncate description to first sentence (period + space) or 120 chars.
|
|
20
|
+
* Same logic as catalog-generator.js's truncateDescription.
|
|
21
|
+
*/
|
|
22
|
+
function truncateDescription(desc) {
|
|
23
|
+
if (!desc) return '';
|
|
24
|
+
const periodIdx = desc.indexOf('. ');
|
|
25
|
+
if (periodIdx > 0 && periodIdx < 120) {
|
|
26
|
+
return desc.slice(0, periodIdx + 1);
|
|
27
|
+
}
|
|
28
|
+
if (desc.length <= 120) return desc;
|
|
29
|
+
return desc.slice(0, 120) + '...';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate all platform adapters for a single exported skill.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} skillName - manifest skill name (e.g., 'bmad-brainstorming')
|
|
36
|
+
* @param {object} skillRow - manifest row as keyed object (.name, .description, etc.)
|
|
37
|
+
* @param {string} instructionsContent - the canonical instructions.md content
|
|
38
|
+
* @param {string} skillOutputDir - path to the skill's export directory (e.g., <output>/bmad-brainstorming/)
|
|
39
|
+
*/
|
|
40
|
+
function generateAdapters(skillName, skillRow, instructionsContent, skillOutputDir) {
|
|
41
|
+
const adaptersDir = path.join(skillOutputDir, 'adapters');
|
|
42
|
+
|
|
43
|
+
// Claude Code adapter
|
|
44
|
+
const claudeDir = path.join(adaptersDir, 'claude-code');
|
|
45
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
46
|
+
const description = truncateDescription(skillRow.description || '');
|
|
47
|
+
// Escape single quotes in description for YAML safety
|
|
48
|
+
const safeDesc = description.replace(/'/g, "''");
|
|
49
|
+
const claudeContent = [
|
|
50
|
+
'---',
|
|
51
|
+
`name: ${skillName}`,
|
|
52
|
+
`description: '${safeDesc}'`,
|
|
53
|
+
'---',
|
|
54
|
+
'',
|
|
55
|
+
instructionsContent,
|
|
56
|
+
].join('\n');
|
|
57
|
+
fs.writeFileSync(path.join(claudeDir, 'SKILL.md'), claudeContent);
|
|
58
|
+
|
|
59
|
+
// GitHub Copilot adapter
|
|
60
|
+
const copilotDir = path.join(adaptersDir, 'copilot');
|
|
61
|
+
fs.mkdirSync(copilotDir, { recursive: true });
|
|
62
|
+
const displayName = (skillRow.name || skillName)
|
|
63
|
+
.replace(/^bmad-cis-agent-/, '')
|
|
64
|
+
.replace(/^bmad-agent-/, '')
|
|
65
|
+
.replace(/^bmad-cis-/, '')
|
|
66
|
+
.replace(/^bmad-/, '')
|
|
67
|
+
.split('-')
|
|
68
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
69
|
+
.join(' ');
|
|
70
|
+
const copilotContent = `<!-- Skill: ${displayName} — append to .github/copilot-instructions.md -->\n${instructionsContent}`;
|
|
71
|
+
fs.writeFileSync(path.join(copilotDir, 'copilot-instructions.md'), copilotContent);
|
|
72
|
+
|
|
73
|
+
// Cursor adapter
|
|
74
|
+
const cursorDir = path.join(adaptersDir, 'cursor');
|
|
75
|
+
fs.mkdirSync(cursorDir, { recursive: true });
|
|
76
|
+
fs.writeFileSync(path.join(cursorDir, `${skillName}.md`), instructionsContent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { generateAdapters };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC-4180-aware CSV parser for skill-manifest.csv and friends.
|
|
3
|
+
*
|
|
4
|
+
* This is the manifest *parser* used by tests and the classification script.
|
|
5
|
+
* It is intentionally separate from `scripts/lib/csv-utils.js` (Team Factory's
|
|
6
|
+
* CSV writer utility) — different concerns, different consumers.
|
|
7
|
+
*
|
|
8
|
+
* Handles:
|
|
9
|
+
* - Quoted fields containing commas
|
|
10
|
+
* - Escaped quotes (`""` inside a quoted field → literal `"`)
|
|
11
|
+
* - Unquoted fields
|
|
12
|
+
* - Trailing CR (CRLF line endings)
|
|
13
|
+
* - UTF-8 BOM stripping
|
|
14
|
+
* - Whitespace-only line filtering
|
|
15
|
+
*
|
|
16
|
+
* Story: sp-1-1 (introduced parser inline in test), sp-1-2 (extracted to shared module).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a single CSV row into an array of fields.
|
|
25
|
+
*
|
|
26
|
+
* Fields are trimmed of leading/trailing whitespace AFTER quote handling.
|
|
27
|
+
* This protects idempotency: hand-edited CSVs commonly accumulate stray
|
|
28
|
+
* spaces around values, and untrimmed comparisons would mark every such
|
|
29
|
+
* row as a manual-override conflict on every classifier run.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} line
|
|
32
|
+
* @returns {string[]}
|
|
33
|
+
*/
|
|
34
|
+
function parseCsvRow(line) {
|
|
35
|
+
// Strip trailing CR for CRLF tolerance
|
|
36
|
+
if (line.endsWith('\r')) line = line.slice(0, -1);
|
|
37
|
+
|
|
38
|
+
const fields = [];
|
|
39
|
+
let current = '';
|
|
40
|
+
let inQuotes = false;
|
|
41
|
+
for (let i = 0; i < line.length; i++) {
|
|
42
|
+
const ch = line[i];
|
|
43
|
+
if (ch === '"') {
|
|
44
|
+
// RFC-4180: doubled quote inside a quoted field is a literal "
|
|
45
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
46
|
+
current += '"';
|
|
47
|
+
i++;
|
|
48
|
+
} else {
|
|
49
|
+
inQuotes = !inQuotes;
|
|
50
|
+
}
|
|
51
|
+
} else if (ch === ',' && !inQuotes) {
|
|
52
|
+
fields.push(current);
|
|
53
|
+
current = '';
|
|
54
|
+
} else {
|
|
55
|
+
current += ch;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
fields.push(current);
|
|
59
|
+
// P2 (sp-1-2 review): trim whitespace from every field. Fields that were
|
|
60
|
+
// originally quoted are unaffected by this since the quotes are already
|
|
61
|
+
// stripped during parse and any internal padding is preserved unless it
|
|
62
|
+
// was actually outside the quotes.
|
|
63
|
+
return fields.map((f) => f.trim());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Count the number of CSV columns in a row (RFC-4180-aware).
|
|
68
|
+
*
|
|
69
|
+
* @param {string} line
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
function countCsvColumns(line) {
|
|
73
|
+
return parseCsvRow(line).length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format a single field for CSV output. Quotes the field if it contains
|
|
78
|
+
* a comma, double-quote, or newline. Escapes embedded quotes by doubling.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} field
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function formatCsvField(field) {
|
|
84
|
+
if (field == null) return '';
|
|
85
|
+
const s = String(field);
|
|
86
|
+
if (s.includes(',') || s.includes('"') || s.includes('\n') || s.includes('\r')) {
|
|
87
|
+
return '"' + s.replace(/"/g, '""') + '"';
|
|
88
|
+
}
|
|
89
|
+
return s;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Format an array of fields as a CSV row (no trailing newline).
|
|
94
|
+
*
|
|
95
|
+
* @param {string[]} fields
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
function formatCsvRow(fields) {
|
|
99
|
+
return fields.map(formatCsvField).join(',');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read a CSV file from disk and return its parsed contents.
|
|
104
|
+
* Strips UTF-8 BOM and ignores blank/whitespace-only lines.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} filePath
|
|
107
|
+
* @returns {{ header: string[], rows: string[][], rawLines: string[] }}
|
|
108
|
+
* - `header`: array of column names
|
|
109
|
+
* - `rows`: array of data rows (each an array of fields)
|
|
110
|
+
* - `rawLines`: original (post-BOM-strip, post-filter) line strings — useful
|
|
111
|
+
* when callers need to preserve exact whitespace/quoting on rows they
|
|
112
|
+
* don't intend to modify
|
|
113
|
+
*/
|
|
114
|
+
function readManifest(filePath) {
|
|
115
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
116
|
+
// Strip UTF-8 BOM if present
|
|
117
|
+
if (content.charCodeAt(0) === 0xfeff) content = content.slice(1);
|
|
118
|
+
|
|
119
|
+
const rawLines = content.split('\n').filter((l) => /\S/.test(l));
|
|
120
|
+
if (rawLines.length === 0) {
|
|
121
|
+
return { header: [], rows: [], rawLines: [] };
|
|
122
|
+
}
|
|
123
|
+
const header = parseCsvRow(rawLines[0]);
|
|
124
|
+
const rows = rawLines.slice(1).map(parseCsvRow);
|
|
125
|
+
return { header, rows, rawLines };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Write a manifest back to disk. Joins rows with `\n` and adds a trailing newline.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} filePath
|
|
132
|
+
* @param {string[]} header
|
|
133
|
+
* @param {string[][]} rows
|
|
134
|
+
*/
|
|
135
|
+
function writeManifest(filePath, header, rows) {
|
|
136
|
+
const lines = [formatCsvRow(header), ...rows.map(formatCsvRow)];
|
|
137
|
+
fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf8');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
parseCsvRow,
|
|
142
|
+
countCsvColumns,
|
|
143
|
+
formatCsvField,
|
|
144
|
+
formatCsvRow,
|
|
145
|
+
readManifest,
|
|
146
|
+
writeManifest,
|
|
147
|
+
};
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* seed-catalog-repo.js — Story sp-4-1
|
|
4
|
+
*
|
|
5
|
+
* Generates the complete convoke-skills-catalog repo content into a staging
|
|
6
|
+
* directory. Orchestrates the export engine, catalog generator, and README
|
|
7
|
+
* builder into a single pipeline with built-in self-verification.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/portability/seed-catalog-repo.js --output <path>
|
|
11
|
+
* node scripts/portability/seed-catalog-repo.js --help
|
|
12
|
+
*
|
|
13
|
+
* The script does NOT create a git repo or interact with GitHub.
|
|
14
|
+
* It only produces a directory tree. The user handles git init + push.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { findProjectRoot } = require('../update/lib/utils');
|
|
22
|
+
const { readManifest } = require('./manifest-csv');
|
|
23
|
+
const { exportSkill, loadSkillRow } = require('./export-engine');
|
|
24
|
+
const { generateCatalog } = require('./catalog-generator');
|
|
25
|
+
const { buildReadme } = require('./convoke-export');
|
|
26
|
+
const { generateAdapters } = require('./generate-adapters');
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// CONSTANTS
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
const { FORBIDDEN_STRINGS } = require('./test-constants');
|
|
33
|
+
|
|
34
|
+
const MIT_LICENSE = `MIT License
|
|
35
|
+
|
|
36
|
+
Copyright (c) 2026 Convoke Contributors
|
|
37
|
+
|
|
38
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
39
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
40
|
+
in the Software without restriction, including without limitation the rights
|
|
41
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
42
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
43
|
+
furnished to do so, subject to the following conditions:
|
|
44
|
+
|
|
45
|
+
The above copyright notice and this permission notice shall be included in all
|
|
46
|
+
copies or substantial portions of the Software.
|
|
47
|
+
|
|
48
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
49
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
50
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
51
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
52
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
53
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
54
|
+
SOFTWARE.
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const CONTRIBUTING_MD = `# Contributing
|
|
58
|
+
|
|
59
|
+
The skills in this repository are **auto-generated** from the main [Convoke Agents](https://github.com/amalik/convoke-agents) repository. They are regenerated on each release.
|
|
60
|
+
|
|
61
|
+
## Important
|
|
62
|
+
|
|
63
|
+
- **Do not edit skill files directly.** Your changes will be overwritten on the next regeneration.
|
|
64
|
+
- To improve a skill's content, contribute to the source skill in the main Convoke repo.
|
|
65
|
+
- To request a new skill or report an issue, [open an issue](https://github.com/amalik/convoke-agents/issues) on the main repo.
|
|
66
|
+
|
|
67
|
+
## How skills are generated
|
|
68
|
+
|
|
69
|
+
Each skill is exported from the Convoke framework using the \`convoke-export\` tool, which:
|
|
70
|
+
|
|
71
|
+
1. Reads the skill's source files (agent definition, workflow, step files)
|
|
72
|
+
2. Transforms them into an LLM-agnostic \`instructions.md\` format
|
|
73
|
+
3. Generates a per-skill \`README.md\` with install instructions for Claude Code, GitHub Copilot, and Cursor
|
|
74
|
+
4. Produces this catalog \`README.md\` organized by user intent
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
This repository is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// GENERATION PIPELINE
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
function generate(outputDir, projectRoot) {
|
|
86
|
+
const manifestPath = path.join(projectRoot, '_bmad', '_config', 'skill-manifest.csv');
|
|
87
|
+
const { header, rows } = readManifest(manifestPath);
|
|
88
|
+
const nameIdx = header.indexOf('name');
|
|
89
|
+
const tierIdx = header.indexOf('tier');
|
|
90
|
+
|
|
91
|
+
// Get unique exportable skill names (standalone + light-deps)
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
const exportableNames = [];
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const name = row[nameIdx];
|
|
96
|
+
if (seen.has(name)) continue;
|
|
97
|
+
seen.add(name);
|
|
98
|
+
if (row[tierIdx] === 'standalone' || row[tierIdx] === 'light-deps') {
|
|
99
|
+
exportableNames.push(name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exportableNames.sort();
|
|
103
|
+
|
|
104
|
+
console.log(`Exporting ${exportableNames.length} skills (standalone + light-deps)...`);
|
|
105
|
+
|
|
106
|
+
// Create output directory
|
|
107
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
// Export each skill
|
|
110
|
+
let exportedCount = 0;
|
|
111
|
+
const failures = [];
|
|
112
|
+
for (const skillName of exportableNames) {
|
|
113
|
+
try {
|
|
114
|
+
const result = exportSkill(skillName, projectRoot);
|
|
115
|
+
const skillRow = loadSkillRow(skillName, projectRoot);
|
|
116
|
+
const readme = buildReadme(skillRow, result, projectRoot);
|
|
117
|
+
|
|
118
|
+
const skillDir = path.join(outputDir, skillName);
|
|
119
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
120
|
+
fs.writeFileSync(path.join(skillDir, 'instructions.md'), result.instructions);
|
|
121
|
+
fs.writeFileSync(path.join(skillDir, 'README.md'), readme);
|
|
122
|
+
generateAdapters(skillName, skillRow, result.instructions, skillDir);
|
|
123
|
+
exportedCount++;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
failures.push({ skill: skillName, error: err.message.split('\n')[0] });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (failures.length > 0) {
|
|
130
|
+
console.error(`${failures.length} skill(s) failed to export:`);
|
|
131
|
+
for (const f of failures) {
|
|
132
|
+
console.error(` ${f.skill}: ${f.error}`);
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`${failures.length} skill export(s) failed`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(`Exported ${exportedCount} skills successfully.`);
|
|
138
|
+
|
|
139
|
+
// Generate catalog README
|
|
140
|
+
console.log('Generating catalog README...');
|
|
141
|
+
const catalogReadme = generateCatalog(projectRoot);
|
|
142
|
+
fs.writeFileSync(path.join(outputDir, 'README.md'), catalogReadme);
|
|
143
|
+
|
|
144
|
+
// Write LICENSE
|
|
145
|
+
fs.writeFileSync(path.join(outputDir, 'LICENSE'), MIT_LICENSE);
|
|
146
|
+
|
|
147
|
+
// Write CONTRIBUTING.md
|
|
148
|
+
fs.writeFileSync(path.join(outputDir, 'CONTRIBUTING.md'), CONTRIBUTING_MD);
|
|
149
|
+
|
|
150
|
+
console.log('Generation complete.');
|
|
151
|
+
return { skillCount: exportedCount, expectedCount: exportableNames.length };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// =============================================================================
|
|
155
|
+
// SELF-VERIFICATION
|
|
156
|
+
// =============================================================================
|
|
157
|
+
|
|
158
|
+
function verify(outputDir, expectedSkillCount) {
|
|
159
|
+
const failures = [];
|
|
160
|
+
|
|
161
|
+
// 1. Directory count
|
|
162
|
+
const entries = fs.readdirSync(outputDir, { withFileTypes: true });
|
|
163
|
+
const skillDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
164
|
+
if (skillDirs.length !== expectedSkillCount) {
|
|
165
|
+
failures.push(`Directory count: expected ${expectedSkillCount}, got ${skillDirs.length}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. Every skill dir has both files
|
|
169
|
+
for (const dir of skillDirs) {
|
|
170
|
+
const instrPath = path.join(outputDir, dir, 'instructions.md');
|
|
171
|
+
const readmePath = path.join(outputDir, dir, 'README.md');
|
|
172
|
+
if (!fs.existsSync(instrPath)) {
|
|
173
|
+
failures.push(`${dir}: missing instructions.md`);
|
|
174
|
+
}
|
|
175
|
+
if (!fs.existsSync(readmePath)) {
|
|
176
|
+
failures.push(`${dir}: missing README.md`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 3. Zero forbidden strings in all instructions.md
|
|
181
|
+
for (const dir of skillDirs) {
|
|
182
|
+
const instrPath = path.join(outputDir, dir, 'instructions.md');
|
|
183
|
+
if (!fs.existsSync(instrPath)) continue;
|
|
184
|
+
const content = fs.readFileSync(instrPath, 'utf8');
|
|
185
|
+
for (const forbidden of FORBIDDEN_STRINGS) {
|
|
186
|
+
if (content.includes(forbidden)) {
|
|
187
|
+
failures.push(`${dir}/instructions.md: contains "${forbidden}"`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 4. README validity (How to use, 3 platforms, under 80 lines)
|
|
193
|
+
for (const dir of skillDirs) {
|
|
194
|
+
const readmePath = path.join(outputDir, dir, 'README.md');
|
|
195
|
+
if (!fs.existsSync(readmePath)) continue;
|
|
196
|
+
const content = fs.readFileSync(readmePath, 'utf8');
|
|
197
|
+
if (!content.includes('How to use')) {
|
|
198
|
+
failures.push(`${dir}/README.md: missing "How to use"`);
|
|
199
|
+
}
|
|
200
|
+
if (!content.includes('Claude Code')) {
|
|
201
|
+
failures.push(`${dir}/README.md: missing Claude Code section`);
|
|
202
|
+
}
|
|
203
|
+
if (!content.includes('Copilot')) {
|
|
204
|
+
failures.push(`${dir}/README.md: missing Copilot section`);
|
|
205
|
+
}
|
|
206
|
+
if (!content.includes('Cursor')) {
|
|
207
|
+
failures.push(`${dir}/README.md: missing Cursor section`);
|
|
208
|
+
}
|
|
209
|
+
const lineCount = content.split('\n').length;
|
|
210
|
+
if (lineCount > 80) {
|
|
211
|
+
failures.push(`${dir}/README.md: ${lineCount} lines (exceeds 80)`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 4b. Adapters exist for each skill
|
|
216
|
+
for (const dir of skillDirs) {
|
|
217
|
+
const adaptersBase = path.join(outputDir, dir, 'adapters');
|
|
218
|
+
if (!fs.existsSync(path.join(adaptersBase, 'claude-code', 'SKILL.md'))) {
|
|
219
|
+
failures.push(`${dir}: missing adapters/claude-code/SKILL.md`);
|
|
220
|
+
}
|
|
221
|
+
if (!fs.existsSync(path.join(adaptersBase, 'copilot', 'copilot-instructions.md'))) {
|
|
222
|
+
failures.push(`${dir}: missing adapters/copilot/copilot-instructions.md`);
|
|
223
|
+
}
|
|
224
|
+
if (!fs.existsSync(path.join(adaptersBase, 'cursor', `${dir}.md`))) {
|
|
225
|
+
failures.push(`${dir}: missing adapters/cursor/${dir}.md`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 5. Zero _bmad/ or .claude/hooks in ANY file
|
|
230
|
+
const allFiles = [];
|
|
231
|
+
function collectMdFiles(dir) {
|
|
232
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
233
|
+
const full = path.join(dir, entry.name);
|
|
234
|
+
if (entry.isDirectory()) {
|
|
235
|
+
collectMdFiles(full);
|
|
236
|
+
} else if (entry.name.endsWith('.md')) {
|
|
237
|
+
allFiles.push(full);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
collectMdFiles(outputDir);
|
|
242
|
+
for (const filePath of allFiles) {
|
|
243
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
244
|
+
const rel = path.relative(outputDir, filePath);
|
|
245
|
+
if (content.includes('_bmad/')) {
|
|
246
|
+
failures.push(`${rel}: contains "_bmad/"`);
|
|
247
|
+
}
|
|
248
|
+
if (content.includes('.claude/hooks')) {
|
|
249
|
+
failures.push(`${rel}: contains ".claude/hooks"`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 6. Root README
|
|
254
|
+
const rootReadme = path.join(outputDir, 'README.md');
|
|
255
|
+
if (!fs.existsSync(rootReadme)) {
|
|
256
|
+
failures.push('Root README.md missing');
|
|
257
|
+
} else {
|
|
258
|
+
const content = fs.readFileSync(rootReadme, 'utf8');
|
|
259
|
+
if (!content.includes('# Convoke Skills Catalog')) {
|
|
260
|
+
failures.push('Root README.md: missing "# Convoke Skills Catalog" heading');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 7. LICENSE
|
|
265
|
+
const licensePath = path.join(outputDir, 'LICENSE');
|
|
266
|
+
if (!fs.existsSync(licensePath)) {
|
|
267
|
+
failures.push('LICENSE missing');
|
|
268
|
+
} else {
|
|
269
|
+
const content = fs.readFileSync(licensePath, 'utf8');
|
|
270
|
+
if (!content.includes('MIT')) {
|
|
271
|
+
failures.push('LICENSE: does not contain "MIT"');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 8. CONTRIBUTING.md
|
|
276
|
+
const contribPath = path.join(outputDir, 'CONTRIBUTING.md');
|
|
277
|
+
if (!fs.existsSync(contribPath)) {
|
|
278
|
+
failures.push('CONTRIBUTING.md missing');
|
|
279
|
+
} else {
|
|
280
|
+
const content = fs.readFileSync(contribPath, 'utf8');
|
|
281
|
+
if (!content.includes('auto-generated')) {
|
|
282
|
+
failures.push('CONTRIBUTING.md: does not contain "auto-generated"');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return failures;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// =============================================================================
|
|
290
|
+
// CLI
|
|
291
|
+
// =============================================================================
|
|
292
|
+
|
|
293
|
+
function printHelp() {
|
|
294
|
+
process.stdout.write(
|
|
295
|
+
[
|
|
296
|
+
'Usage: seed-catalog-repo --output <path>',
|
|
297
|
+
'',
|
|
298
|
+
'Generate the complete convoke-skills-catalog repo content into a staging directory.',
|
|
299
|
+
'Does NOT create a git repo or interact with GitHub.',
|
|
300
|
+
'',
|
|
301
|
+
'Options:',
|
|
302
|
+
' --output <path> Staging directory path (required)',
|
|
303
|
+
' --help, -h Show this help message',
|
|
304
|
+
'',
|
|
305
|
+
'Exit codes:',
|
|
306
|
+
' 0 Success (all skills exported, verification passed)',
|
|
307
|
+
' 1 Usage error',
|
|
308
|
+
' 2 Generation failure',
|
|
309
|
+
' 3 Verification failure',
|
|
310
|
+
'',
|
|
311
|
+
'After the staging directory is generated, you can create the repo manually:',
|
|
312
|
+
'',
|
|
313
|
+
' cd <staging-dir>',
|
|
314
|
+
' git init',
|
|
315
|
+
' git add -A',
|
|
316
|
+
' git commit -m "Initial catalog seed"',
|
|
317
|
+
' gh repo create convoke-skills-catalog --public --source=. --push',
|
|
318
|
+
'',
|
|
319
|
+
].join('\n')
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function main() {
|
|
324
|
+
const argv = process.argv.slice(2);
|
|
325
|
+
|
|
326
|
+
if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) {
|
|
327
|
+
printHelp();
|
|
328
|
+
return argv.length === 0 ? 1 : 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let outputDir = null;
|
|
332
|
+
for (let i = 0; i < argv.length; i++) {
|
|
333
|
+
if (argv[i] === '--output') {
|
|
334
|
+
const next = argv[i + 1];
|
|
335
|
+
if (!next || next.startsWith('--')) {
|
|
336
|
+
process.stderr.write('Error: --output requires a path argument\n');
|
|
337
|
+
return 1;
|
|
338
|
+
}
|
|
339
|
+
outputDir = argv[++i];
|
|
340
|
+
} else if (argv[i].startsWith('--')) {
|
|
341
|
+
process.stderr.write(`Unknown flag: ${argv[i]}. Run --help for usage.\n`);
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!outputDir) {
|
|
347
|
+
process.stderr.write('Error: --output <path> is required. Run --help for usage.\n');
|
|
348
|
+
return 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Resolve output path
|
|
352
|
+
outputDir = path.isAbsolute(outputDir) ? outputDir : path.resolve(process.cwd(), outputDir);
|
|
353
|
+
|
|
354
|
+
let projectRoot;
|
|
355
|
+
try {
|
|
356
|
+
projectRoot = findProjectRoot();
|
|
357
|
+
} catch (e) {
|
|
358
|
+
process.stderr.write(`Error: could not find project root — ${e.message}\n`);
|
|
359
|
+
return 2;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Safety: refuse to write into a non-empty pre-existing directory.
|
|
363
|
+
// Prevents destructive cleanup from deleting user content on failure.
|
|
364
|
+
if (fs.existsSync(outputDir)) {
|
|
365
|
+
const existing = fs.readdirSync(outputDir);
|
|
366
|
+
if (existing.length > 0) {
|
|
367
|
+
process.stderr.write(`Error: output directory "${outputDir}" already exists and is non-empty.\nUse an empty or non-existent path.\n`);
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const dirCreatedByUs = !fs.existsSync(outputDir);
|
|
372
|
+
|
|
373
|
+
// Generate
|
|
374
|
+
let genResult;
|
|
375
|
+
try {
|
|
376
|
+
genResult = generate(outputDir, projectRoot);
|
|
377
|
+
} catch (e) {
|
|
378
|
+
process.stderr.write(`Generation failed: ${e.message}\n`);
|
|
379
|
+
// Only clean up if WE created the directory
|
|
380
|
+
if (dirCreatedByUs && fs.existsSync(outputDir)) {
|
|
381
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
382
|
+
}
|
|
383
|
+
return 2;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Verify
|
|
387
|
+
let failures;
|
|
388
|
+
try {
|
|
389
|
+
console.log('Running verification...');
|
|
390
|
+
failures = verify(outputDir, genResult.expectedCount);
|
|
391
|
+
} catch (e) {
|
|
392
|
+
process.stderr.write(`Verification crashed: ${e.message}\n`);
|
|
393
|
+
if (dirCreatedByUs && fs.existsSync(outputDir)) {
|
|
394
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
395
|
+
}
|
|
396
|
+
return 3;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (failures.length > 0) {
|
|
400
|
+
process.stderr.write(`Verification failed with ${failures.length} issue(s):\n`);
|
|
401
|
+
for (const f of failures) {
|
|
402
|
+
process.stderr.write(` - ${f}\n`);
|
|
403
|
+
}
|
|
404
|
+
if (dirCreatedByUs) {
|
|
405
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
406
|
+
}
|
|
407
|
+
return 3;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
console.log(`\nCatalog staging complete!`);
|
|
411
|
+
console.log(` Skills: ${genResult.skillCount}`);
|
|
412
|
+
console.log(` Directory: ${outputDir}`);
|
|
413
|
+
console.log(` Files: ${genResult.skillCount * 2 + 3} (${genResult.skillCount} x instructions.md + README.md + catalog + LICENSE + CONTRIBUTING.md)`);
|
|
414
|
+
console.log(` Verification: PASSED (zero violations)`);
|
|
415
|
+
console.log(`\nNext steps:`);
|
|
416
|
+
console.log(` cd ${outputDir}`);
|
|
417
|
+
console.log(` git init && git add -A && git commit -m "Initial catalog seed"`);
|
|
418
|
+
console.log(` gh repo create convoke-skills-catalog --public --source=. --push`);
|
|
419
|
+
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (require.main === module) {
|
|
424
|
+
process.exit(main());
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
module.exports = { generate, verify, main };
|