Compare commits

..

9 Commits

Author SHA1 Message Date
PinkyD cf1eb081a1
Merge 503a9f8024 into db2270c7ea 2026-06-21 15:18:07 -07:00
pbean 503a9f8024 chore(bmad-module): regenerate vendored bundles for esbuild 0.28.1
main #2493 (astro 6 upgrade) added `overrides: {esbuild: ^0.28.1}` to pin
esbuild for a security fix. The vendored yaml.mjs and ide-sync.mjs bundles
are byte-compared in CI against a fresh esbuild rebuild, so the embedded
esbuild version must match. The branch merge of main brought the override
but left the bundles built with 0.25.12; regenerated both with 0.28.1 so
vendor:check passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:17:45 -07:00
PinkyD 1715dfbc48
Merge branch 'main' into feat/bmad-marketplace-plugin 2026-06-21 15:08:12 -07:00
Brian db2270c7ea
fix(deps): resolve Dependabot security alerts via astro 6 upgrade (#2493)
Clears all 11 open Dependabot alerts on main:

- astro 5.18.1 -> 6.4.6, @astrojs/starlight 0.37.5 -> 0.40.0,
  @astrojs/sitemap 3.6.0 -> 3.7.3 (8 XSS/SSRF advisories)
- esbuild pinned to 0.28.1 via override (astro/vite cap at ^0.27;
  fixes dev-server arbitrary file read on Windows)
- markdown-it -> 14.2.0 via override (smartquotes ReDoS)
- brace-expansion (under glob) -> 5.0.6 (range DoS)

Astro 6 migration for the docs site:
- content config moved to src/content.config.ts with loaders
- sidebar autogenerate groups wrapped in items[] (Starlight v0.39)
- 404 page uses render(entry) instead of entry.render()

Verified: docs:build produces an identical page set vs the
pre-upgrade baseline; sidebar validation and format checks pass.
2026-06-21 16:52:15 -05:00
pbean 244deb849d chore(bmad-module): regenerate vendored platform-codes after main sync
Merge of main brought the hermes-agent IDE target (#2489) into
tools/installer/ide/platform-codes.yaml. Regenerate the vendored sidecar so
vendor:check matches the engine in the PR's merge-with-main tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 11:42:34 -07:00
pbean f8305b531f Merge remote-tracking branch 'origin/main' into feat/bmad-marketplace-plugin 2026-06-21 11:42:13 -07:00
pbean 962e9a068a chore(bmad-module): diagnose vendor:check ide-sync drift in CI
Print committed-vs-fresh lengths and the first differing window on a
vendor:check mismatch so a CI-only drift (passes locally, fails in CI) is
actionable from the log. Temporary diagnostic to locate the divergence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 11:40:05 -07:00
pbean ce314ba600 fix(bmad-module): address review — backup safety, legacy update, help exit, TOML unescape
- fs-safe: keep the backup dir on a failed swap+rollback (it's the only
  surviving copy of the previous install) and report its path instead of
  deleting it.
- update: handle legacy marketplace.json modules the same way install does
  (hasBmadPluginJson branch + resolveLegacyModule + synthesized staging) so
  legacy-installed community modules are no longer un-updatable.
- cli: explicit --help/-h exits 0; bare no-args invocation keeps exit 2.
- config-gen: unescape TOML scalars in a single left-to-right pass so a
  literal backslash before n/r/t (e.g. a Windows path \new) round-trips
  intact; export parseTomlScalar and add round-trip regression tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:18:29 -07:00
Larry Lewis e600181ab8
feat(installer): add hermes-agent tool target (#2489)
Co-authored-by: bigblackcoder <llewis@sovrlabs.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2026-06-20 19:53:35 -05:00
16 changed files with 928 additions and 1476 deletions

2161
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -71,6 +71,12 @@
"markdownlint-cli2" "markdownlint-cli2"
] ]
}, },
"overrides": {
"esbuild": "^0.28.1",
"markdownlint-cli2": {
"markdown-it": "^14.2.0"
}
},
"dependencies": { "dependencies": {
"@clack/core": "^1.3.1", "@clack/core": "^1.3.1",
"@clack/prompts": "^1.4.0", "@clack/prompts": "^1.4.0",
@ -87,10 +93,10 @@
"yaml": "^2.7.0" "yaml": "^2.7.0"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/sitemap": "^3.6.0", "@astrojs/sitemap": "^3.7.3",
"@astrojs/starlight": "^0.37.5", "@astrojs/starlight": "^0.40.0",
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",
"astro": "^5.16.0", "astro": "^6.4.6",
"c8": "^10.1.3", "c8": "^10.1.3",
"eslint": "^9.33.0", "eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",

View File

@ -62,7 +62,13 @@ function parseArgs(argv) {
export async function main() { export async function main() {
const argv = process.argv.slice(2); const argv = process.argv.slice(2);
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') { // An explicit help request is a successful invocation → exit 0. A bare call
// with no verb is an incomplete usage → EXIT.USAGE (2).
if (argv[0] === '--help' || argv[0] === '-h') {
printUsage();
process.exit(0);
}
if (argv.length === 0) {
printUsage(); printUsage();
process.exit(EXIT.USAGE); process.exit(EXIT.USAGE);
} }

View File

@ -39,19 +39,29 @@ export function formatTomlValue(value) {
} }
// Minimal reverse of formatTomlValue for the scalars we read back (core values). // Minimal reverse of formatTomlValue for the scalars we read back (core values).
function parseTomlScalar(raw) { // Exported for round-trip unit tests (test/test-bmad-module-source.mjs).
export function parseTomlScalar(raw) {
const s = raw.trim(); const s = raw.trim();
if (s === 'true') return true; if (s === 'true') return true;
if (s === 'false') return false; if (s === 'false') return false;
if (/^-?\d+(\.\d+)?$/.test(s)) return Number(s); if (/^-?\d+(\.\d+)?$/.test(s)) return Number(s);
if (s.startsWith('"') && s.endsWith('"')) { if (s.startsWith('"') && s.endsWith('"')) {
return s // Single left-to-right pass: each backslash consumes exactly one following
.slice(1, -1) // char. A chained replaceAll would mis-handle round-tripped literal
.replaceAll('\\n', '\n') // backslashes (e.g. `\\n` → `\` + newline instead of `\` + `n`), since an
.replaceAll('\\r', '\r') // earlier pass can rewrite the escape introduced by a later one. The escape
.replaceAll('\\t', '\t') // set here is the exact inverse of formatTomlValue (\\ \" \n \r \t).
.replaceAll('\\"', '"') const inner = s.slice(1, -1);
.replaceAll('\\\\', '\\'); let out = '';
for (let i = 0; i < inner.length; i++) {
if (inner[i] === '\\' && i + 1 < inner.length) {
const n = inner[++i];
out += n === 'n' ? '\n' : n === 'r' ? '\r' : n === 't' ? '\t' : n === '"' ? '"' : n === '\\' ? '\\' : '\\' + n;
} else {
out += inner[i];
}
}
return out;
} }
return s; return s;
} }

View File

@ -126,7 +126,16 @@ export async function atomicSwapDir(stagedDir, targetDir) {
if (hadTarget) await fsp.rm(backup, { recursive: true, force: true }); if (hadTarget) await fsp.rm(backup, { recursive: true, force: true });
} catch (e) { } catch (e) {
await fsp.rm(sibling, { recursive: true, force: true }); await fsp.rm(sibling, { recursive: true, force: true });
await fsp.rm(backup, { recursive: true, force: true }); // Keep `backup` if it still exists — on a failed swap+rollback it is the
// only surviving copy of the previous install. Surface its location so the
// user can recover manually rather than silently destroying it here.
const backupSurvives = await fsp
.stat(backup)
.then(() => true)
.catch(() => false);
if (backupSurvives) {
process.stderr.write(`[bmad-module] previous install preserved at ${backup}\n`);
}
throw e; throw e;
} }
} }

View File

@ -140,6 +140,10 @@ if (checkMode) {
` The committed bundle no longer matches tools/installer/ide/*.\n` + ` The committed bundle no longer matches tools/installer/ide/*.\n` +
` Fix: run \`npm run vendor:build\` and commit the regenerated files.\n`, ` Fix: run \`npm run vendor:build\` and commit the regenerated files.\n`,
); );
// Pinpoint the first divergence so a CI-only mismatch is diagnosable from the
// log instead of just "they differ".
reportFirstDiff('ide-sync.mjs', currentBundle, built);
reportFirstDiff('platform-codes.yaml', currentSidecar, sidecar);
process.exit(1); process.exit(1);
} }
@ -149,6 +153,26 @@ process.stdout.write(`built ide-sync.mjs + platform-codes.yaml (self-check OK, e
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// On a --check mismatch, print the committed vs freshly-built lengths and a
// window around the first differing character. Keeps CI logs actionable when a
// drift is environment-specific and can't be reproduced locally.
function reportFirstDiff(label, committed, fresh) {
if (committed === fresh) return;
if (committed == null) {
process.stderr.write(` [diff] ${label}: committed file missing\n`);
return;
}
const n = Math.min(committed.length, fresh.length);
let i = 0;
while (i < n && committed[i] === fresh[i]) i++;
const win = (s) => JSON.stringify(s.slice(Math.max(0, i - 40), i + 40));
process.stderr.write(
` [diff] ${label}: committed=${committed.length}B fresh=${fresh.length}B firstDiff@${i}\n` +
` committed: ${win(committed)}\n` +
` fresh : ${win(fresh)}\n`,
);
}
async function selfCheck(bundleText, sidecarText) { async function selfCheck(bundleText, sidecarText) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-ide-sync-check-')); const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-ide-sync-check-'));
try { try {

View File

@ -3,7 +3,7 @@
// Self-contained bundle of BMAD's IDE-distribution engine // Self-contained bundle of BMAD's IDE-distribution engine
// (tools/installer/ide/* via tools/installer/core/ide-sync.js). // (tools/installer/ide/* via tools/installer/core/ide-sync.js).
// //
// bundler : esbuild 0.25.12 // bundler : esbuild 0.28.1
// yaml : 2.8.4 // yaml : 2.8.4
// csv-parse : 6.1.0 // csv-parse : 6.1.0
// //
@ -30,7 +30,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
throw Error('Dynamic require of "' + x + '" is not supported'); throw Error('Dynamic require of "' + x + '" is not supported');
}); });
var __commonJS = (cb, mod) => function __require2() { var __commonJS = (cb, mod) => function __require2() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; try {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
} catch (e) {
throw mod = 0, e;
}
}; };
var __copyProps = (to, from, except, desc) => { var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") { if (from && typeof from === "object" || typeof from === "function") {
@ -332,13 +336,13 @@ var require_prompts = __commonJS({
var require_identity = __commonJS({ var require_identity = __commonJS({
"node_modules/yaml/dist/nodes/identity.js"(exports) { "node_modules/yaml/dist/nodes/identity.js"(exports) {
"use strict"; "use strict";
var ALIAS = Symbol.for("yaml.alias"); var ALIAS = /* @__PURE__ */ Symbol.for("yaml.alias");
var DOC = Symbol.for("yaml.document"); var DOC = /* @__PURE__ */ Symbol.for("yaml.document");
var MAP = Symbol.for("yaml.map"); var MAP = /* @__PURE__ */ Symbol.for("yaml.map");
var PAIR = Symbol.for("yaml.pair"); var PAIR = /* @__PURE__ */ Symbol.for("yaml.pair");
var SCALAR = Symbol.for("yaml.scalar"); var SCALAR = /* @__PURE__ */ Symbol.for("yaml.scalar");
var SEQ = Symbol.for("yaml.seq"); var SEQ = /* @__PURE__ */ Symbol.for("yaml.seq");
var NODE_TYPE = Symbol.for("yaml.node.type"); var NODE_TYPE = /* @__PURE__ */ Symbol.for("yaml.node.type");
var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS; var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC; var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP; var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
@ -390,9 +394,9 @@ var require_visit = __commonJS({
"node_modules/yaml/dist/visit.js"(exports) { "node_modules/yaml/dist/visit.js"(exports) {
"use strict"; "use strict";
var identity = require_identity(); var identity = require_identity();
var BREAK = Symbol("break visit"); var BREAK = /* @__PURE__ */ Symbol("break visit");
var SKIP = Symbol("skip children"); var SKIP = /* @__PURE__ */ Symbol("skip children");
var REMOVE = Symbol("remove node"); var REMOVE = /* @__PURE__ */ Symbol("remove node");
function visit(node, visitor) { function visit(node, visitor) {
const visitor_ = initVisitor(visitor); const visitor_ = initVisitor(visitor);
if (identity.isDocument(node)) { if (identity.isDocument(node)) {
@ -5850,9 +5854,9 @@ var require_cst_stringify = __commonJS({
var require_cst_visit = __commonJS({ var require_cst_visit = __commonJS({
"node_modules/yaml/dist/parse/cst-visit.js"(exports) { "node_modules/yaml/dist/parse/cst-visit.js"(exports) {
"use strict"; "use strict";
var BREAK = Symbol("break visit"); var BREAK = /* @__PURE__ */ Symbol("break visit");
var SKIP = Symbol("skip children"); var SKIP = /* @__PURE__ */ Symbol("skip children");
var REMOVE = Symbol("remove item"); var REMOVE = /* @__PURE__ */ Symbol("remove item");
function visit(cst, visitor) { function visit(cst, visitor) {
if ("type" in cst && cst.type === "document") if ("type" in cst && cst.type === "document")
cst = { start: cst.start, value: cst.value }; cst = { start: cst.start, value: cst.value };

View File

@ -162,6 +162,13 @@ platforms:
target_dir: .agents/skills target_dir: .agents/skills
global_target_dir: ~/.config/agents/skills global_target_dir: ~/.config/agents/skills
hermes:
name: "Hermes Agent"
preferred: false
installer:
target_dir: .agents/skills
global_target_dir: ~/.hermes/skills
iflow: iflow:
name: "iFlow" name: "iFlow"
preferred: false preferred: false

View File

@ -3,7 +3,7 @@
// Vendored, self-contained bundle of the `yaml` npm package (eemeli/yaml). // Vendored, self-contained bundle of the `yaml` npm package (eemeli/yaml).
// //
// yaml : 2.8.4 // yaml : 2.8.4
// bundler : esbuild 0.25.12 // bundler : esbuild 0.28.1
// //
// Shipped because the skill is copied into projects without node_modules; see // Shipped because the skill is copied into projects without node_modules; see
// build-vendor.mjs and vendor/README.md for the rationale. Only `parse` and // build-vendor.mjs and vendor/README.md for the rationale. Only `parse` and
@ -25,7 +25,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
throw Error('Dynamic require of "' + x + '" is not supported'); throw Error('Dynamic require of "' + x + '" is not supported');
}); });
var __commonJS = (cb, mod) => function __require2() { var __commonJS = (cb, mod) => function __require2() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; try {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
} catch (e) {
throw mod = 0, e;
}
}; };
var __copyProps = (to, from, except, desc) => { var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") { if (from && typeof from === "object" || typeof from === "function") {
@ -48,13 +52,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
var require_identity = __commonJS({ var require_identity = __commonJS({
"node_modules/yaml/dist/nodes/identity.js"(exports) { "node_modules/yaml/dist/nodes/identity.js"(exports) {
"use strict"; "use strict";
var ALIAS = Symbol.for("yaml.alias"); var ALIAS = /* @__PURE__ */ Symbol.for("yaml.alias");
var DOC = Symbol.for("yaml.document"); var DOC = /* @__PURE__ */ Symbol.for("yaml.document");
var MAP = Symbol.for("yaml.map"); var MAP = /* @__PURE__ */ Symbol.for("yaml.map");
var PAIR = Symbol.for("yaml.pair"); var PAIR = /* @__PURE__ */ Symbol.for("yaml.pair");
var SCALAR = Symbol.for("yaml.scalar"); var SCALAR = /* @__PURE__ */ Symbol.for("yaml.scalar");
var SEQ = Symbol.for("yaml.seq"); var SEQ = /* @__PURE__ */ Symbol.for("yaml.seq");
var NODE_TYPE = Symbol.for("yaml.node.type"); var NODE_TYPE = /* @__PURE__ */ Symbol.for("yaml.node.type");
var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS; var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC; var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP; var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
@ -106,9 +110,9 @@ var require_visit = __commonJS({
"node_modules/yaml/dist/visit.js"(exports) { "node_modules/yaml/dist/visit.js"(exports) {
"use strict"; "use strict";
var identity = require_identity(); var identity = require_identity();
var BREAK = Symbol("break visit"); var BREAK = /* @__PURE__ */ Symbol("break visit");
var SKIP = Symbol("skip children"); var SKIP = /* @__PURE__ */ Symbol("skip children");
var REMOVE = Symbol("remove node"); var REMOVE = /* @__PURE__ */ Symbol("remove node");
function visit(node, visitor) { function visit(node, visitor) {
const visitor_ = initVisitor(visitor); const visitor_ = initVisitor(visitor);
if (identity.isDocument(node)) { if (identity.isDocument(node)) {
@ -5566,9 +5570,9 @@ var require_cst_stringify = __commonJS({
var require_cst_visit = __commonJS({ var require_cst_visit = __commonJS({
"node_modules/yaml/dist/parse/cst-visit.js"(exports) { "node_modules/yaml/dist/parse/cst-visit.js"(exports) {
"use strict"; "use strict";
var BREAK = Symbol("break visit"); var BREAK = /* @__PURE__ */ Symbol("break visit");
var SKIP = Symbol("skip children"); var SKIP = /* @__PURE__ */ Symbol("skip children");
var REMOVE = Symbol("remove item"); var REMOVE = /* @__PURE__ */ Symbol("remove item");
function visit(cst, visitor) { function visit(cst, visitor) {
if ("type" in cst && cst.type === "document") if ("type" in cst && cst.type === "document")
cst = { start: cst.start, value: cst.value }; cst = { start: cst.start, value: cst.value };

View File

@ -1,8 +1,10 @@
import path from 'node:path'; import path from 'node:path';
import fsp from 'node:fs/promises';
import { EXIT, BmadModuleError } from './lib/exit.mjs'; import { EXIT, BmadModuleError } from './lib/exit.mjs';
import { findBmadDir } from './lib/bmad-dir.mjs'; import { findBmadDir } from './lib/bmad-dir.mjs';
import { parseSource, materializeSource } from './lib/source.mjs'; import { parseSource, materializeSource } from './lib/source.mjs';
import { readAndValidateManifest } from './lib/plugin-json.mjs'; import { readAndValidateManifest, validateManifestObject, hasBmadPluginJson } from './lib/plugin-json.mjs';
import { resolveLegacyModule } from './lib/legacy-resolver.mjs';
import { readUserIgnores, buildIgnoreMatcher, buildCopyPlan, rewriteManifestPaths, validateDeclaredPaths } from './lib/install-plan.mjs'; import { readUserIgnores, buildIgnoreMatcher, buildCopyPlan, rewriteManifestPaths, validateDeclaredPaths } from './lib/install-plan.mjs';
import { stageCopyPlan, atomicSwapDir, sha256File, pruneEmptyDirs, safePathInsideRoot } from './lib/fs-safe.mjs'; import { stageCopyPlan, atomicSwapDir, sha256File, pruneEmptyDirs, safePathInsideRoot } from './lib/fs-safe.mjs';
import { import {
@ -70,7 +72,29 @@ async function updateOne(bmadDir, projectDir, entry, opts) {
return; return;
} }
const manifest = await readAndValidateManifest(materialized.dir); // Re-read the manifest the same way install does: new-spec modules carry a
// `.claude-plugin/plugin.json#bmad`; legacy modules carry a
// `.claude-plugin/marketplace.json` resolved into a synthetic manifest. A
// legacy-installed module re-clones to the legacy format, so update must
// handle both — otherwise readAndValidateManifest throws BAD_MANIFEST and
// every legacy community module becomes un-updatable.
let manifest;
let synthesized = null;
if (await hasBmadPluginJson(materialized.dir)) {
manifest = await readAndValidateManifest(materialized.dir);
} else {
const legacy = await resolveLegacyModule(materialized.dir, { selector: code });
if (!legacy) {
throw new BmadModuleError(
EXIT.BAD_MANIFEST,
`no .claude-plugin/plugin.json#bmad and no .claude-plugin/marketplace.json at ${materialized.dir}`,
);
}
// Legacy first-party modules (gds, bmm, …) legitimately use reserved codes.
validateManifestObject(legacy.manifest, { allowReserved: true });
manifest = legacy.manifest;
synthesized = legacy.synthesized;
}
if (manifest.bmad.code !== code) { if (manifest.bmad.code !== code) {
throw new BmadModuleError( throw new BmadModuleError(
EXIT.PREFIX_COLLISION, EXIT.PREFIX_COLLISION,
@ -103,6 +127,18 @@ async function updateOne(bmadDir, projectDir, entry, opts) {
); );
} }
// Strategy-5 legacy modules have no module.yaml/module-help.csv on disk —
// the resolver synthesized them. Write them into the throwaway temp source so
// buildCopyPlan/validateDeclaredPaths discover them via the normal path.
if (synthesized) {
if (synthesized['module.yaml']) {
await fsp.writeFile(path.join(materialized.dir, 'module.yaml'), synthesized['module.yaml'], 'utf8');
}
if (synthesized['module-help.csv']) {
await fsp.writeFile(path.join(materialized.dir, 'module-help.csv'), synthesized['module-help.csv'], 'utf8');
}
}
// Build new copy plan, stage, swap. // Build new copy plan, stage, swap.
validateDeclaredPaths(materialized.dir, manifest); validateDeclaredPaths(materialized.dir, manifest);
const userIgnores = await readUserIgnores(materialized.dir, manifest); const userIgnores = await readUserIgnores(materialized.dir, manifest);

View File

@ -14,6 +14,7 @@
import { parseSource } from '../src/core-skills/bmad-module/scripts/lib/source.mjs'; import { parseSource } from '../src/core-skills/bmad-module/scripts/lib/source.mjs';
import { valid, prerelease, compare, rcompare, validRange } from '../src/core-skills/bmad-module/scripts/lib/semver-lite.mjs'; import { valid, prerelease, compare, rcompare, validRange } from '../src/core-skills/bmad-module/scripts/lib/semver-lite.mjs';
import { parseGitHubRepo, normalizeStableTag } from '../src/core-skills/bmad-module/scripts/lib/channel-resolver.mjs'; import { parseGitHubRepo, normalizeStableTag } from '../src/core-skills/bmad-module/scripts/lib/channel-resolver.mjs';
import { formatTomlValue, parseTomlScalar } from '../src/core-skills/bmad-module/scripts/lib/config-gen.mjs';
const colors = { reset: '', green: '', red: '', cyan: '', dim: '' }; const colors = { reset: '', green: '', red: '', cyan: '', dim: '' };
let passed = 0; let passed = 0;
@ -152,6 +153,18 @@ eq(
'normalizeStableTag excludes prereleases/invalid', 'normalizeStableTag excludes prereleases/invalid',
); );
// ─── config-gen TOML scalar round-trip ────────────────────────────────────────
// parseTomlScalar must be the exact inverse of formatTomlValue. Regression guard
// for the unescape bug where a literal backslash followed by `n` (e.g. a Windows
// path segment `\new`) was corrupted into a backslash + newline on read-back.
console.log(`\n${colors.cyan}config-gen TOML scalars${colors.reset}\n`);
for (const v of [String.raw`a\new`, 'x\ty', 'l1\nl2', 'say "hi"', String.raw`back\\slash`, 'trailing\\', 'plain', '']) {
eq(parseTomlScalar(formatTomlValue(v)), v, `TOML round-trip: ${JSON.stringify(v)}`);
}
eq(parseTomlScalar('true'), true, 'parseTomlScalar bare true');
eq(parseTomlScalar('42'), 42, 'parseTomlScalar bare number');
// ─── Summary ────────────────────────────────────────────────────────────────── // ─── Summary ──────────────────────────────────────────────────────────────────
console.log(`\n${colors.cyan}Results: ${passed} passed, ${failed} failed${colors.reset}\n`); console.log(`\n${colors.cyan}Results: ${passed} passed, ${failed} failed${colors.reset}\n`);
process.exit(failed > 0 ? 1 : 0); process.exit(failed > 0 ? 1 : 0);

View File

@ -162,6 +162,13 @@ platforms:
target_dir: .agents/skills target_dir: .agents/skills
global_target_dir: ~/.config/agents/skills global_target_dir: ~/.config/agents/skills
hermes:
name: "Hermes Agent"
preferred: false
installer:
target_dir: .agents/skills
global_target_dir: ~/.hermes/skills
iflow: iflow:
name: "iFlow" name: "iFlow"
preferred: false preferred: false

View File

@ -106,25 +106,25 @@ export default defineConfig({
label: 'Tutorials', label: 'Tutorials',
translations: { 'vi-VN': 'Hướng dẫn nhập môn', 'zh-CN': '教程', 'fr-FR': 'Tutoriels', 'cs-CZ': 'Tutoriály' }, translations: { 'vi-VN': 'Hướng dẫn nhập môn', 'zh-CN': '教程', 'fr-FR': 'Tutoriels', 'cs-CZ': 'Tutoriály' },
collapsed: false, collapsed: false,
autogenerate: { directory: 'tutorials' }, items: [{ autogenerate: { directory: 'tutorials' } }],
}, },
{ {
label: 'How-To Guides', label: 'How-To Guides',
translations: { 'vi-VN': 'Hướng dẫn tác vụ', 'zh-CN': '操作指南', 'fr-FR': 'Guides pratiques', 'cs-CZ': 'Praktické návody' }, translations: { 'vi-VN': 'Hướng dẫn tác vụ', 'zh-CN': '操作指南', 'fr-FR': 'Guides pratiques', 'cs-CZ': 'Praktické návody' },
collapsed: true, collapsed: true,
autogenerate: { directory: 'how-to' }, items: [{ autogenerate: { directory: 'how-to' } }],
}, },
{ {
label: 'Explanation', label: 'Explanation',
translations: { 'vi-VN': 'Giải thích', 'zh-CN': '概念说明', 'fr-FR': 'Explications', 'cs-CZ': 'Vysvětlení' }, translations: { 'vi-VN': 'Giải thích', 'zh-CN': '概念说明', 'fr-FR': 'Explications', 'cs-CZ': 'Vysvětlení' },
collapsed: true, collapsed: true,
autogenerate: { directory: 'explanation' }, items: [{ autogenerate: { directory: 'explanation' } }],
}, },
{ {
label: 'Reference', label: 'Reference',
translations: { 'vi-VN': 'Tham chiếu', 'zh-CN': '参考', 'fr-FR': 'Référence', 'cs-CZ': 'Reference' }, translations: { 'vi-VN': 'Tham chiếu', 'zh-CN': '参考', 'fr-FR': 'Référence', 'cs-CZ': 'Reference' },
collapsed: true, collapsed: true,
autogenerate: { directory: 'reference' }, items: [{ autogenerate: { directory: 'reference' } }],
}, },
// TEA docs moved to standalone module site; keep BMM sidebar focused. // TEA docs moved to standalone module site; keep BMM sidebar focused.
{ {

View File

@ -0,0 +1,8 @@
import { defineCollection } from 'astro:content';
import { docsLoader, i18nLoader } from '@astrojs/starlight/loaders';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }),
};

View File

@ -1,7 +0,0 @@
import { defineCollection } from 'astro:content';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ schema: docsSchema() }),
i18n: defineCollection({ type: 'data', schema: i18nSchema() }),
};

View File

@ -1,10 +1,10 @@
--- ---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import { getEntry } from 'astro:content'; import { getEntry, render } from 'astro:content';
import { translatedLocales } from '../lib/locales.mjs'; import { translatedLocales } from '../lib/locales.mjs';
const entry = await getEntry('docs', '404'); const entry = await getEntry('docs', '404');
const { Content } = await entry.render(); const { Content } = await render(entry);
--- ---
<StarlightPage frontmatter={{ title: entry.data.title, template: entry.data.template }}> <StarlightPage frontmatter={{ title: entry.data.title, template: entry.data.template }}>